闭包是JavaScript
中一个非常容易让人迷惑的知识点,本文会从闭包的概念和执行过程入手,详细的剖析闭包的原理。
注意:如果不了解js引擎的运行原理,可以查看上一章内容【V8引擎】JavaScript变量提升
一个函数和对其周围状态(lexical environment
,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包(closure
);
也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域;
在 JavaScript
中,每当创建一个函数,闭包就会在函数创建的同时被创建出来;
一个普通的函数function
,如果它可以访问外层作用于的自由变量,那么这个函数就是一个闭包;
从广义的角度来说:JavaScript
中的函数都是闭包;
从狭义的角度来说:JavaScript
中一个函数,如果访问了外层作用于的变量,那么它是一个闭包;
我们看下面的闭包代码,我们可能会产生疑问。
我们可以取消debugger注释,根据步骤和图示
一步步查看闭包在浏览器中的运行过程,方便了解闭包的原理。
//debugger
function foo() {
var i = 1
return function () {
console.log(i++)
}
}
var add1 = foo()
add1()//1
add1()//2
i = 10086
add1()//3
add1()//4
foo
函数中的变量 i
为什么一直在递增?函数执行完出栈后变量不是应该被销毁了吗?
为什么会有这么奇怪的现象呢?
正常情况下,我们的 foo
函数执行完毕出栈后, AO
对象会被释放,里面的变量也会被回收;
但是因为我们内层的匿名函数中有作用域引用指向了这个 AO
对象 ,所以它一直不会被释放掉;
下面我们就从JS引擎的运行原理来深度剖析闭包这个有趣的现象!
在GEC全局执行上下文中,JS
引擎通过预解析会把foo
的内存地址和window
等属性存入GO
对象
add1
变量因为 var
声明提升变成了 undefined
由于没有用关键字声明变量 i = 10086 不会被预解析
Tip : 由于变量声明自带不可删除属性,比如var add1 = foo() 跟 i= 10086,前者是变量声明,带不可删除属性,因此无法被删除;后者为全局变量的一个属性,因此可以从全局变量中删除。
如果是严格模式下 不使用关键字声明变量 例如 i =10086 会直接报错!
JS引擎执行到 foo()
时,会根据函数体创建一个FEC函数执行上下文,并压入栈中
add1还是undefined,要等foo()函数执行完return
当 foo()
函数执行完后,return
了一个匿名函数出来,这时候 var add1
变量就能指向匿名函数对象的地址了
foo函数会生产自己的ao
对象,ao
对象存储着定义的形参和变量等
foo函数执行完了会自动出栈
这时候我们发现AO对象生成以后被匿名函数0xb00的父级作用域引用了,指针一直指着AO对象无法销毁
因为add1
内部没有形参和定义的变量所以它的AO对象
一直是空的
那么匿名函数中i++
的i
因为在自身AO对象
中找不到,它就会通过父级作用域往上查找,匿名函数i
此时就变成了 parentScope
(父级作用域)的i
即:变成了 AO对象(foo)(0x200) i++
由于上一个add1
函数打印i++
所以这一次的foo
函数中AO
对象已经变成了2
console.log(i++)
结果当然是 2 了
再执行 ++
i 变成了 3,但是还没有打印
这时候i会被赋值 10086
后面的两个add1()/add1()函数执行和第四第五步是同理的就不重复演示了
AO对象中的i会因为后续执行最终变成数字5,而且一直被指针引用着无法释放。
经过一个例子我们发现闭包的缺点,就是成这些内存都是无法被释放的;
所以我们经常说的闭包会造成内存泄露,其实就是刚才的引用链中的所有对象都是无法释放的;
将变量add1
设置为null
,因为当add1
设置为null
时,就不在对函数对象 0xb00
有引用,那么对应指向AO对象地址的0x200
指针就消失了,它们会被GC
(垃圾回收机制)销毁掉
所以解决方法是在退出函数之前,将不使用的变量全部删除。