闭包(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 节。