闭包是面试必问、也是最绕的概念。大白话:函数里再套个函数,这个小函数还一直记着外面的变量。打个比方:你的私人储物柜(外层变量)锁在办公室里,别人进不来;但你的助理(内层函数)能帮你存取。你出差走了(外层执行完了),助理还在,储物柜的东西也还没丢。
| 要素 | 大白话 |
|---|---|
| ① 外层函数 | 里面藏着一个私人变量 |
| ② 内层函数用了外层变量 | 小助理要碰你储物柜里的东西 |
| ③ 把内层 return 出去存住 | 外面用变量接住,外层跑完东西也不丢 |
function outer() {
let count = 0; // ① 外层的私人储物柜
function inner() { // ② 小助理 inner,要用这个柜子
count++;
console.log(count);
}
return inner; // ③ 把小助理交出去
}
let fn = outer(); // outer跑完了,但fn接住了小助理
fn(); // 1 ← 柜子没丢,助理还记得
fn(); // 2 ← 接着往上加
let fn1 = outer(); // 雇第一个助理,给他一个柜子 let fn2 = outer(); // 雇第二个助理,另一个柜子 fn1(); fn1(); // 1 2(fn1记自己的) fn2(); fn2(); // 1 2(fn2记自己的,各管各)
如果你想让它打印 0、1、2,结果它打印 3、3、3,就会一脸懵。因为 var i 是大家共用的同一个,等 100 毫秒后定时器真去读的时候,循环早就跑完,i 已经停在 3 了。
for (var i = 0; i < 3; i++) {
// 定时器100毫秒后才执行,但那时循环早跑完了
setTimeout(function () {
console.log(i); // 三个函数读的是同一个 i
}, 100);
}
// 输出:3 3 3(i 最后停在 3)
for (var i = 0; i < 3; i++) {
(function (j) { // 每一轮立刻新开一个小隔间
setTimeout(function () {
console.log(j); // 这个隔间锁死了当时的 j
}, 100);
})(i); // 把当前 i 传进去当 j
}
// 输出:0 1 2
好处:把私人变量藏起来(外面改不了)、能记住状态(计数器、缓存)。坏处:这些变量不会自动被清掉,一直占内存。用完要主动松手:fn = null;。
① 用在哪:做私有计数器、收藏夹开关、保存上次状态的小工具。
② 常见坑:循环里 var + 定时器打印出 333;闭包用完不释放一直占内存。
③ 怎么解决:循环里用 let,或 IIFE 把每次 i 锁进去;不用了 fn = null 释放。