老生常谈 -- 闭包

警告
本文最后更新于 2022-09-17,文中内容可能已过时。

闭包是由函数和作用域共同产生的一种词法绑定现象,看上去就是内部函数能访问到外部函数内的变量,即使外部函数已经执行完毕

先说一下上下文,主要分为全局上下文函数上下文,每一个上下文都有一个关联的变量对象,这个上下文中定义的变量和函数都保存在这个VO上。

变量对象(variable object) 无法通过代码访问,但后台处理数据会用到,代码执行期间始终存在

  • 全局上下文: 是最外层的上下文,就是 window/global(根据宿主环境),销毁时机:应用程序退出前,eg:关闭程序
  • 函数上下文: 是函数在调用时函数的上下文被推入到上下文调用栈中,销毁时机:函数执行完毕,被弹出上下文栈,把控制权还给之前的上下文。

上下文中的代码执行的时候会创建作用域链,上面连着一个个上下文的变量对象,如果上下文是函数,则把函数的活动对象(AO)用作变量对象。 正在执行的上下文的变量对象始终位于作用域链的最顶端,作用域链的下一个变量对象来自包含函数的上下文,依此类推直到全局上下文的变量对象。

活动对象(ativation object) 由 arguments 和形参来初始化,只在函数执行期间存在

除了全局上下文和函数上下文,还有一个不常用的 eval()上下文。 try/catchcatch(e)with(ctx) 会临时在作用域链的前端添加一个上下文


有了上面的铺垫,对于闭包就比较好理解了。let‘s go!

全局上下中的函数,在定义的时候就会创建的作用域链,把全局上下文的变量对象 VO 保存到函数的[[scope]]中。当函数执行的时候,创建函数的执行上下文,然后复制函数的[[scope]]来创建作用域链,接着创建并且把活动对象 AO 推入作用域链的顶端。

这是一个比较普通的情况,当全局上下文中的函数执行完毕,活动对象会被销毁,内存中就只剩下全局变量对象了。

闭包不一样在哪里呢?其实就是它会把包含函数的变量对象保存到自己的作用域中。这样即使是外部包含函数执行完毕,包含函数的执行上下文和作用域链被销毁,但是包含函数的变量对象仍然保留在内存中,直到引用它的函数被销毁后才会销毁。

js

function demo(val1, val2) {
  return function () {
    var val3 = 14
    return val1 + val2
  }
}
var a = 1000
var b = 300
var sum = demo(a, b)
var res = sum() // 1314

在上面的例子中: 全局上下文的变量对象上有变量 a,b,sum,res 和函数 demo; demo 函数定义的时候已经拷贝到了全局变量对象到它的作用域链上,执行的时候会创建活动对象(arguments,val1,val2)并把它推导作用域链的顶端; 而内部的匿名函数把包含函数的变量对象再放入自己的作用域链上,当执行的时候也会创建活动对象并把它推到作用域链的顶端。

tips: 顶级声明中,var 声明的变量、函数是在全局上下文的VO中的,let、const并不是,虽然作用域链的解析效果一样。

内存泄露

访问包含函数的 this, arguments。 回调函数,柯里化,模拟私有成员变量,防抖节流等等。

一文颠覆大众对闭包的认知