闭包是什么?—— 用「会嘲讽的ATM机」的比喻彻底说清!

219 阅读3分钟

❓ 闭包到底是什么?

闭包 = 内部函数 + 它绑定的外部变量环境
既不是外部函数,也不是返回的对象,而是「内部函数和它锁定的外部变量」的组合体!


❓ 为什么叫「闭包」(Closure)?

  • 词源:来自拉丁语「clausura」(关闭、封闭),意为「封闭了外部变量的函数」。
  • 核心逻辑:内部函数像一扇门,把外部变量「关」在自己的作用域里,形成封闭的包裹体
  • 哲学意义:函数不再只是孤立的代码块,而是携带了「记忆」的独立个体!

🌰 例子:会嘲讽的ATM机

function 创建ATM机(初始余额) {
  let 余额 = 初始余额; // 闭包藏起来的钱!

  return {
    取钱: (金额) => {
      if (金额 > 余额) console.log("😶余额不足,赶紧去赚钱!");
      else {
        余额 -= 金额;
        console.log(`取出${金额},余额:${余额}`);
      }
    },
    存钱: (金额) => {
      余额 += 金额;
      console.log(`存入${金额},余额:${余额}`);
    }
  };
}

const 我的ATM = 创建ATM机(100);
我的ATM.取钱(50); // 余额50
我的ATM.存钱(200); // 余额250
我的ATM.取钱(300); // 余额不足,开始嘲讽

🔍 解剖闭包身份:

角色对应代码是否是闭包?
外部函数 创建ATMfunction 创建ATM机(){...}❌ 只是闭包的「诞生工厂」
内部函数 取钱/存钱() => { ...余额 }✅ 闭包本体(函数+环境)
返回的对象{ 取钱, 存钱 }❌ 对象的属性是闭包

💡 关键理解:

  1. 闭包是「内部函数」的超级形态:普通函数执行完就失忆,闭包函数却能记住外部变量。
  2. 环境是闭包的一部分:闭包函数不能脱离它引用的变量。
  3. 命名精髓:闭包 = 封闭(closure)了外部变量,形成一个自给自足的包裹体。

🌰 再举个例子:

// 外部函数只是「造物主」
function 创建计数器() {
  let count = 0; // 被闭包关起来的变量

  // 闭包:内部函数 + count
  return () => { 
    count++; 
    console.log(`计数:${count}`);
  };
}

const 计数器 = 创建计数器();
计数器(); // 计数:1
计数器(); // 计数:2 (普通函数早该失忆了,闭包却记得!)

🎯 闭包常见使用场景与示例

1. 模块化与私有变量

场景:隐藏内部变量,只暴露特定方法(类似「保险箱」)。

// 创建一个计数器模块,count 是私有变量
function createCounter() {
  let count = 0; // 闭包保护的变量

  return {
    increment: () => { count++; },
    getValue: () => count,
    reset: () => { count = 0; }
  };
}

const counter = createCounter();
counter.increment();
console.log(counter.getValue()); // 1
counter.reset(); 
// 无法直接访问 counter.count,闭包保护了隐私!

2. 函数工厂(定制化函数)

场景:根据参数生成不同功能的函数(类似「流水线生产」)。

// 创建不同倍数的乘法器
function createMultiplier(factor) {
  return (num) => num * factor; // 闭包记住了 factor
}

const double = createMultiplier(2);
const triple = createMultiplier(3);

console.log(double(5)); // 10(5*2)
console.log(triple(5)); // 15(5*3)

3. 事件处理(保留上下文)

场景:在异步操作中保持变量状态

// 按钮点击计数器(闭包保存点击次数)
function setupButton() {
  let clicks = 0;
  document.getElementById('myBtn').addEventListener('click', () => {
    clicks++;
    console.log(`点击次数:${clicks}`);
  });
}
setupButton();
// 每次点击都记住 clicks,无需全局变量!

4. 缓存(记忆化优化)

场景:缓存复杂计算结果,避免重复计算

// 缓存阶乘计算结果
function createFactorial() {
  const cache = {}; // 闭包中的缓存对象

  return (n) => {
    if (n in cache) return cache[n];
    if (n === 1) return 1;
    const result = n * factorial(n - 1);
    cache[n] = result; // 存入缓存
    return result;
  };
}

const factorial = createFactorial();
console.log(factorial(5)); // 120(首次计算)
console.log(factorial(5)); // 120(直接从缓存读取)

5. 延迟调用(setTimeout)

场景:在异步回调中保留循环变量(解决经典面试题)。

// 错误写法:所有回调都输出 i=5
for (var i = 0; i < 5; i++) {
  setTimeout(() => console.log(i), 100); 
}

// 正确写法:闭包保存每个 i 的值
for (var i = 0; i < 5; i++) {
  (function (currentI) { // 闭包立即捕获 currentI
    setTimeout(() => console.log(currentI), 100); 
  })(i); // 传递当前 i
}
// 输出 0,1,2,3,4

🌟 闭包诞生的基石与内存逻辑

  • 词法作用域(静态作用域): 函数在定义时确定作用域链,而非运行时!
  • 垃圾回收(GC)机制:当内存空间不再被引用时,会被自动回收。
    闭包通过「作用域链」强行维持外部变量的引用 → 阻止GC回收

🚨 注意事项:

  • 内存泄漏:长期持有的闭包可能占用内存,用完后及时解除引用(如设为 null)。
  • 不要滥用:简单场景用闭包反而让代码难读,优先考虑函数参数传递。
  • 引擎优化:即使闭包存在,如果变量后续未被使用,可能被优化回收(如V8引擎的「逃逸分析」)。