闭包(Closure)是 JavaScript 中一个核心且强大的概念,指的是有权访问另一个函数作用域中变量的函数。简单来说,当一个内部函数被其外部函数之外的地方引用时,就形成了一个闭包。

闭包(Closure)是函数与其词法环境的组合。当函数能够记住并访问其定义时所在的词法作用域(即使该函数在当前词法作用域之外执行),就形成了闭包。简单理解:闭包 = 函数 + 创建时的作用域链引用。

一、闭包的本质

闭包本质上是函数和其词法环境的引用关系,这种引用使得函数即使在其定义环境外执行,也能保持对原定义环境中变量的访问能力。

简单来说,可以理解为:闭包让函数"随身携带"了它被定义时的环境,无论这个函数走到哪里,它都能访问那个环境中的变量。

二、闭包的核心特征

  1. 访问外部作用域 :内部函数可以访问外部函数中定义的变量和参数。

  2. 持久引用 :当外部函数执行完毕后,其作用域中的变量不会被垃圾回收,因为内部函数仍在引用它们。

  3. 词法作用域 :闭包基于词法作用域(静态作用域),即函数的作用域在定义时就确定了。

三、闭包的形成条件

  1. 存在函数嵌套。

  2. 内部函数引用了外部函数的变量。

  3. 内部函数在外部函数作用域之外被执行。

四、闭包的工作原理

要理解闭包,首先需要了解 JavaScript 的作用域链和变量生命周期:

  1. 作用域链:函数在定义时会创建一个作用域链,包含自身作用域、外部函数作用域直到全局作用域。

  2. 变量生命周期:函数执行完毕后,其作用域通常会被销毁,内部变量也会被回收。

  3. 闭包的保持:但如果内部函数被外部引用,外部函数的作用域会被保留,内部变量也不会被销毁。

闭包基本示例
function outer() {
  // 虽然用let声明(块级作用域),但它在outer函数的顶级作用域
  let count = 0; // 外部函数outer的变量
  
  // inner定义在与count相同的作用域内
  function inner() { // 内部函数inner形成闭包
    // 可以访问count,即内部函数可以访问外部函数的变量
    count++; // 修改外部变量 
    return count;
  }
  
  return inner; // 返回内部函数,使其能在外部执行
}

const counter = outer(); // counter现在是一个闭包
console.log(counter()); // 输出: 1
console.log(counter()); // 输出: 2
console.log(counter()); // 输出: 3

在这个示例中:

  • outer 函数执行完毕后,其作用域并没有被销毁。

  • inner 函数通过闭包保持了对 count 变量的访问。

  • 每次调用 counter() 都会修改同一个 count 变量。

  • 不同的闭包实例(如 counter 和 counter2)拥有各自独立的变量副本。

五、闭包的常见用途

1. 数据封装与私有变量
function createPerson(name) {
  let age = 0; // 私有变量
  
  return {
    getName: function() {
      return name;
    },
    getAge: function() {
      return age;
    },
    growOlder: function() {
      age++;
    }
  };
}

const person = createPerson("水月");
person.getName(); // "水月"
person.getAge(); // 0
person.growOlder();
person.getAge(); // 1
// person.age; // undefined (无法直接访问)
2. 函数柯里化

函数柯里化(Currying):

函数柯里化是函数式编程中的一种重要技术,它将接收多个参数的函数转换为一系列接收单一参数的函数的过程。简单来说,就是把 f(a, b, c) 这样的函数调用方式,转换为 f(a)(b)(c) 的形式。

柯里化利用了 JavaScript 的闭包特性:

  • 每次调用函数时只接收一个参数。

  • 通过闭包记住已接收的参数。

  • 当收集到足够的参数时,执行最终的计算并返回结果。

// 普通函数
function add(a, b, c) {
  return a + b + c;
}

// 柯里化函数
function curriedAdd(a) {
  return function(b) {
    return function(c) {
      return a + b + c;
    };
  };
}

// 箭头函数简化版
const curriedAdd = a => b => c => a + b + c;

// 使用方式
add(1, 2, 3);           // 输出: 6
curriedAdd(1)(2)(3);    // 输出: 6
3. 模块化开发
const module = (function() {
  let privateData = "私有数据";
  
  function privateFunction() {
    console.log(privateData);
  }
  
  return {
    publicMethod: function() {
      privateFunction();
    },
    publicData: "公共数据"
  };
})();

module.publicMethod(); // 输出: 私有数据
console.log(module.publicData); // 输出: 公共数据
// console.log(module.privateData); // undefined
4. 事件处理与回调
function setupEventListeners() {
  let count = 0;
  
  document.getElementById("btn").addEventListener("click", function() {
    count++;
    console.log(`点击了 ${count} 次`);
  });
}

setupEventListeners();

六、闭包的优缺点

优点

特性

描述

数据隐私

可以创建私有变量和方法,外部无法直接访问

状态保持

能够在函数执行完毕后保持变量的状态,实现状态持久化

模块化

有助于实现模块化代码,避免全局变量污染,提高代码可维护性

函数工厂

可以创建具有特定配置的函数,提高代码复用性

缺点

特性

描述

内存消耗

闭包会保持外部函数的作用域,可能导致内存泄漏,增加内存占用

性能问题

作用域链查找会比直接访问变量慢一些,可能影响执行效率

调试困难

闭包的作用域关系较为复杂,可能使调试和错误定位变得困难

总结

闭包是JavaScript中一个强大的特性,它:

  • 允许函数访问并操作其定义时所在作用域中的变量。

  • 实现了数据封装和私有变量。

  • 常用于函数柯里化、模块化开发和事件处理。

  • 需要注意内存管理,避免不必要的性能消耗。

理解闭包是掌握 JavaScript 高级特性的关键一步,它为编写更优雅、更模块化的代码提供了可能。

附经典闭包面试题:

// 问题:以下代码点击每个按钮会输出什么?
for (var i = 0; i < 5; i++) {
  const btn = document.createElement("button");
  btn.innerHTML = `按钮 ${i}`;
  btn.addEventListener("click", function() {
    console.log(i);
  });
  document.body.appendChild(btn);
}

// 答案:点击任何按钮都会输出 5

// 解决方案1:使用闭包
for (var i = 0; i < 5; i++) {
  (function(j) {
    const btn = document.createElement("button");
    btn.innerHTML = `按钮 ${j}`;
    btn.addEventListener("click", function() {
      console.log(j);
    });
    document.body.appendChild(btn);
  })(i);
}

// 解决方案2:使用 let
for (let i = 0; i < 5; i++) {
  const btn = document.createElement("button");
  btn.innerHTML = `按钮 ${i}`;
  btn.addEventListener("click", function() {
    console.log(i);
  });
  document.body.appendChild(btn);
}