欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

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

程序员文章站 2022-03-30 09:45:41
JavaScript闭包详解闭包前置知识:作用域,作用域链,变量生命周期1.当我们调用函数的时候,js引擎为我们做了什么?2.JavaScript的垃圾回收机制闭包前置知识:作用域,作用域链,变量生命周期从我自学前端以来,就有无数的人告诉我,闭包几乎是JavaScript中最重要的几个技术点之一,必须把把闭包掌握,才算是踏入JavaScript的大门。现在,让我们一起揭开闭包的神秘面纱,看看这到底是个什么机制。在学习闭包前,我们需要对JavaScript的变量生命周期,作用域,作用域链等有一定的认识,...

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

从我自学前端以来,就有无数的人告诉我,闭包几乎是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

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

  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);

荐
                                                        JavaScript闭包(1):闭包的形成机制梳理
可以看到,在上述例子中,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);

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

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

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

本文地址:https://blog.csdn.net/DDboom/article/details/107236078