JS基础篇之作用域、执行上下文、this、闭包
前言:JS 的作用域、执行上下文、this、闭包是老生常谈的话题,也是新手比较懵懂的知识点。当然即便你作为老手,也未必真的能理解透彻这些概念。
一、作用域和执行上下文
作用域:
js中的作用域是词法作用域,即由 函数声明时 所在的位置决定的。词法作用域是指在编译阶段就产生的,一整套函数标识符的访问规则。(区别于词法作用域,动态作用域是在函数执行的时候确认的,js的没有动态作用域,但js的this很像动态作用域,后面会提到。词法作用域的概念十分重要,请多加记忆并理解。) 说到底js的作用域只是一个“空地盘”,其中并没有真实的变量,但是却定义了变量如何访问的规则。
作用域链本质上是一个指向变量对象的指针列表,它只引用不包含实际变量对象,是作用域概念的延申。作用域链定义了变量在当前上下文访问不到的时候如何沿作用域链继续查询的一套规则。
执行上下文:
执行上下文是指 函数调用时 产生的变量对象,这个变量对象我们无法直接访问,但是可以访问其中的变量、this对象等。例如:
let fn, bar; // 1、进入全局上下文环境
bar = function(x) {
let b = 5;
fn(x + b); // 3、进入fn函数上下文环境
};
fn = function(y) {
let c = 5;
console.log(y + c); //4、fn出栈,bar出栈
};
bar(10); // 2、进入bar函数上下文环境

