文章

闭包的定义与形成原理深度解析

深入解析闭包的定义、形成条件、词法环境与作用域链的底层原理

闭包的定义与形成原理深度解析

一句话概括

闭包是一个记住它诞生时作用域”的函数,它能访问外部函数的变量——这是 JavaScript 最核心也最常被面试追问的概念。

背景

闭包是 JavaScript 的灵魂。无论你是写前端框架(React/Vue)、封装工具函数(防抖/节流),还是阅读源码,闭包无处不在。

面试中,闭包是基础考察题

  • “什么是闭包?” —— 送分题
  • “闭包有什么实际应用?” —— 进阶题
  • “闭包会导致内存泄漏,怎么解决?” —— 深度题

理解闭包,才能真正理解 JavaScript 的作用域、模块化、和函数式编程。

概念与定义

什么是闭包?

MDN 定义:闭包是指能够访问自由变量的函数

更直白的说法:闭包 = 函数 + 函数诞生时的环境

1
2
3
4
5
6
7
8
9
10
11
12
function outer() {
  const a = 1;  // 外部变量

  function inner() {
    console.log(a);  // 访问外部变量
  }

  return inner;  // 返回函数(它带着诞生时的环境)
}

const fn = outer();  // fn 就是一个闭包
fn();  // 输出 1

形成闭包的三个条件

  1. 存在嵌套函数:函数 A 内部定义函数 B
  2. 内部函数引用外部变量:B 使用了 A 中的变量
  3. 内部函数被返回或在外部被引用:否则会立即被垃圾回收

最小示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// ❌ 没有形成闭包:内部函数没有被引用
function outer() {
  const a = 1;
  function inner() {
    console.log(a);
  }
  inner();  // 直接调用,没有返回
}
// inner 会在 outer 执行结束后被销毁

// ✅ 形成闭包:inner 被返回
function outer() {
  const a = 1;
  function inner() {
    console.log(a);
  }
  return inner;  // 返回函数,inner 不会被销毁
}

const fn = outer();  // fn 带着闭包存活
fn();  // 输出 1

核心知识点拆解

闭包的本质:词法环境(Lexical Environment)

JavaScript 引擎用词法环境记录变量:

1
2
3
4
5
6
7
8
9
10
11
// 词法环境结构
{
  // 环境记录:存储变量
  environmentRecord: {
    a: 1,
    outer: function,
    inner: function
  },
  // 外部引用:指向父级作用域
  outer: <GlobalEnvironment>
}

闭包的核心原理

  • 每个函数创建时,会记住定义时的词法环境
  • 即使函数在外部执行,依然能访问定义时的变量

闭包与作用域链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const global = "全局";

function outer() {
  const outerVar = "外部";

  function inner() {
    const innerVar = "内部";
    console.log(global, outerVar, innerVar);
  }

  return inner;
}

const fn = outer();
fn();  // 输出: 全局 外部 内部

作用域链查找顺序

  1. 先在 inner 的环境记录中找 → 找到 innerVar
  2. 没找到 → 通过 outer 引用去 outer 的环境记录找 → 找到 outerVar
  3. 还没找到 → 通过 outer 的 outer 引用去全局环境找 → 找到 global

闭包的三个经典问题

问题现象原因解决方案
for 循环闭包点击任意按钮都输出 5循环结束后变量值为 5使用 let / 闭包包一层 / forEach
定时器闭包定时器访问的永远是最终值闭包捕获的是变量引用使用 let / 闭包传参
内存泄漏闭包导致变量无法释放闭包被全局引用手动置空 / 避免全局引用

实战案例

案例一:循环中的闭包陷阱

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// ❌ 经典错误:所有回调都输出 5
for (var i = 0; i < 5; i++) {
  setTimeout(() => console.log(i), 100);
}
// 输出: 5, 5, 5, 5, 5

// ✅ 解决方案1:使用 let(块级作用域)
for (let i = 0; i < 5; i++) {
  setTimeout(() => console.log(i), 100);
}
// 输出: 0, 1, 2, 3, 4

// ✅ 解决方案2:闭包包一层(IIFE)
for (var i = 0; i < 5; i++) {
  ((j) => {
    setTimeout(() => console.log(j), 100);
  })(i);
}

// ✅ 解决方案3:传入参数
for (var i = 0; i < 5; i++) {
  setTimeout(((j) => () => console.log(j))(i), 100);
}

案例二:防抖节流封装

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 防抖函数:闭包保存每次调用的参数
function debounce(fn, delay) {
  let timer = null;  // 闭包变量

  return function (...args) {
    if (timer) clearTimeout(timer);
    timer = setTimeout(() => {
      fn.apply(this, args);
    }, delay);
  };
}

