谈一谈闭包
闭包
一、 闭包原理
1. 前景知识
- 变量对象:每一个函数执行的时候都会用 参数、局部变量来生成自己的变量,这种叫 AO。全局的变量对象是是 window,叫 GO。
- 作用域链生成:
- 定义函数的时候:定义函数时所在的执行环境也有一个作用域链,这个作用域链会被保存在函数的内部[[Scope]]属性。比如,我们在全局定义一个函数 A,在 A 里定义一个函数 B, A 的作用域链是:
A.AO --> window
,所以B.[[Scope]] = A.AO --> window
- 执行函数的时候:会创建自己的 AO 对象,然后复制 [[Scope]] 并把自己的 AO 放到 [[Scope]] 最前端作为自己的作用域链:
B.作用域链 = B.AO --> A.AO --> window
- 作用域链本质上是一个指向变量对象的指针列表,它只包含引用但不实际包含变量对象。
- 函数执行完毕后,它的作用域链一定会被销毁,但是它的作用域链上引用的变量对象不一定会被销毁(闭包)
- 定义函数的时候:定义函数时所在的执行环境也有一个作用域链,这个作用域链会被保存在函数的内部[[Scope]]属性。比如,我们在全局定义一个函数 A,在 A 里定义一个函数 B, A 的作用域链是:
2. 什么是闭包
闭包的概念很简单,就是指有权访问另一个函数作用域中的变量的函数
我们来分析一个例子:
let test = ()=>{
let a = 99;
let fun = (()=>{ // 函数 a
let a = 1;
return ()=>{ // 函数 fun
console.log(a);
};
})();
fun(); // 1
}
test();
test = null;
我们把上面例子的匿名函数都起一个名字,然后我们可以得到函数 fun 的作用域链是长这个样子的:fun.AO --> a.AO --> test.AO --> window
,所以,根据作用域链的先后关系,最后的输出结果就是 1
二、闭包里的内存回收
闭包理解起来其实不难,但是要想更深入理解闭包的原理,那么就得从变量对象入手。
我们都知道,正常执行完毕一个函数后,这个函数里面的变量外部是访问不了的了,从代码层面去看是我们没有指针去访问里面的变量,但从内存层面看,是这个函数的作用域链、变量对象都被浏览器进行销毁回收了。当然,闭包是例外,闭包作用域链上的各级函数AO对象肯定没被销毁,那么为什么它们没有被销毁呢?
浏览器的内存回收是另一个大问题,这里我们不深入细究,我们知道一点就行:一个对象如果没有任何指针变量去引用它,那么浏览器就会去尝试回收它。
OK,那我们逆推就可以知道,闭包作用域链上的各级函数AO对象之所以能被保存下来没被回收,就是因为它们还在被引用。被谁引用?就是被闭包作用域链引用啊。
所以,我们再分析上面的闭包代码,我们可以得知这样的流程:
- 执行函数 test,生成
test.AO
和test.作用域链
- 执行函数 a,生成
a.AO
和a.作用域链
- 定义函数 fun,
fun.[[Scope]] = a.AO --> test.AO --> window
- 函数 a 执行完毕,a 被销毁,
a.作用域链
也被销毁,a.AO
被fun.[[Scope]]
引用没被销毁 - 执行 fun,生成 ·
fun.AO
和fun.作用域链 = fun.AO --> a.AO --> test.AO --> window
- 函数 fun 执行完毕,函数 fun 还被 fun 指针引用没被销毁,
fun.作用域链
被销毁,fun.作用域链
上引用的fun.AO, a.AO
被销毁,test.AO
和window
因为还有被其他指针引用所以没被销毁。 - 函数 test 执行完毕,函数 test 还被 test 指针引用没被销毁,
test.作用域链
被销毁,test.作用域链
上引用test.AO
被销毁,test.AO
里的函数 fun 也被销毁。 - test = null,函数 test 被销毁 。
这也就是前面说的,函数执行完毕后,它的作用域链一定会被销毁,但是它的作用域链上引用的变量对象不一定会被销毁,因为可能被return的闭包的作用域链引用。
所以,闭包的本质是,闭包父函数的变量对象一直被闭包的作用域链引用着无法被浏览器回收。所以哪怕父函数执行完毕之后依旧可以通过闭包去访问父函数变量对象里的变量。
三、闭包测试题目
看一道题目:
let b = 1;
let fun1 = ()=>{
let a = 1;
return ()=>{
console.log(a, b); // 1 1
console.log(c); // 报错
}
}
let back = fun1();
let fun2 = (back)=>{
let b = 99;
let c = 99;
back();
}
fun2(back);
通过分析可知,back 函数的作用域链是back.AO --> fun1.AO --> window
,所以 back() 可以正常打印 a=1,b=1,但是打印 c 就会报错的,因为在它的作用域链的变量对象上不存在 c 这个变量。
所以发现没有,虽然 back 函数是在 fun2 里面调用的,但是 back 的作用域链却没有包含 fun2 的变量对象。因为,一个函数的作用域链的范围其实在函数定义的时候就已经决定了,函数执行时能决定的只是作用域链最前端的一部分而已。