每次函数调用时,都会产生一个新的执行上下文环境,JavaScript引擎会以栈的方式来处理它们,这个栈,我们称其为函数调用栈(call stack)。栈底永远都是全局上下文,而栈顶就是当前处于活动状态的正在执行的上下文,也称为活动对象(running execution context,图中蓝色的块),区别与底下被挂起的上下文(变量对象)。
总结:作用域是在函数声明的时候就确定的一套变量访问规则,而执行上下文是函数执行时才产生的一系列变量的环境。也就是说作用域定义了执行上下文中的变量的访问规则,执行上下文在这个作用域规则的前提下进行变量查找,函数引用等具体操作。
总结:作用域是在函数声明的时候就确定的一套变量访问规则,而执行上下文是函数执行时才产生的一系列变量的环境。也就是说作用域定义了执行上下文中的变量的访问规则,执行上下文在这个作用域规则的前提下进行变量查找,函数引用等具体操作。
理解函数的执行过程
函数的执行过程分成两部分,一部分用来生成执行上下文环境,确定this的指向、声明变量以及生成作用域链;另一部分则是按顺序逐行执行代码。
建立执行上下文阶段:(发生在 函数被调用时 && 函数体内的代码执行前 )
- 生成变量对象,顺序:创建 arguments 对象 --> 创建function函数声明 --> 创建var变量声明
- 生成作用域链
- 确定this的指向
函数执行阶段:
- 逐行执行代码,这个阶段会完成变量赋值,函数引用,以及执行其他代码。
二、this指向
关于js的this关键字,我记得第一次接触还是在做前端半年或一年的时候(哈哈我就是这么水)。那时候徐哥(java大佬)教我在绑定click事件的时候把this传给事件处理函数,类似<button onclick="handle(this)">确认</button>,我当时就懵了,this是什么鬼?!从此正式开启了我三年的js痛苦之旅:封装啊、闭包啊、面向对象啊、继承啊等等等等。this的指向说来说去其实只有四种:
let fn = function(){
alert(this.name)
}
let obj = {
name: '',
fn
}
fn() // 方法1
obj.fn() // 方法2
fn.call(obj) // 方法3
let instance = new fn() // 方法4
- 方法1中直接调用函数
fn(),这种看着像光杆司令的调用方式,this指向window(严格模式下是undefined)。 - 方法2中是点调用
obj.fn(),此时this指向obj对象。点调用中this指的是点前面的对象。 - 方法3中利用
call函数把fn中的this指向了第一个参数,这里是obj。即利用call、apply、bind函数可以把函数的this变量指向第一个参数。 - 方法4中用
new实例化了一个对象instance,这时fn中的this就指向了实例instance。
如果同时发生了多个规则怎么办?其实上面四条规则的优先级是递增的:
fn() < obj.fn() < fn.call(obj) < new fn()
首先,new调用的优先级最高,只要有new关键字,this就指向实例本身;接下来如果没有new关键字,有call、apply、bind函数,那么this就指向第一个参数;然后如果没有new、call、apply、bind,只有obj.foo()这种点调用方式,this指向点前面的对象;最后是光杆司令foo() 这种调用方式,this指向window(严格模式下是undefined)。
es6中新增了箭头函数,而箭头函数最大的特色就是没有自己的this、arguments、super、new.target,并且箭头函数没有原型对象prototype不能用作构造函数(new一个箭头函数会报错)。因为没有自己的this,所以箭头函数中的this其实指的是包含函数中的this。无论是点调用,还是call调用,都无法改变箭头函数中的this。
三、闭包
js的闭包是新手的噩梦,在学js的前三年,我查阅了无数的博文,苦苦搜索闭包的概念,然而最终一无所获。MDN上这样定义闭包:闭包是函数和声明该函数的词法环境的组合。
what?能说人话吗?
很长时间以来我对闭包都停留在“定义在一个函数内部的函数”这样肤浅的理解上。事实上这只是闭包形成的必要条件之一。直到后来看了kyle大佬的《你不知道的JAVASCRIPT》上册中关于闭包的定义,我才豁然开朗:
当函数能够记住并访问所在的词法作用域时,就产生了闭包。
let single = (function(){
let count = 0
return {
plus(){
count++
return count
},
minus(){
count--
return count
}
}
})()
single.plus() //
single.minus() //
这是个单例模式,这个模式返回了一个对象并赋值给变量single,变量single中包含两个函数plus和minus,而这两个函数都用到了所在词法作用域中的变量count。正常情况下count和所在的执行上下文会在函数执行结束时被销毁,但是由于count还在被外部环境使用,所以在函数执行结束时count和所在的执行上下文不会被销毁,这就产生了闭包。每次调用single.plus()或者single.minus(),就会对闭包中的count变量进行修改,这两个函数就保持住了对所在的词法作用域的引用。
闭包其实是一种特殊的函数,它可以访问函数内部的变量,还可以让这些变量的值始终保持在内存中,不会在函数调用后被垃圾回收机制清除。
看个经典案例:
// 方法1
for (var i = 1; i <= 5; i++) {
setTimeout(function() {
console.log(i)
}, 1000)
}
// 方法2
for (let i = 1; i <= 5; i++) {
setTimeout(function() {
console.log(i)
}, 1000)
}
方法1中,循环设置了五个定时器,一秒后定时器中回调函数将执行,打印变量i的值。毋庸置疑,一秒之后i已经递增到了5,所以定时器打印了五次5 。(定时器中并没有找到当前作用域的变量i,所以沿作用域链找到了全局作用域中的i)
方法2中,由于es6的let会创建局部作用域,所以循环设置了五个作用域,而五个作用域中的变量i分布是1-5,每个作用域中又设置了一个定时器,打印一秒后变量i的值。一秒后,定时器从各自父作用域中分别找到的变量i是1-5 。这是个利用闭包解决循环中变量发生异常的新方法。
最后
我真的学不动了。
链接:https://juejin.im/post/5cf8612df265da1bcb4f1bf8
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
JS基础篇之作用域、执行上下文、this、闭包的更多相关文章
- JS底层知识理解之执行上下文篇
JS底层知识理解之执行上下文篇 一.什么是执行上下文(Execution Context) 执行上下文可以理解为当前代码的执行环境,它会形成一个作用域. 二.JavaScript引擎会以什么方式去处理 ...
- 前端面试题目汇总摘录(JS 基础篇)
JS 基础 JavaScript 的 typeof 返回那些数据类型 object number function boolean undefined string typeof null; // o ...
- JS基础篇--sort()方法的用法,参数以及排序原理
JS基础篇--sort()方法的用法,参数以及排序原理 sort() 方法用于对数组的元素进行排序,并返回数组.默认排序顺序是根据字符串Unicode码点.语法:arrayObject.sort( ...
- js的基础(平民理解的执行上下文/调用堆栈/内存栈/值类型/引用类型)
与以前的切图比较,现在的前端开发对js的要求似乎越来越高,在开发中,我们不仅仅是要知道如何运用现有的框架(react/vue/ng), 而且我们对一些基础的知识的依赖越来越大. 现在我们就用平民的方法 ...
- 前端面试题目汇总摘录(JS 基础篇 —— 2018.11.02更新)
温故而知新,保持空杯心态 JS 基础 JavaScript 的 typeof 返回那些数据类型 object number function boolean undefined string type ...
- js基础篇——call/apply、arguments、undefined/null
a.call和apply方法详解 call方法: 语法:call([thisObj[,arg1[, arg2[, [,.argN]]]]]) 定义:调用一个对象的一个方法,以另一个对象替换当前对象 ...
- js 基础篇(点击事件轮播图的实现)
轮播图在以后的应用中还是比较常见的,不需要多少行代码就能实现.但是在只掌握了js基础知识的情况下,怎么来用较少的而且逻辑又简单的方法来实现呢?下面来分析下几种不同的做法: 1.利用位移的方法来实现 首 ...
- JS基础知识(作用域/垃圾管理)
1.js没有块级作用域 if (true) { var color = “blue”; } alert(color); //”blue” for (var i=0; i < 10; i++){ ...
- Vue.js基础篇实战--一个ToDoList小应用
距离开始学Vue已经过去一个多月了,总想把学到的东西柔和在一起,做点东西出来,于是有了这个Todolist小应用. 使用vuex 纯粹基础,没有用到web pack,vuex,npm,下次把它改造一下 ...
随机推荐
- 8、1 周末总结+Mongdb
都说加一个Id 注解就行了,其实还要加一条数据测试表是否生成 这里我们探讨 数据库是谁,表是谁 数据库在mongo启动的时候就指定了,这个无需我们关心 表根据实体类自动生成, 1.pom.xml & ...
- PMBOK(第六版) PMP备考知识总汇!
记录本人学习PMBOK第六版的学习笔记. 备考知识总汇! PMBOK序章 PMP备考指南之相关事项介绍 PMP备考指南之第一章:引论 PMP备考指南之第二章:项目运作环境 PMP备考指南之第三章:项目 ...
- Python-11-生成器
一.定义 可以理解为一种数据类型,这种数据类型自动实现了迭代器协议(其他数据类型需要调用__iter__方法),所以生成器就是一种迭代器. 二.生成器的两种形式 1. 生成器函数 使用yield代替r ...
- redis复制机制
摘自redis设计与实现 通过客户端,发送slave of xxx给redis从服务器,即可实现主从服务器之间的复制.如果主服务器设置了requirepass进行身份验证,从服务器需要设置master ...
- T100-----调试程序,快速定位到错误行
1.r.d 作业编码 2.ctrl+d3.输入 watch g_errparam.code if g_errparam.code='错误编码', 点几次OK, 再直接run程序,会自动跳 ...
- response letter
1.Firstly, we would like to thank you for your kind letter and for reviewers’ constructive commentsc ...
- golang---获取windows系统相关信息
package main import ( "fmt" "net" "runtime" "strings" " ...
- java之hibernate之基于外键的一对一单向关联映射
这篇讲解基于外键的一对一单向关联映射 1.考察如下信息,人和身份证之间是一个一对一的关系.表的设计 注意:基于外键的一对一关联的表结构和多对一的表结构是一致的,但是,外键是唯一的. 2.类的结构 Pe ...
- sql group by hour 按小时分组统计
Time字段以小时分组统计 select datepart(hour,time) hour,count(1) count from table where Similarity<75 group ...
- 怎么对ZYNQ的FCLK做时钟组约束
前言 对于包含PS和PL的设计,两者的数据交互PL必然会用到PS端的时钟. 对于FCLK(PS端时钟输入到PL端)的约束,此时钟的基础约束已在IP中产生.以下想约束其异步时钟的时钟组特性. 注意事项: ...