// 使用
const handleSearch = debounce((value) => {
  console.log('搜索:', value);
}, 300);

案例三:模块化模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 使用闭包实现私有变量
const Counter = (() => {
  let count = 0;  // 私有变量

  return {
    getCount: () => count,
    increment: () => ++count,
    reset: () => {
      count = 0;
      return count;
    }
  };
})();

Counter.increment();
Counter.increment();
console.log(Counter.getCount());  // 2
Counter.reset();
console.log(Counter.getCount());  // 0
// console.log(count);  // 报错:count is not defined

底层原理

V8 引擎对闭包的优化

V8 使用预编译+解释执行混合模式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function outer() {
  const a = 1;
  function inner() {
    return a + 1;
  }
  return inner;
}

// 编译阶段(Ignition)
// 1. 解析函数,生成 AST
// 2. 生成字节码:LoadContextVariable a, Return

// 执行阶段
// 1. 创建闭包对象:{ [[Scope]]: outerContext }
// 2. inner 的 [[Scope]] 指向 outerContext
// 3. 访问 a 时,沿着作用域链查找

闭包与内存分配

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function outer() {
  const largeData = new Array(1000000);  // 大对象

  function inner() {
    console.log(largeData[0]);  // 引用了 largeData
  }

  return inner;
}

const fn = outer();
// 闭包结构:
// fn: {
//   [[Scope]]: {
//     largeData: <Array(1000000)>,  // 闭包作用域中的大对象
//     inner: <Function>
//   }
// }

💡 人话总结

  • 闭包就是一个带着出生证”的函数
  • 这个出生证记录了它出生时周围有什么”邻居”(变量)
  • 即使它被传到天涯海角,只要它还活着,这些”邻居”就不能被销毁
  • 这既是闭包的强大之处(记住状态),也是内存泄漏的根源

高频面试题解析

Q1:什么是闭包?闭包有什么优点和缺点?

参考答案: 闭包是指能够访问外部函数作用域中变量的函数。

优点

  1. 私有化变量:形成私有作用域,保护变量不被污染
  2. 记忆状态:闭包可以保存函数创建时的状态
  3. 函数工厂:可以创建带预设参数的函数

缺点

  1. 内存泄漏:闭包会持有外部变量引用,导致无法被垃圾回收
  2. 内存占用:比普通函数占用更多内存
  3. 性能损耗:作用域链查找比局部变量慢

Q2:闭包在实际开发中有什么应用场景?

参考答案:

  1. 防抖/节流:闭包保存定时器状态
  2. 模块化:创建私有变量和接口
  3. 函数柯里化:保存部分参数
  4. 偏函数:预设参数
  5. 私有方法:如 React useEffect 的 cleanup 函数

Q3:for 循环中使用 var,为什么闭包会出问题?

参考答案: var 声明的变量是函数作用域(或全局作用域),没有块级作用域。

1
2
3
for (var i = 0; i < 5; i++) {
  setTimeout(() => console.log(i), 100);
}

执行过程:

  1. 循环创建 5 个 setTimeout
  2. 所有回调函数都引用同一个变量 i
  3. 循环结束时 i = 5
  4. 100ms 后所有回调执行,读取 i = 5

解决:

  • 使用 let(块级作用域,每次迭代创建新变量)
  • 使用 IIFE 创建新作用域
  • 使用 forEach 替代 for

Q4:如何解决闭包导致的内存泄漏?

参考答案: 内存泄漏的场景:

  1. 闭包被全局变量引用,外部变量无法释放
  2. DOM 事件监听器未移除
  3. 定时器未清理

解决方案:

  1. 手动置空:使用完闭包后,将引用置为 null
  2. 及时清理:组件销毁时移除事件监听和定时器
  3. 避免全局引用:尽量在函数内使用闭包,不要把闭包挂在 window 上
  4. 使用 WeakMap:WeakMap 的键是弱引用,不会阻止垃圾回收

总结与扩展

核心要点

  1. 闭包 = 函数 + 词法环境:函数定义时记住诞生时的环境
  2. 三个形成条件:嵌套函数 + 引用外部变量 + 被返回/引用
  3. 作用域链查找:从内到外逐层查找变量
  4. 经典陷阱:for 循环 + var / 定时器闭包 / 内存泄漏

延伸学习方向

  • JavaScript 执行上下文:理解变量环境和作用域链的载体
  • V8 引擎优化:HiddenClass、Inline Cache 对闭包的影响
  • 函数式编程:闭包在柯里化、偏函数中的应用
  • 模块化规范:CommonJS、ES Module、IIFE 模块化的区别

相关主题

本文由作者按照 CC BY 4.0 进行授权