JS执行上下文与闭包的理解
执行上下文
所谓的执行上下文,其实就是代码运行的环境,JS 执行上下文分为全局执行上下文,函数执行上下文以及 eval 执行上下文。
全局执行上下文
JS 程序一开始执行之前,JS 引擎首先就会对待吗进行预解析,生成一个全局执行上下文环境,在此过程中,JS 引擎会做这些事情:
- 将 var 定义的全局变量添加为全局对象的属性,并且赋值为 undefined。
- 将 function 生命的全局函数添加为全局对象的方法,并且赋值 function(也就是说 function 声明的全局函数会直接被赋值为全局对象的方法,并且可以直接调用)。
- 将 this 赋值给全局对象。
- 开始执行代码。
理解为全局对象添加属性和方法
上面说到 var 生命的变量和 function 声明的函数都会在预解析的时候被挂载为全局对象的上,var 变量和 function 函数之所以一个只是挂载声明而函数是挂载整个函数声明和函数体也很好理解。因为 var 声明变量,什么叫做变量?变量其实就是内存中的一块用来存储数据的空间,因此 JS 引擎在预解析的时候碰到 var 声明的全局变量的时候相当于得到了需要开辟这样一个内存空间的信息,因此会在内存中占位这样一块空间,但是不会赋值。而对于 function 声明的全局函数,因为函数拥有自己的执行上下文,而且 function 声明的函数通常需要在声明之前就被调用,因此函数整个函数体都需要被挂在在全局对象。因此看以下代码:
console.log(a); // undefined
if (true) {
var a = 10;
console.log(a); // 10
}
console.log(a); // 10
b(); // B
function b() {
console.log("B");
}
上面的代码中第一行直接访问 a 变量的值得到 undefined,这个很好理解因为 js 引擎在内存开辟了空间但是没有赋值,所以为 undefined,然后执行到第四句,由于变量的声明在 JS 预解析的阶段已经完成了,所以这句代码在执行的时候实际上就是给变量 a 赋值为 10,然后访问 a 的值自然就是 10 了,然后下面还有 function 声明的函数 b,由于 function 生命的函数会被挂载为全局对象的方法,因此全局调用 b 方法就相当于 this.b()。
变量提升
其实上面的例子就已经解释了什么叫做变量提升,明明 a 变量是在判断逻辑代码块里面声明的,但是外部却可以访问到,这种就属于变量提升,类似上面的例子,变量提升完全可以用执行上下文去解释。
暂时性死区
我们经常讲 var 声明的全局变量会存在变量提升(因为再预解析的时候被添加为全局对象的属性),而 let 和 const 声明的变量是不存在变量提升的。这种说法其实并不完全正确,实际上无论是 var 还是 let 和 const 生命的变量都会再预解析的时候被创建,然而由于 js 引擎再碰到 let 和 const 声明的变量的时候,虽然会创建这个变量,但会在 js 代码执行的时候限制变量的访问,直到执行到声明该变量的语句(这里注意预解析和执行两种不同环境)。执行到声明语句之前不能访问该变量,这期间就叫做暂时性死区。let 和 const 声明的变量是不存在变量提升的。这种说法其实并不完全正确,只是由于暂时性死区的存在导致他们和 var 变量之间特性存在差异。
先看第一段代码
if (true) {
console.log(a); // a is not defined
// let a = 10;
}
运行这段代码得到报错:a is not defined,这显然没问题,因为这段代码中既找不到全局 var 声明的变量 a,也没有 let 或者 const 声明的变量 a,因此这里报错“a 没有定义”显然合理。再看下一段代码:
if (true) {
console.log(a); // Cannot access 'a' before initialization
let a = 10;
}
上面这段代码和之前的区别就是在访问 a 之后又声明了 a,得到的结果同样是报错,区别是报错的信息从a is not defined
变成了Cannot access 'a' before initialization
,这句话的意思是再 a 声明之前无法访问 a 变量,这说明 js 引擎再执行到console.log(a)
这句代码的时候是知道存在 a 这个变量的,只是他限制了我们在执行到 a 的声明之前是无法使用 a 变量的,这期间就叫做暂时性死区。
全局中的 this 指向
全局中的 this 指向的是 window 对象(浏览器环境,NodeJs 则为 global 对象,这个具体是什么要看 JS 的 runtime 的),下面的代码可以证明:
console.log(this === window); // true
var a = 10;
console.log(this.a); // 10
this.a = 11;
console.log(a); // 11
console.log(window.a); // 11
函数执行上下文
对于函数执行上下文的理解:JS 中,函数每次运行都会在准备执行函数体之前创建对应的函数执行上下文(也就是),每次运行完成后,产生的剩下文会被销毁,上下文中定义的变量函数等将会被垃圾回收机制(GC)回收(除非被闭包出去,下面会解释什么叫做闭包),在函数执行上下文中:
- JS 引擎会将函数的形参变量添加为执行上下文的属性;
- 将函数体内声明的 var 变量添加为执行上下文的属性,赋值 undefined;
- function 生命的函数添加为执行上下文的方法,赋值 function;
- 将 this 指向调用函数的对象(箭头函数除外。箭头函数是在声明的时候咎确定了 this,除此之外还可以通过 call、apply 以及 bind 方法来强行改变函数的 this 指向);
- 开始执行函数体代码;
第二点和第三点的理解参照全局执行上下文中的 var 变量和 function 声明的函数来理解,这里不再赘述。
第一点,函数形参添加为执行上下文的属性,因此可以再函数体内直接使用形参变量。this 指向调用函数的对象:
function fn(a) {
let b = 0;
console.log(a, b);
}
fn(); // undefined 0
fn(1); // 1 0
第一次调用的时候,会创建一个对应的上下文,形参变量 a 会被添加为该执行上下文的属性,但由于未传入实参,因此 a 违背赋值,依然是 undefined,至此,此次调用完毕,该执行上下文以及属性 a 以及变量 b 都将被 GC 回收销毁。
第二次调用的时候,又创建一个对应的上下文,并且由于传入了实参 a 因此打印结果为1 0
,至此,此次调用完毕,该执行上下文以及属性 a 以及变量 b 都将被 GC 回收销毁。
从函数执行上下文谈闭包
讲了上面的执行上下文以及垃圾回收机制(这个可以自行了解,大致意思就是垃圾回收中心会找到不会在被用到的变量或者数据,回收他们并销毁,防止内存泄露)后,我们可以从函数执行上下文的角度去解释以下什么叫做闭包:
函数执行会创建一个函数执行上下文,一般地,函数执行结束后该上下文会被销毁,此次调用在上下文中生成的变量等 JS 数据会被销毁(垃圾回收中心回收),但是如果由于某种情况,导致变量没能被回收,这就产生了闭包,比如,该函数返回了一个内部函数,并且该内部函数引用了该函数的变量,导致垃圾回收中心无法直接回收该变量,这就形成了闭包。请看下面代码:
function fn() {
let count = 0;
return function () {
count++;
console.log(count);
};
}
const foo = fn();
foo(); // 1
foo(); // 2
foo(); // 3
这既是结构最简单的闭包,我们在调用函数 fn 的时候,函数执行上下文里面有局部变量 count,但是由于该函数返回的函数里面引用了该变量,因此导致该变量无法被回收,就形成了闭包。我们下面三次调用 foo 方法得到的结果也证明了该变量没有被销毁。这就叫做闭包。