JavaScript的闭包


闭包是函数式编程中特有的东西,同业也是JavaScript的特色,很多高级应用都要依靠闭包实现。以前看过很多关于闭包的解释,大多解释得比较含糊,没有说到底层的原理上或是对底层原理一笔带过,导致会有似懂非懂,似是而非的感觉。闭包还涉及到另一个重要的知识点,就是JavaScript中上下文和运行时如何找上下文的问题。这把这个问题搞清楚,才能解释闭包的问题。

上下文

当我们启动程序时,我们从全局执行上下文中开始。一些变量是在全局执行上下文中声明的。我们称之为全局变量。当程序调用一个函数时,会发生什么?有以下几个步骤:

  1. JavaScript创建一个新的本函数的执行上下文,我们叫作本地执行上下文。
  2. 这个本地执行上下文将有它自己的一组变量,这些变量将是这个执行上下文的本地变量。
  3. 新的执行上下文被推到到执行堆栈中。可以将执行堆栈看作是一种保存程序在其执行中的位置的容器。

函数什么时候结束?当它遇到一个return语句或一个结束括号}。当一个函数结束时,会发生以下情况:

  1. 这个函数的本地执行上下文从执行堆栈中弹出。
  2. 函数将返回值返回给外层的调用上下文(调用上下文是调用这个本地的执行上下文),它可以是全局执行上下文,也可以是另外一个本地的执行上下文。返回的值可以是一个对象、一个数组、一个函数、一个布尔值等等,如果函数没有return语句,则返回undefined
  3. 函数的本地执行上下文被销毁,销毁是很重要,这个本地执行上下文中声明的所有变量都将被删除,不再有变量。实际上就是被垃圾回收器回收了,释放内存空间。

词法作用域

任何语言的变量都有作用域,作用域机制是为了方便垃圾回收器回收的,在词法上分为两种作用域:

  • 全局作用域 —— 第一次执行代码的默认环境(在浏览器中最外层就是window,即var声明的变量)
  • 函数作用域 —— 当执行流进入函数体时

fn(…) —— 当调用方法时,我们把当前 执行上下文 认作是当前代码执行的一个环境与作用域。举个栗子,我们看一下下面的代码:

 let val1 = 2
 function multiplyThis(n) {
   let ret = n * val1
   return ret
 }
 let multiplied = multiplyThis(6)
 console.log('example of scope:', multiplied)

为了理解JavaScript引擎是如何工作的以及理解词法作用域的一些知识。,让我们详细分析一下:

  1. 在全局执行上下文中声明一个新的变量val1,并将其赋值为2
  2. 2-5行,声明一个新的变量 multiplyThis,并给它分配一个函数定义。
  3. 6行,声明一个在全局执行上下文 multiplied 新变量。
  4. 从全局执行上下文内存中查找变量multiplyThis,并将其作为函数执行,传递数字 6 作为参数。
  5. 新函数调用(创建新执行上下文),创建一个新的 multiplyThis 函数执行上下文。
  6. multiplyThis 执行上下文中,声明一个变量n并将其赋值为6
  7. 3 行。在multiplyThis执行上下文中,声明一个变量ret
  8. 来到的重要的一步,继续第 3 行。对两个操作数 nval1 进行乘法运算.在multiplyThis执行上下文中查找变量 n。我们在步骤6中声明了它,它的内容是数字6。在multiplyThis执行上下文中查找变量val1multiplyThis执行上下文没有一个标记为 val1 的变量。我们向调用上下文查找,调用上下文是全局执行上下文,在全局执行上下文中寻找 val1。哦,是的、在那儿,它在步骤1中定义,数值是2。(在JavaScript中如果一个变量在本作用域内找不到,就会一直向外层的作用域找,直到找到为止,如果最外层也没有那就是undefined
  9. 继续第 3 行。将两个操作数相乘并将其赋值给ret变量,6 * 2 = 12,ret 现在值为 12
  10. 返回ret变量,销毁multiplyThis执行上下文及其变量 retn 。变量 val1 没有被销毁,因为它是全局执行上下文的一部分。
  11. 回到第6行。在调用上下文中,数字 12 赋值给 multiplied 的变量。
  12. 最后在第7行,我们在控制台中打印 multiplied 变量的值

在这个例子中,我们需要记住一个函数可以访问在它的调用上下文中定义的变量,这个就是词法作用域(Lexical scope)

闭包

直接上代码


function outer() {
  var a = 'hello world';
  function inner() {
    return a ;
  }
  return inner();
}

var b = outer();
// hello world
console.log(b());

在outer函数中声明了一个字符串变量a和一个函数变量inner,然后我返回了函数变量inner。然后在外面执行返回的函数。疑问:为什么输出hello world,因为按正常理解,最后执行的b函数实际是inner函数,而inner函数是通过执行outer函数返回的,在执行完outer函数后,outer函数的上下文已经销毁了,outer函数上下文中的变量应该被回收了,为什么inner函数能访问到outer函数中的a变量

为什么会这样?

之所以能实现这种效果,是因为闭包的特性使得返回的匿名函数的作用域链一直保存着对a变量的引用。什么意思呢,从另一个方面来解释,假设不存在闭包这个特性,那上面的代码执行效果又会变成什么样?

如果按照前面的理解,执行完outer函数后,outer函数上下文被销毁,变量被回收。那么函数变量inner应该也被回收,所以第10行代码应该返回undefined,当然你会反驳说因为第10行代码把inner赋值给了外层的b变量,所以相当于外层的上下文引用了inner函数,所以inner不能被回收,但外层上下文没有因为a变量,所以a应该被回收。此时引用链应该是 windows -> inner,既然说到了这,别忘了inner中引用了 a变量,所以完整的引用链应该是 windows -> inner -> a。所以外层上下文通过 inner 间接的引用了 a变量。这就是闭包实现的真正原因

闭包的原理:内部函数(返回的匿名函数)的作用域链仍然保有对 外部函数的变量type的引用。垃圾回收器不会对它回收

当调用outer()之后,内部函数执行环境的作用域链就有了包含a变量的活动对象,垃圾回收的机制之一就是 判断一个对象是否存在被引用,如果是则不清除。而此时内部函数被全局的b变量引用,所以在执行完b()后,内部变量a依然存在。

闭包的滥用会导致一些副作用,比如内存溢出、调试困难等。所以要慎用,清除闭包的方法就是消除引用。在该例中,令b = null 即可清除引用。

总结

说了这么多总结一下吧,一句话:闭包的原因是因为存在引用链,阻止了垃圾回收。


Author: 顺坚
Reprint policy: All articles in this blog are used except for special statements CC BY 4.0 reprint polocy. If reproduced, please indicate source 顺坚 !
评论
 Previous
Antd中使用React-dnd Antd中使用React-dnd
这是一篇踩坑的记录,最近项目有个需求要用到Antd中的拖拽表格的拖拽效果用于表格数据的重排序,Antd的官方文档中也有拖拽表格的Demo例子。所以当SA问能否实现时,我微微的点了点头。由于种种原因我没有升级Antd和React版本,所以本文
2021-10-18
Next 
股票涨跌的逻辑 股票涨跌的逻辑
由浅入深的带大家理解资本市场一些现象的底层逻辑 文章会详细解答以下几个问题: 为什么观察成交量那么重要,量价齐升为什么是好事? 为什么股价总是处于波动,横盘越久爆发行情可能性越大? 趋势的本质是什么?macd为什么有用? 首先需明白股价
2021-10-10
  TOC