闭包

18 阅读3分钟

定义

指能够访问自由变量的函数。自由变量是指在函数中使用的,既不是函数参数也不是函数局部变量的变量。换句话说,闭包就是能够读取其他函数内部变量的函数。
在JS中,闭包通常是指一个函数及其捆绑的周边环境(词法环境)的组合。也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域。

  • 词法作用域Lexical Scoping JS的作用域在函数定义时确定,而非执行时。闭包基于此特性实现。
  • 作用域链Scope Chain 在JS中,函数在创建时会保存一个作用域链。这个作用域链包含了函数被创建时所能访问的所有变量对象Variable Object。当函数执行时,会创建一个活动对象(Activation Object,包含局部变量、参数等),并将其添加到作用域链的前端。
  • 垃圾回收机制 正常情况下,函数执行完毕后其局部变量会被销毁。但闭包会阻止垃圾回收器回收被引用的外部变量。

形成

当一个函数返回另一个函数,且返回的函数中使用了外层函数的变量,这时就形成了一个闭包。因为返回的函数持有外层函数作用域的引用,所以外层函数的作用域不会被销毁,即使外层函数已经执行完毕。

const outer = () => {
  const outerVar = "outer";
  
  const inner = () => {
    console.log(outerVar)
  };
  
  return inner;
}

const closure = outer();
closure();
  • inner引用了outerVar
  • inner被返回并在外部调用

作用

  • 封装私有变量:通过闭包可以创建私有变量,只能通过特定的方法访问。
  • 保持状态: 闭包可以让变量的值始终保持在内存中,不会垃圾回收机制回收。
  • 模块化:闭包是实现模块模式的基础,可以创建独立的模块,避免全局污染。

内存管理

由于闭包会使得函数中的变量被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能会导致内存泄漏。解决方法是,在退出函数之前,将不使用的局部变量全部删除(设置为null)。

性能影响

闭包访问外部变量比访问局部变量慢,在性能关键代码中需谨慎使用。

应用实例

计数器


function createCounter() {
  let count = 0;

  return function() {
   return count++;
  };
}

const counter = createCounter();

console.log(counter()); // 0
console.log(counter()); // 1
console.log(counter()); // 2

私有方法

使用闭包模拟私有方法:

copnst counter = (function() {
  let privateCounter = 0;

  function changeBy(val) {
    privateCounter += val;
  }

  return {
    increment: function() {
      changeBy(1);
    },
    decrement: function() {
      changeBy(-1);
    },
    value: function() {
      return privateCounter;
    }
  };
})();

console.log(counter.value()); // 0
counter.increment();
counter.increment();
console.log(counter.value()); // 2

counter.decrement();
console.log(counter.value()); // 1

常见问题

在循环中使用闭包可能会遇到一个经典问题:


for (var i = 0; i < 5; i++) {
  setTimeout(function() {
    console.log(i); // 输出5个5
  }, 1000);
}

这是因为setTimeout的回调函数在循环结束后才执行,此时i的值已经是5。解决方法是使用闭包为每次循环创建一个独立的作用域:

// 使用IIFE(立即执行函数表达式)创建闭包
for (var i = 0; i < 5; i++) {
  (function(j) {
    setTimeout(function() {
      console.log(j);
    }, 1000);
   })(i);
}

或者使用ES6的let关键字(块级作用域):

for (let i = 0; i < 5; i++) {
  setTimeout(function() {
    console.log(i); // 输出0,1,2,3,4
  }, 1000);
}

最佳实践

  • 优先使用块级作用域let/const
  • 明确解除不再需要的闭包引用
  • 复杂状态管理改用class/模块

延伸

image.png