引言
JavaScript的闭包是个很奇特的存在,这在我们平常学的其它语言中是不曾见过的;当然,也不用太神话它,觉得这是个什么牛逼的存在;不管遇到什么,只要在心中默念“这不过如此”,接下来就好学了。闭包本身比较简单,本文重在聊一聊为什么闭包得以存在,以及闭包的背后那些你可能会忽略的一些细节。
首先我们先宏观的认识一下闭包:
闭包即引用了作用域外的函数,准确来说应该是引用外部变量的函数+被引用的外部变量组成的整体;特性是即使外部函数被销毁,被引用的外部变量仍然存在。为什么会存在,不随着外部函数一起销毁呢?That's a question。所以研究闭包,必然从这里开始:垃圾回收
垃圾回收
为了不让问题变得太复杂,这里只简单讲讲回收算法;大家可能听过标记清除算法(没有听过的可以看看红宝书,不看也行,我稍微讲讲)。标记清除算法大致就是先给每个变量做标记,然后从全局作用域开始(这也是区别于引用计数,不会产生循环计数的关键所在)往下进行遍历,类似dfs;如果能够遍历到某个变量,说明它可被访问,取消其标记;之后仍然被标记的变量则被回收。(之后垃圾回收程序还会整理内存碎片,而且v8采用的垃圾回收算法是分代垃圾回收算法,将内存分为新生区和老生区,新生区有分为from和to...;当然这些对本问题不重要,感兴趣的可以看看三元的博客)。反正简单来说就是,因为可以被遍历到,所以没被清除。为什么可以遍历到呢?
可能有人开始瞎白话了,JavaScript采用的是静态作用域,静态作用域在函数定义时就已经确定其作用域了blabla。讲了和没讲一样。
让我们来看看红宝书是怎么讲的:闭包和作用域链密切相关,假设内部函数b定义在外部函数a的内部,它是一个内部函数,内部函数会将包含函数的活动对象添加到自己的作用域链中,这个过程大致是这样的:
- 外部函数
执行上下文入栈,创建变量对象 - 进入代码执行阶段,激活变量对象
- 内部函数此时会将全局变量对象和外部
活动对象预添加到[[scope]]中 - 等到内部函数被调用时,将
[[scope]]中的变量对象取出加入自己的作用域链,再将自己的活动对象推入作用域链前端,形成最终的作用域链 而b实际上是通过作用域链访问到的value;原因就在于在上面第三步时,内部函数预添加了外部作用域的变量对象。
关键来了:
内部函数被外部函数返回,假设被全局作用域某个变量接收,根据标记清除算法,从全局作用域开始dfs,一直可以层层追溯到外部函数的变量对象,所以不会被清除。这里不一定非得被全局变量接收,只要从全局作用域开始,能被访问到就行,比如被加入到了宏任务队列等待执行。
细心的同学注意到了上面提到的几个词:变量对象、活动对象、执行上下文、作用域链、内部函数,下面逐一解释。
-
执行上下文(Execution context)
-
执行上下文有两种,全局执行上下文(window对象)和函数执行上下文;每个上下文都有一个关联的变量对象(全局执行上下文的变量对象也是window对象),变量对象中存储着该执行上下文中定义的的所有变量和函数。
-
执行上下文的生命周期分为三个阶段:
- 创建阶段(创建变量对象,确定this指向以及其他需要的状态)
- 代码执行阶段(此时变量对象被激活为活动对象,活动对象中的属性此时能被访问)
- 销毁阶段(回收内存空间)
-
变量对象的创建过程也分为几个阶段:
- 创建
arguments对象 - 为函数
形参创建属性 - 搜索该执行上下文中的所有
函数声明并创建属性 - 搜索该执行上下文中所有的
变量声明并创建属性
//举个例子 function foo(a) { var b = 2; function c() {} var d = function() {}; } foo(1); //将调用过程拆解: //执行上下文入栈,进入创建阶段,创建变量对象如下: VO = { arguments: { 0: 1, length: 1 }, a: 1, b: undefined, c: reference to function c(){}, d: undefined } //进入代码执行阶段,成为活动对象,如下: AO = { arguments: { 0: 1, length: 1 }, a: 1, b: 2, c: reference to function c(){}, d: reference to function (){} } - 创建
-
变量对象和活动对象,它们本质是同一个东西,只是处于执行上下文不同的生命周期。变量对象是引擎意义上的,在JavaScript环境中是无法访问的,当执行上下文进入代码执行阶段,它所关联的变量对象会被激活,此时就成了活动对象
-
-
作用域链
-
JavaScript采用词法作用域,也就是静态作用域
-
作用域就是可访问变量、函数、对象的集合,javascript能定义全局作用域和局部作用域
-
作用域链,顾名思义,作用域的链表,准确来说是一个包含指针的列表,每个指针指向一个变量对象。我们都引用过全局变量,其实这便是访问了作用域链的结果,当引用某个变量时,首先会查找当前活动对象,逐层向外直至全局变量对象,这便是作用域链的作用。你可能会问作用域和活动对象是什么关系?我只能告诉你,JavaScript作用域是一个静态的概念,在函数定义时就已经确定了,它规定了如何查找变量。活动对象则是一个实体,是在函数调用时动态创建,会被加入到内部函数的作用域链当中,只要了解这一点即可,至于它们之间到底什么关系我形容不出来,意会,其实也没必要纠结。
-
可以看看这个例子:了解什么是静态作用域
var value = 1; function foo() { console.log(value); } function bar() { var value = 2; foo(); } bar();结果为1,你做对了吗?
原因:foo并不是定义在bar中的,如果你搞混了,说明你对闭包还是有误解,再回头看看。
-
-
内部函数
见名知意,嵌套在另外一个函数中的函数
参考文献
红宝书