JavaScript闭包与词法作用域的引擎内部实现细节
闭包并非语法特性,而是引擎基于词法作用域与函数一等公民自然产生的行为。函数定义时绑定词法环境,外层执行上下文弹出后,词法环境因内部函数引用存活于堆内存。闭包共享变量环境引用而非副本,V8通过变量分析仅保留被实际引用的外层变量以优化内存。
先说一个核心判断:闭包绝不是 JavaScript 语法层面设计出的“技巧”。它并非特殊的语法糖,而是引擎(如 V8)在执行上下文与词法环境这一底层机制中,自然产生的行为。换言之,只要你的编程语言支持函数作为一等公民,并采用词法作用域,闭包就必然存在——它不是开发者主动“创造”的,而是语言机制自带的产物。
来看底层是如何运作的。
每个函数在定义的那一刻,引擎都会为其绑定一个**词法环境(Lexical Environment)**。该词法环境由两部分组成:一个叫“环境记录”的映射表,用于存储变量、参数、函数声明;另一个是“外层词法环境引用”,指向函数定义位置所在作用域的上一层环境。这个引用在函数创建时即被固化,不可更改。
举个例子:function outer() { let x = 1; return function inner() { return x; } }。这里的 inner 函数被创建时,其词法环境中的 outer 引用直接指向了 outer 函数的词法环境。即使 outer() 执行完毕,执行上下文被弹出调用栈,这个引用依然存在——这才是所有闭包行为的根源。
很多人误以为“执行上下文栈”决定了变量的存活。实际上并非如此。调用栈只负责管理函数的执行顺序,并不决定变量的生命周期。当 outer() 返回 inner 之后:
outer的执行上下文确实从栈中被弹出;- 但其词法环境对象并没有被销毁——因为
inner的词法环境仍然持有对它的引用; - 对于 V8 的垃圾回收器而言,这个对象是“可达”的,所以不会被回收;
- 只要
inner函数或其引用链上的对象还存活,outer的词法环境就会一直驻留在堆内存中。
一句话总结:闭包延长的不是执行上下文的寿命,而是词法环境对象的寿命。
接下来澄清一个常见的误解。很多人认为闭包是“保存了变量的副本”,但实际上闭包持有的是一条“对变量所在环境记录的引用”。这意味着:
- 如果外层变量是基本类型,比如
let count = 0,闭包中读写的就是同一块内存地址上的值; - 如果外层变量是对象,那么所有闭包共享同一个对象引用,修改会互相可见;
- 多次调用
outer()会生成多个独立的词法环境,每个返回的inner实例绑定各自的外层环境,互不干扰。
这也是为什么 for 循环中使用 var 声明的变量容易出问题,而 let 则不会。因为每次 let 声明都会创建一个新的绑定,对应独立的环境记录项。
最后聊一下 V8 的优化策略。引擎并不会盲目地将所有外层变量都保留下来。在编译阶段,V8 会执行一个称为“闭包变量分析”的过程:
- 只将内部函数实际访问到的那些外层变量,纳入词法环境的引用链中;
- 同一作用域内未被引用的变量,会被忽略,不参与闭包保留;
- 如果函数根本不访问任何外层变量,V8 甚至可能直接省略这个
outer引用,使其退化为一个普通函数。
这种优化非常关键,可以显著降低内存占用。所以,闭包并不像表面上看起来那么“重”,现代 JS 引擎在处理这个问题上已经非常精细了。

