闭包(Closure)是 JS 面试必考、实战必用的知识点,也是"一看就懂、一写就错"的典型。本文从作用域讲起,推导闭包本质,再给出计数器、私有变量等经典应用。

1. 作用域:变量"住在哪里"

JS 有三种作用域:全局(window/global)、函数(var 与函数内部)、块级(let/const 的 {} 内)。内层可读外层变量,反过来不行。

const globalVar = "我是全局的";

function outer() {
  const outerVar = "我是 outer 的";

  function inner() {
    const innerVar = "我是 inner 的";
    console.log(globalVar); // 能读到全局
    console.log(outerVar);  // 能读到外层函数
  }

  inner();
}

outer();

2. 词法作用域:看代码结构,不看调用位置

函数能访问哪些变量,由它在代码里写在哪里决定,而不是"在哪里被调用"决定。这就是词法作用域(Lexical Scope),理解闭包的基石。

const who = "全局的 who";

function sayWho() {
  console.log(who); // 看定义位置:访问的是全局 who
}

function run() {
  const who = "run 里的 who";
  sayWho(); // 输出 "全局的 who",而不是 "run 里的 who"
}

run();

3. 闭包:函数 + 它记住的外部变量

函数"被返回出去"时,会随身携带定义时的词法环境——即使外部函数早已执行完。这个"函数 + 环境"就是闭包

function createCounter() {
  let count = 0;   // 这个变量被"记住"了

  return function () {
    count++;
    return count;
  };
}

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

4. 经典应用一:私有变量

闭包可以模拟"私有状态",外部无法直接读写,只能通过暴露的方法操作——这是模块化思想的雏形。

function createBankAccount(init) {
  let balance = init;

  return {
    deposit(amount) {
      if (amount > 0) balance += amount;
    },
    withdraw(amount) {
      if (amount <= balance) balance -= amount;
    },
    getBalance() {
      return balance;
    },
  };
}

const account = createBankAccount(100);
account.deposit(50);
account.withdraw(30);
console.log(account.getBalance()); // 120
console.log(account.balance);      // undefined,拿不到!

5. 经典陷阱:循环中的闭包

var 在循环里创建闭包是个著名陷阱:所有回调共享同一个变量。用 let(或 IIFE 每次捕获)可以修复。

// 陷阱:var 没有块级作用域
for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100);
}
// 输出:3 3 3(var 只有一个,循环后是 3)

// 修复:用 let
for (let j = 0; j < 3; j++) {
  setTimeout(() => console.log(j), 100);
}
// 输出:0 1 2

6. 闭包与内存

闭包让外部变量一直存活,这是特性也是负担:闭包被长期持有时,其引用的变量无法被垃圾回收,用完的监听器、定时器记得清理。原则:闭包只捕获最小数据;页面销毁时 removeEventListener 解除引用。

7. 闭包速记清单

💡 检验是否真懂闭包:不看资料,手写一个"只允许调用 3 次,之后报错"的函数。写不出来就再读一遍本文第 3、4 节。