老生常谈 -- 闭包
开门见山
闭包是由函数和作用域共同产生的一种词法绑定现象,看上去就是内部函数能访问到外部函数内的变量,即使外部函数已经执行完毕。
深度探索
执行上下文&变量对象(VO)
先说一下上下文,主要分为全局上下文
和函数上下文
,每一个上下文都有一个关联的变量对象
,这个上下文中定义的变量和函数都保存在这个VO
上。
变量对象(variable object) 无法通过代码访问,但后台处理数据会用到,代码执行期间始终存在
- 全局上下文: 是最外层的上下文,就是
window/global
(根据宿主环境),销毁时机:应用程序退出前,eg:关闭程序 - 函数上下文: 是函数在调用时函数的上下文被推入到
上下文调用栈
中,销毁时机:函数执行完毕,被弹出上下文栈,把控制权还给之前的上下文。
作用域链&活动对象(AO)
上下文中的代码执行的时候会创建作用域链
,上面连着一个个上下文的变量对象,如果上下文是函数,则把函数的活动对象(AO)用作变量对象。
正在执行的上下文的变量对象始终位于作用域链的最顶端,作用域链的下一个变量对象来自包含函数的上下文,依此类推直到全局上下文的变量对象。
活动对象(ativation object) 由 arguments 和形参来初始化,只在函数执行期间存在
注意
除了全局上下文和函数上下文,还有一个不常用的 eval()上下文。
try/catch
的 catch(e)
和 with(ctx)
会临时在作用域链的前端添加一个上下文
有了上面的铺垫,对于闭包就比较好理解了。let‘s go!
定义在全局的函数
全局上下中的函数,在定义的时候就会创建的作用域链,把全局上下文的变量对象 VO 保存到函数的[[scope]]
中。当函数执行的时候,创建函数的执行上下文,然后复制函数的[[scope]]
来创建作用域链,接着创建并且把活动对象 AO 推入作用域链的顶端。
这是一个比较普通的情况,当全局上下文中的函数执行完毕,活动对象会被销毁,内存中就只剩下全局变量对象了。
闭包的不同
闭包不一样在哪里呢?其实就是它会把包含函数的变量对象保存到自己的作用域中。这样即使是外部包含函数执行完毕,包含函数的执行上下文和作用域链被销毁,但是包含函数的变量对象仍然保留在内存中,直到引用它的函数被销毁后才会销毁。
demo
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。 回调函数,柯里化,模拟私有成员变量,防抖节流等等。
更新
参考
- JavaScript 高级程序设计(第四版)
- JavaScript 闭包的底层运行机制
- 深入理解 JavaScript 的执行上下文