JavaScript闭包的形成机制梳理
闭包前置知识:作用域,作用域链,变量生命周期
从我自学前端以来,就有无数的人告诉我,闭包几乎是JavaScript中最重要的几个技术点之一,必须把把闭包掌握,才算是踏入JavaScript的大门。现在,让我们一起揭开闭包的神秘面纱,看看这到底是个什么机制。
在学习闭包前,我们需要对JavaScript的变量生命周期,作用域,作用域链等有一定的认识,最好对函数的执行过程也有一定的了解,虽然自认为学得一般,但我还是在学习的过程中进行了总结,并发布了两篇相关博客:
1.当我们调用函数的时候,js引擎为我们做了什么?
首先我们需要知道,在JavaScript中,当某个函数被调用时,引擎会做一些前置准备:
- 会创建一个执行环境(
execution context
) - 使用函数命名的形参结合
arguments
,以及其他声明的参数,来初始化活动对象(activation object
) - 将全局对象,创建的活动对象添加到作用域链。
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
- 在上述代码中,我们先在全局环境声明了
num1
变量和fn1
函数,又在fn1
函数中声明了fn2
函数,并留下了一句fn2(3,4)
调用语句。 - 我们在声明
fn1
函数时,创建了一个预先包含全局变量的作用域链,并保存到fn1
内部的[[Scope]]
属性中; - 然后,我们在全局使用
fn1(1,2)
调用函数fn1
,js引擎为fn1
创建一个执行环境,再复制fn1
的[[Scope]]
属性中的变量对象构建起fn1
执行环境的作用域链; - 创建自己的变量对象作为活动对象,并推入
fn1
执行环境作用域链的前端。将fn1(1,2)
压进调用栈; fn1(1,2)
在执行过程中遇到自己函数体内的fn2
声明语句,声明fn2
,并将fn1
的[[Scope]]
中保存的指针复制到自己的[[Scope]]
属性中;fn1(1,2)
继续执行,遇到遇到自己函数体内的fn2()
调用语句,调用函数fn2(3,4)
,此时js引擎为fn2
创建一个执行环境,再复制fn2
的[[Scope]]
属性中的变量对象构建起fn2
执行环境的作用域链;- 创建自己的变量对象作为活动对象,并推入
fn2
执行环境作用域链的前端。将fn2(3,4)
压进调用栈 fn2(3,4)
执行结束,销毁fn2(3,4)
的执行环境和活动对象等,fn2(3,4)
出调用栈;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语言具有自动垃圾收集机制。这种垃圾收集机制的回收原则是——找出那些不再继续使用的变量,然后释放其占用的内存。
在上文的例子中,fn2
和fn1
的执行环境在最后执行完毕时,都被销毁。这里涉及到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
里面声明的num1
和num2
变量,打破了外层作用域不能访问内层作用域的规则。
如下:
closureFn(3,4);// 0 1 2 3 4
3.2闭包(Closure
)的本质
虽然说的复杂,但说白了,闭包就是基于JavaScript使用词法作用域和函数作用域的机制,而使用函数来保存本该消除的变量。这就是所谓的闭包。
如下:
console.dir(closureFn);
本篇只是先简单通过两个例子对闭包的形成机制作一个梳理,暂未对闭包的各类实现方式和应用场景做太多探讨。
欲知后事如何,且听下回分解
来源:oschina
链接:https://my.oschina.net/u/4364439/blog/4358052