javascript闭包的理解:变量与变量引用对象的分别计数

106 阅读3分钟

什么是闭包?

”闭包指的是那些引用了另一个函数作用域中变量的函数,通常是在嵌套函数中实现的“。

其实我对这句话并不了解,但是不妨碍我记住了如下格式的代码就是闭包,会阻止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,从而导致函数执行上下文无法被释放。使用向嵌套函数传递参数的方式能避免形成闭包。