什么是闭包?
”闭包指的是那些引用了另一个函数作用域中变量的函数,通常是在嵌套函数中实现的“。
其实我对这句话并不了解,但是不妨碍我记住了如下格式的代码就是闭包,会阻止outerFn的上下文无法被释放:
function outerFn() {
const title = 'hello world';
// innerFn中引用了外部的变量'title',并且被return的这种形式就是闭包
return innerFn() {
console.log(title);
}
}
let fn = outerFn();
但是这两种方式则毫无记忆,只是记住了计时器需要清除,至于为什么则不甚了了
function outerFn() {
const title = 'hello world';
setInterval(() => {
console.log(title);
}, 1000);
}
function outerFn() {
const title = 'hello world';
function innerFn() {
console.log(title);
}
// 闭包,导致无法尾优化
return innerFn();
}
当然,还有为什么返回对象就不会形成闭包,在函数生创建的对象obj被外部的result引用了,应该无法释放,那创建它的outerFn的上下文为什么被释放了,不是闭包呢?
function outerFn() {
const obj = { title:'hello world'};
return obj;
}
const result = outerFn();
将“变量”与“变量引用的对象”混为一谈
一直以来,我存在的最大的理解错误就是将“变量被标记清除”理解成了“变量引用的对象被标记清除”。实际上“变量” 跟 “变量引用的对象”都应该有自己的引用计数。引用计数规则如下(只是借用引用计数的概念):
- “变量”每被一个嵌套函数声明引用,计数加一。嵌套函数被标记清除,则“变量”计数减一(函数执行结束后优先清除嵌套函数)。
- “变量引用的对象”每被一个“变量”引用,计数加一。
- 返回值、传参数是拷贝。相当于创建新变量,原变量的引用计数不变。
- 函数执行上下文能否清除只关注自身的“变量”引用计数情况,而不关注“变量引用的对象”引用计数情况。
利用新规则解释闭包的形成
function outerFn() {
const obj = { title:'hello world'};
return obj;
}
/**
* 本上下文中,变量obj没有嵌套函数声明引用,初始引用计数是0
* 返回赋值给result不会导致变量obj引用计数增加
* 函数执行结束后,因为没有一个变量的引用计数大于0,所以其上下文可以被标记回收,*没有形成闭包*。
* obj被回收后,对象’{ title:'hello world'}‘还被result持有,所以不会被标记回收
*/
const result = outerFn();
function outerFn() {
const title = 'hello world';
return innerFn() {
console.log(title);
}
}
/**
* 本上下文中,变量title被innerFn声明引用, 初始引用计数是1
* 函数执行结束,准备清除innerFn,结果发现innerFn被fn持有,引用计数是1,无法清除
* innerFn无法清除,导致title的引用计数还是1,所以其上下文不可以被标记回收,*因此形成闭包*。
* 当fn解除对innerFn的引用时,innerFn被标记清除,而后title被标记清除,最后outerFn上下文才能被标记清除。
*/
let fn = outerFn();
function outerFn() {
const title = 'hello world';
/**
* 同理:setInterval的持有嵌套函数的引用计数一直存在,导致title的引用计数还是1,所以其上下文不可以被标记回收,*因此形成闭包*。
*/
setInterval(() => {
console.log(title);
}, 1000);
}
function outerFn() {
const title = 'hello world';
function innerFn() {
console.log(title);
}
/**
* innerFn没有执行完时,title变量的引用计数一直存在,形成闭包,导致上下文无法被优化释放。
* 这就是为什么尾优化不允许出现闭包的原因。
*/
return innerFn();
}
使用传参数解除闭包
function outerFn() {
const title = 'hello world';
/**
* 通过参数的形式传入title变量,不会导致title变量引用计数增加,因此不会形成闭包
*/
setInterval((val) => {
console.log(val);
}, 1000, title);
}
总结
函数执行形成闭包的原因是它的“变量”引用计数无法归0,从而导致函数执行上下文无法被释放。使用向嵌套函数传递参数的方式能避免形成闭包。