JavaScript闭包(1):闭包的形成机制梳理

☆樱花仙子☆ 提交于 2020-10-06 03:13:48

闭包前置知识:作用域,作用域链,变量生命周期

从我自学前端以来,就有无数的人告诉我,闭包几乎是JavaScript中最重要的几个技术点之一,必须把把闭包掌握,才算是踏入JavaScript的大门。现在,让我们一起揭开闭包的神秘面纱,看看这到底是个什么机制。
在学习闭包前,我们需要对JavaScript的变量生命周期,作用域,作用域链等有一定的认识,最好对函数的执行过程也有一定的了解,虽然自认为学得一般,但我还是在学习的过程中进行了总结,并发布了两篇相关博客:

1.当我们调用函数的时候,js引擎为我们做了什么?

首先我们需要知道,在JavaScript中,当某个函数被调用时,引擎会做一些前置准备:

  1. 会创建一个执行环境(execution context)
  2. 使用函数命名的形参结合arguments,以及其他声明的参数,来初始化活动对象(activation object)
  3. 将全局对象,创建的活动对象添加到作用域链。

1.1举例说明

话不多说,直接上图:

var num0 = 0;
function fn1(num1, num2){
	function fn2(num3, num4){
		console.log(num0, num1, num2, num3, num4);
	}
	fn2(3,4);
}
fn1(1,2);//0 1 2 3 4

在这里插入图片描述

  1. 在上述代码中,我们先在全局环境声明了num1变量和fn1函数,又在fn1函数中声明了fn2函数,并留下了一句fn2(3,4)调用语句。
  2. 我们在声明fn1函数时,创建了一个预先包含全局变量的作用域链,并保存到fn1内部的[[Scope]]属性中;
  3. 然后,我们在全局使用fn1(1,2)调用函数fn1,js引擎为fn1创建一个执行环境,再复制fn1[[Scope]]属性中的变量对象构建起fn1执行环境的作用域链;
  4. 创建自己的变量对象作为活动对象,并推入fn1执行环境作用域链的前端。fn1(1,2)压进调用栈
  5. fn1(1,2)在执行过程中遇到自己函数体内的fn2声明语句,声明fn2,并将fn1[[Scope]]中保存的指针复制到自己的[[Scope]]属性中;
  6. fn1(1,2)继续执行,遇到遇到自己函数体内的fn2()调用语句,调用函数fn2(3,4),此时js引擎为fn2创建一个执行环境,再复制fn2[[Scope]]属性中的变量对象构建起fn2执行环境的作用域链;
  7. 创建自己的变量对象作为活动对象,并推入fn2执行环境作用域链的前端。fn2(3,4)压进调用栈
  8. fn2(3,4)执行结束,销毁fn2(3,4)的执行环境和活动对象等fn2(3,4)出调用栈;
  9. fn1(1,2)执行结束,销毁fn1(1,2)的执行环境和活动对象等fn1(1,2)出调用栈。

1.2 可能会引起的一些误解

PS:

  • 后台的每个执行环境都有一个表示变量的对象——变量对象。
  • 全局的变量对象始终存在,而像fn1()fn2()这样的局部环境的变量对象则只在函数执行时存在。
  • 作用域链本质上是一个指向变量对象的指针列表,它只是引用,并不实际包含变量对象。
  • fn2的执行环境不是创建在fn1(1,2)的活动对象中,而是创建在全局中。
  • fn2[[Scope]][1]虽然和fn1[[Scope]][0]指向同一个块,但实际上fn2[[Scope]][1]里并不包括fn2:xxxxx(function)这个属性。

再PS:
图中为了方便,只在执行环境(execution context)中写了[[Scope]]一种属性,并不意味着执行环境只有这一种属性,实际上执行环境还有许多属性记录函数的各种运行参数,如函数的调用栈、调用方式、以及我们熟悉的this属性等。

2.JavaScript的垃圾回收机制——标记清除机制(mark-and-sweep)

和C/C++不同,JavaScript语言具有自动垃圾收集机制。这种垃圾收集机制的回收原则是——找出那些不再继续使用的变量,然后释放其占用的内存。

在上文的例子中,fn2fn1的执行环境在最后执行完毕时,都被销毁。这里涉及到JavaScript的一种垃圾回收机制——标记清除机制(mark-and-sweep)——这也是JavaScript最常用的一种垃圾回收机制。

正如上段所说,垃圾收集机制的回收原则是:释放不再使用的变量

那么我们要如何判断一个变量是否还要使用呢?常用的方法是判断其是否在执行环境中。垃圾收集器在运行的时候,会给所有变量都打上标记,然后再去除以下两种变量的标记:
(1). 在环境中的变量
(2). 被环境中的变量引用的变量

很显然,在js中,全局执行环境是在页面打开时创建,页面关闭时才销毁的。所以,在大多数时候,只有能在全局环境下直接访问(或通过引用访问)的变量会被清除标记。 最后,回收器会释放所有持有标记的变量,以达到垃圾回收的目的。

上述例子中,fn1(1,2)fn2(3,4)执行完后,执行环境销毁,对应的变量对象(活动对象)失去了从全局环境开始的可达性,无法从仅存的执行环境(即全局环境)访问的变量对象被标记,从而被销毁。

3.闭包(Closure)的形成——赋予本该销毁的活动对象以全局可达性

把函数调用过程和垃圾回收了解完后,我们终于可以聊聊闭包了!可喜可贺!!如果说闭包是RPG游戏的关底boss,那你此时已经升到可以一刀砍死boss的等级了。一刀999,是兄弟就来砍我。

3.1举例说明

还是上述的例子,只是这次要做一点改动

var num0 = 0;
function fn1(num1, num2){
	function fn2(num3, num4){
		console.log(num0, num1, num2, num3, num4);
	}
	return fn2;
}
var closureFn = fn1(1,2);

在这里插入图片描述
可以看到,在上述例子中,fn1(1,2)的活动对象本该在函数执行完毕后推出环境,但因为在全局声明了一个对fn2函数的引用,而fn2函数声明时在自己的[[Scope]]存放了指向fn1(1,2)活动对象的指针,该活动对象被赋予了全局可达性,即它存在于执行环境中,故没有被销毁。
基于这种特性,我们可以在全局环境下访问到fn1里面声明的num1num2变量,打破了外层作用域不能访问内层作用域的规则。
如下:


closureFn(3,4);// 0 1 2 3 4

3.2闭包(Closure)的本质

虽然说的复杂,但说白了,闭包就是基于JavaScript使用词法作用域和函数作用域的机制,而使用函数来保存本该消除的变量。这就是所谓的闭包。
如下:

console.dir(closureFn);

在这里插入图片描述

本篇只是先简单通过两个例子对闭包的形成机制作一个梳理,暂未对闭包的各类实现方式和应用场景做太多探讨。

欲知后事如何,且听下回分解

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!