闭包

50 阅读4分钟

在 JavaScript 中,闭包(Closure)  是一个核心且强大的概念,几乎渗透到所有高级开发场景中。闭包的实现依赖于 JavaScript 的词法作用域(Lexical Scoping)和函数作为一等公民的特性。以下是闭包的详细解释和相关知识:

闭包的定义

闭包是指 函数能够记住并访问其词法作用域(Lexical Scope) ,即使该函数在其词法作用域之外执行。简单来说:

  • 当一个函数内部定义了另一个函数,并且内部函数引用了外部函数的变量时,闭包就形成了。
  • 闭包使得外部函数的变量在函数执行结束后仍然存活,不会被垃圾回收机制回收。

闭包形成的条件

闭包的形成需要以下条件:

  1. 函数嵌套:内部函数定义在外部函数内部。
  2. 内部函数引用外部变量:内部函数引用了外部函数的变量或参数。
  3. 内部函数被外部保留:内部函数被返回或在其他作用域中被引用(如被赋值给全局变量、作为回调函数等)。

闭包的示例

基本闭包

function outer() {
  let count = 0; // 外部函数的变量

  function inner() {
    count++;
    console.log(count);
  }

  return inner; // 返回内部函数
}

const counter = outer();
counter(); // 1
counter(); // 2
  • inner 函数引用了外部函数 outer 的变量 count
  • counter 是 outer 返回的 inner 函数,每次调用 counter 都会修改并保留 count 的值。

循环中的闭包

function createFunctions() {
  const result = [];
  for (var i = 0; i < 3; i++) {
    result.push(function() {
      console.log(i); // 所有函数共享同一个变量 i
    });
  }
  return result;
}

const funcs = createFunctions();
funcs[0](); // 3(不是预期的 0)
funcs[1](); // 3
funcs[2](); // 3
  • 问题原因:所有内部函数共享同一个变量 i(因为 var 是函数作用域)。
  • 解决方案:使用闭包或 let(块级作用域)隔离变量。
// 使用闭包隔离变量
function createFunctions() {
  const result = [];
  for (var i = 0; i < 3; i++) {
    result.push(
      (function(j) {
        return function() {
          console.log(j);
        };
      })(i) // 立即执行函数传递当前的 i 值
    );
  }
  return result;
}

// 使用 let 替代 var
function createFunctions() {
  const result = [];
  for (let i = 0; i < 3; i++) {
    result.push(function() {
      console.log(i);
    });
  }
  return result;
}

闭包的应用场景

1. 数据封装与私有变量

闭包可以创建私有变量,避免外部直接访问和修改数据,提升代码安全性。

示例:计数器

function createCounter() {
  let count = 0; // 私有变量,外部无法直接访问
  return {
    increment: function() { count++ },
    decrement: function() { count-- },
    getCount: function() { return count }
  };
}

const counter = createCounter();
counter.increment();
console.log(counter.getCount()); // 1
  • 闭包的作用incrementdecrementgetCount 方法通过闭包访问并修改私有变量 count
  • 优势:隐藏内部实现细节,仅暴露必要接口。

2. 模块模式(Module Pattern)

利用闭包实现模块化,防止全局命名空间污染。

示例:工具模块

const mathModule = (function() {
  let privateVar = "内部数据"; // 私有变量

  function privateMethod() {
    console.log("私有方法");
  }

  return { // 暴露公共接口
    add: (a, b) => a + b,
    multiply: (a, b) => a * b
  };
})();

console.log(mathModule.add(2, 3)); // 5
// mathModule.privateMethod(); // 报错,私有方法不可访问
  • 闭包的作用:立即执行函数(IIFE)返回公共方法,私有变量和方法被闭包保护。
  • 优势:代码模块化,减少全局变量冲突。

函数柯里化(Currying)

通过闭包将多参数函数转换为链式调用的单参数函数,增强函数复用性。

示例:乘法函数柯里化

function multiply(a) {
  return function(b) { // 闭包保留参数 a
    return a * b;
  };
}

const double = multiply(2);
console.log(double(5)); // 10 (2*5)
console.log(double(10)); // 20 (2*10)
  • 闭包的作用:内层函数保留外层函数的参数 a,形成特定功能的函数。
  • 优势:复用部分参数逻辑,生成更具体的函数。

4、事件处理和回调函数

闭包在异步操作或事件处理中保留上下文状态。

示例:循环绑定事件

function setupButtons() {
  for (let i = 0; i < 3; i++) {
    document.getElementById(`btn-${i}`).addEventListener('click', function() {
      console.log(`按钮 ${i} 被点击`); // 正确输出 0, 1, 2
    });
  }
}
  • 闭包的作用let 的块级作用域 + 闭包,每个事件回调函数捕获当前循环的 i
  • 问题解决:避免循环中变量共享导致的问题。

5、防抖(Debounce)和节流(Throttle)

闭包用于管理函数执行频率,优化性能。

示例:防抖函数

function debounce(func, delay) {
  let timerId; // 闭包保存定时器ID
  return function(...args) {
    clearTimeout(timerId);
    timerId = setTimeout(() => func.apply(this, args), delay);
  };
}

const handleResize = debounce(() => {
  console.log('窗口大小调整完毕');
}, 300);

window.addEventListener('resize', handleResize);
  • 闭包的作用:保留 timerId,确保每次调用都能清除之前的定时器。
  • 优势:减少高频事件(如滚动、输入)的触发次数。

闭包的注意事项

  • 内存泄漏:闭包可能导致外部函数的变量无法被回收,需及时解除无用引用(如将变量设为 null)。
  • 性能影响:过度使用闭包可能增加内存消耗,需权衡使用场景。

总结

闭包的应用场景包括但不限于:

  1. 封装私有变量,提升代码安全性。
  2. 模块化开发,避免全局污染。
  3. 函数柯里化,增强函数复用性。
  4. 处理异步/事件回调,保留上下文状态。
  5. 优化高频操作(防抖、节流)。
  6. 缓存计算结果,提升性能。

合理使用闭包可以使代码更模块化、高效和可维护,但需注意内存管理和性能优化。