← 返回目录

12. 闭包原理与循环坑

闭包就是函数套函数,内层函数能记住外层的变量,即使外层已经执行完。

核心三步:
外层函数——定义变量
内层函数——使用外层变量
return 内层——把内层函数交出去,外部还能访问

坑:循环里用 var 做闭包全部输出最后一个值、闭包会占用内存、用 let 解决循环闭包问题

12.1 互动演示(闭包计数器 + 循环 var 打印 333 坑)

点「+1」看数字记住了没有;再看循环闭包那个著名的 333 坑。

12.2 知识点逐条讲解(配合代码)

① 闭包三要素(少一个都不算)

要素
① 外层函数里面藏着一个私人变量
② 内层函数用了外层变量小助理要碰你储物柜里的东西
③ 把内层 return 出去存住外面用变量接住,外层跑完东西也不丢
function outer() {
  // ① 外层的私人储物柜
  let count = 0;
  // ② 小助理 inner,要用这个柜子
  function inner() {
    count++;
    console.log(count);
  }
  // ③ 把小助理交出去
  return inner;
}

// outer跑完了,但fn接住了小助理
let fn = outer();

// 1 ← 柜子没丢,助理还记得
fn();

// 2 ← 接着往上加
fn();

② 多雇几个助理,各记各的账

// 雇第一个助理,给他一个柜子
let fn1 = outer();

// 雇第二个助理,另一个柜子
let fn2 = outer();

fn1();

// 1 2(fn1记自己的)
fn1();

fn2();

// 1 2(fn2记自己的,各管各)
fn2();

③ 著名坑:for + var 闭包打印 3 3 3

如果你想让它打印 0、1、2,结果它打印 3、3、3,就会一脸懵。因为 let i 是大家共用的同一个,等 100 毫秒后定时器真去读的时候,循环早就跑完,i 已经停在 3 了。

// 定时器100毫秒后才执行,但那时循环早跑完了
for (let i = 0; i < 3; i++) {
  setTimeout(function () {
    // 三个函数读的是同一个 i
    console.log(i);
  }, 100);
// 输出:3 3 3(i 最后停在 3)
}

④ 修复:IIFE 立即函数,把每一轮的 i 单独锁起来

for (let i = 0; i < 3; i++) {
  // 每一轮立刻新开一个小隔间
  (function (j) {
    setTimeout(function () {
      // 这个隔间锁死了当时的 j
      console.log(j);
    }, 100);
  // 把当前 i 传进去当 j
  })(i);
// 输出:0 1 2
}

⑤ 优缺点与释放

好处:把私人变量藏起来(外面改不了)、能记住状态(计数器、缓存)。坏处:这些变量不会自动被清掉,一直占内存。用完要主动松手:fn = null;

⑥ X.3 实战:用在哪 / 常见坑 / 怎么解决

① 用在哪:做私有计数器、收藏夹开关、保存上次状态的小工具。

② 常见坑:循环里 var + 定时器打印出 333;闭包用完不释放一直占内存。

③ 怎么解决:循环里用 let,或 IIFE 把每次 i 锁进去;不用了 fn = null 释放。

一句话记住:闭包=函数套函数、小函数记住外面的变量、外层跑完变量也不丢;用来做私人计数器;for+var 闭包会打印 333,用 IIFE 把每轮 i 锁进自己的隔间;用完 fn=null 松手

本页重点

API作用参数返回值代码示例
函数套函数内层记住外层的私人变量
function outer() {
  function inner() {}
}
return 内层函数把小函数交出去存住函数函数
function outer() {

  return function () {};
}
fn = null用完松手释放闭包
fn = null;  // 放走,才能回收