闭包

1. 需求示例:点击第 n 个按钮

window.onload = function () {
  const btn = document.getElementsByTagName('button');

  // 不能将 i 定义成 var,否则每个按钮都会变成"第 4 个"
  // for (var i = 0, length = btn.length; i < length; ++i) {
  //   btn[i].onclick = function () {
  //     console.log('第' + (i + 1) + '个');
  //   };
  // }

  // 改用闭包 + var
  for (var i = 0, length = btn.length; i < length; ++i) {
    (function (i) {
      btn[i].onclick = function () {
        console.log('第' + (i + 1) + '个');
      };
    })(i);
  }

  // 或直接用 let(推荐)
  for (let i = 0, length = btn.length; i < length; ++i) {
    btn[i].onclick = function () { console.log('第' + (i + 1) + '个'); };
  }
};

2. for 循环的优化

for 循环每次都会计算第二个分号里的表达式,所以 i < btn.length 每次都会重新读取 btn.length。可以缓存到外部以节省性能。

3. 如何产生闭包

当一个**嵌套的内部(子)函数引用了嵌套的外部(父)函数的变量(函数)**时,就产生了闭包。

  • 只要定义传入外部参数的内部函数就会产生闭包(不用调用内部函数)。
  • 执行含有内部函数、且内部函数传入了外部函数参数的外部函数时,会额外产生闭包;而执行内部函数不会额外产生闭包。
  • 因为函数提升,在外部函数中定义的内部函数会先在执行上下文对象中被定义,所以执行外部函数时立刻产生闭包。

4. 什么是闭包?

闭包 = 函数 + 其引用的外部词法环境。闭包让你能从内部函数访问外部函数作用域。

5. 常见闭包

  1. 将函数作为另一个函数的返回值。
  2. 将函数作为实参传递给另一个函数调用。

6. 闭包的作用

  1. 使函数内部的变量在执行完后仍然存活在内存中(延长了局部变量的生命周期)。
  2. 让函数外部可以操作(读写)到函数内部的变量。
function fn1() {
  let a = 2;
  function fn2() { a++; console.log(a); }
  function fn3() { a++; console.log(a); }
  return fn3;
}
const f = fn1();    // 31 行产生闭包 fn2、fn3
f();                // 3
f();                // 4

局部变量 a 通过闭包 fn3 被外部持续引用,因此 fn1 执行完后 a 并未消失。

7. 闭包的生命周期

  • 闭包在嵌套的内部函数定义时产生(不是调用时)。
  • 如果函数是通过表达式定义的,闭包就在表达式那一行产生:
function fn1() {
  var a = 2;
  var fn2 = function () { // 此时才产生闭包
    a++;
    console.log(a);
  };
}

8. 闭包的应用:定义 JS 模块

模块封装要点:

  • 要保证私有,必须封装在函数中,不能在对象里(对象的属性可见,不私有)。
  • 向外暴露多个对象或函数要封装成对象。
// 方式一
function myModule() {
  let data = 'private';
  function getData() { return data; }
  return { getData };
}

// 方式二(更常用)
const myModule = (function () {
  let data = 'private';
  function getData() { return data; }
  return { getData };
})();

9. 闭包的缺点及解决

缺点:

  • 闭包引用的外部变量会长期驻留在内存中,可能造成内存泄漏。
  • 过度使用闭包会导致内存占用过高。

解决:

  • 在不需要时手动解除引用:fn = null,让闭包和外部变量都可被 GC。
  • 避免在循环里大量创建闭包。
  • 用 let / 块级作用域替代闭包(如 for 循环示例)。

来源整理自:我的有道云笔记