正在加载,请稍候…

JavaScript 闭包详解:工作原理与重要性

通过清晰示例理解 JavaScript 闭包。学习闭包如何捕获作用域、常见用例(如数据隐私和工厂函数)以及需要避免的陷阱。

JavaScript 闭包详解:工作原理与重要性

JavaScript 闭包详解:工作原理与重要性

闭包是 JavaScript 面试中最常被问到的主题之一,也是最容易被误解的概念之一。一旦你真正理解了它们,它们就会成为你每天使用的强大工具。

什么是闭包?

闭包是一个函数,它记住了定义时所在作用域的变量,即使该作用域已经执行完毕。

function makeCounter() {
  let count = 0; // 这个变量被“封闭”了
  
  return function() {
    count++;
    return count;
  };
}

const counter = makeCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3

// 外部无法访问 count!
console.log(count); // ReferenceError: count is not defined

内部函数“封闭”了 count 变量。即使 makeCounter() 返回后,内部函数仍然可以访问 count

JavaScript 闭包详解:工作原理与重要性 插图

闭包如何工作:词法作用域

JavaScript 使用词法作用域:函数的作用域由它在代码中的编写位置决定,而不是调用位置。

const name = 'Global';

function outer() {
  const name = 'Outer';
  
  function inner() {
    // inner() 沿着作用域链查找:inner → outer → global
    console.log(name); // 'Outer'(不是 'Global')
  }
  
  inner();
}

outer();

inner()作用域链是:

  1. inner 自身的作用域
  2. outer 的作用域 ← 在这里找到 name
  3. 全局作用域

常见用例

1. 数据隐私 / 模块模式

function createBankAccount(initialBalance) {
  let balance = initialBalance; // 私有——无法直接访问
  
  return {
    deposit(amount) {
      if (amount > 0) balance += amount;
      return balance;
    },
    withdraw(amount) {
      if (amount > balance) throw new Error('Insufficient funds');
      balance -= amount;
      return balance;
    },
    getBalance() {
      return balance;
    }
  };
}

const account = createBankAccount(1000);
console.log(account.getBalance()); // 1000
account.deposit(500);
console.log(account.getBalance()); // 1500

// 无法直接访问 balance!
console.log(account.balance); // undefined

JavaScript 闭包详解:工作原理与重要性 插图

2. 工厂函数

function createMultiplier(factor) {
  // factor 被封闭
  return (number) => number * factor;
}

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

console.log(double(5));  // 10
console.log(triple(5));  // 15
console.log(tenX(5));    // 50

3. 记忆化(缓存)

function memoize(fn) {
  const cache = new Map(); // 被返回的函数封闭
  
  return function(...args) {
    const key = JSON.stringify(args);
    
    if (cache.has(key)) {
      console.log('Cache hit!');
      return cache.get(key);
    }
    
    const result = fn.apply(this, args);
    cache.set(key, result);
    return result;
  };
}

const expensiveCalc = memoize((n) => {
  // 模拟耗时的计算
  let result = 0;
  for (let i = 0; i < n * 1000000; i++) result += i;
  return result;
});

expensiveCalc(10); // 计算(慢)
expensiveCalc(10); // 缓存命中!(瞬间)

4. 偏函数应用

function multiply(a, b) {
  return a * b;
}

function partial(fn, ...presetArgs) {
  return function(...laterArgs) {
    return fn(...presetArgs, ...laterArgs);
  };
}

const multiplyByFive = partial(multiply, 5);
console.log(multiplyByFive(3));  // 15
console.log(multiplyByFive(10)); // 50

JavaScript 闭包详解:工作原理与重要性 插图

5. 带状态的事件处理器

function createButton(label) {
  let clickCount = 0;
  
  const button = document.createElement('button');
  button.textContent = label;
  
  button.addEventListener('click', function() {
    // 这个处理器封闭了 clickCount
    clickCount++;
    console.log(`"${label}" clicked ${clickCount} times`);
  });
  
  return button;
}

const btn1 = createButton('Save');    // 有自己的 clickCount
const btn2 = createButton('Delete');  // 有自己的 clickCount

经典的循环 Bug(及修复)

// ❌ Bug:所有处理器封闭了同一个 'i'
for (var i = 0; i < 3; i++) {
  setTimeout(function() {
    console.log(i); // 打印 3, 3, 3 — 而不是 0, 1, 2!
  }, 100);
}
// 当定时器运行时,循环已经结束,i === 3

// ✅ 修复 1:使用 let(每次迭代创建新绑定)
for (let i = 0; i < 3; i++) {
  setTimeout(function() {
    console.log(i); // 打印 0, 1, 2 ✓
  }, 100);
}

// ✅ 修复 2:使用 IIFE 捕获 i
for (var i = 0; i < 3; i++) {
  (function(capturedI) {
    setTimeout(function() {
      console.log(capturedI); // 打印 0, 1, 2 ✓
    }, 100);
  })(i);
}

现代 JavaScript 中的闭包

// React hooks 大量使用闭包
function Counter() {
  const [count, setCount] = useState(0);
  
  // handleClick 封闭了 count 和 setCount
  const handleClick = () => {
    setCount(count + 1); // 使用封闭的 count
  };
  
  return <button onClick={handleClick}>{count}</button>;
}

// 陈旧闭包问题(经典 React bug)
function BadCounter() {
  const [count, setCount] = useState(0);
  
  useEffect(() => {
    const interval = setInterval(() => {
      // ❌ 陈旧闭包:count 始终是第一次渲染时的 0
      setCount(count + 1);
    }, 1000);
    return () => clearInterval(interval);
  }, []); // 空依赖——封闭了初始 count
  
  return <div>{count}</div>;
}

// 修复:使用函数式更新
function GoodCounter() {
  const [count, setCount] = useState(0);
  
  useEffect(() => {
    const interval = setInterval(() => {
      // ✅ 函数式更新:始终使用最新的 count
      setCount(prev => prev + 1);
    }, 1000);
    return () => clearInterval(interval);
  }, []);
  
  return <div>{count}</div>;
}

内存注意事项

// ⚠️ 闭包使外部变量保持存活
function createHeavyObject() {
  const largeData = new Array(1000000).fill('data'); // 100 万个元素
  
  return function() {
    // 只要这个函数存在,largeData 就永远无法被垃圾回收!
    return largeData[0];
  };
}

// ✅ 限制封闭的内容
function createHeavyObjectFixed() {
  const largeData = new Array(1000000).fill('data');
  const firstItem = largeData[0]; // 只捕获你需要的内容
  
  return function() {
    return firstItem; // largeData 现在可以被垃圾回收了
  };
}

总结

  • 闭包是一个函数及其捕获的作用域(词法环境)
  • 闭包实现了数据隐私、工厂函数和带状态的回调
  • JavaScript 中的每个函数都是闭包(它们都捕获了周围的作用域)
  • 注意 React hooks 和异步代码中的陈旧闭包
  • 内存:闭包使引用的变量保持存活——避免不必要地捕获大对象

→ 使用 String Obfuscator 工具混淆你的 JavaScript 代码。