闭包不是魔法是作用域链的必然结果很多和我一样的初学者在一开始学习闭包Closure的时候觉得是JS的某种特异功能。但是实际上闭包在ECMAScript 规范中是一个自然产物。要彻底理解闭包我们必须拆解V8 引擎在执行代码的时候的底层逻辑调用栈call stack执行上下文execution context以及词法环境lexical environment中outer的引用。一. 执行上下文与 outer在 JavaScript 中每当一个函数被调用引擎就会为它创建一份执行上下文Execution Context并压入调用栈。 每个执行上下文中都包含一个词法环境Lexical Environment。这个环境内部有两个重要组成部分环境记录Environment Record存放当前函数内部声明的变量和函数。外部环境引用outer指向它在词法上写代码的位置的外层执行上下文。 正是这个 outer 引用构成了我们常说的作用域链Scope Chain。当引擎在当前函数的环境中找不到某个变量时就会顺着 outer 指向的外部环境一路向上查找直到全局环境。底层铁律outer 的指向在函数定义声明的时候就已经决定了而不是在函数执行调用的时候决定。这就是“词法作用域”。二.从内存视角拆解一个标准闭包我们用一段最经典的闭包代码来看看当它被 V8 引擎执行时内存和调用栈里究竟发生了什么function createCounter() { let count 0; function change() { count; console.log(count); } return change; } const counter createCounter(); counter(); // 11. 执行 createCounter() 时createCounter 的执行上下文被压入调用栈。它的词法环境中变量 count 被初始化为 0同时定义了函数 change。注意此时 change 函数作为一个对象被创建由于它在源码里写在 createCounter 内部V8 引擎在创建它时会赋予它一个隐藏属性 [[Scopes]]这个属性会保持对当前 createCounter 词法环境的引用。2. createCounter() 执行完毕并返回时按照常规逻辑一个函数执行完它的执行上下文就会从调用栈弹出并销毁释放内存。但是它的内部函数 change 被返回了并被全局变量 counter 引用。因为 counter即 change还活着而 counter 的 [[Scopes]] 属性死死勾住了 createCounter 的词法环境。3. V8 引擎的破例Closure 对象的诞生V8 发现 createCounter 虽然退栈了但它里面的 count 变量还在被内部函数引用着。于是垃圾回收机制GC不会清理这段内存。 V8 会把 change 函数用到的外部变量这里是 count打包在堆内存Heap中创建一个专门的对象这个对象就叫Closure闭包。三. 调用闭包函数时的 outer 查找规则现在我们执行 counter()即调用 change 函数V8 创建 change 的执行上下文压入调用栈。此时change 的词法环境被创建它的 outer 引用指向哪里指向它出生时的那个外层环境即保留在堆内存中的 createCounter 的 Closure 空间。3.执行 count引擎先在 change 本地环境中找 count没找到。顺着 outer 链条进入 createCounter 的闭包环境找到了 count将其修改为 1。 当 counter() 执行完change 的上下文弹栈销毁但那个堆内存中的 Closure 闭包空间依然存在。下一次你再调用 counter()它依然顺着 outer 找到同一个 count 变量实现累加。四.为什么要从底层理解闭包如果只停留在比喻层面你很难解释下面这两个高级前端面试必考的“深水区”问题1. 内存泄漏的本质是什么如果闭包函数如上面的 counter一直存活在全局作用域中没有被置为 null那么它通过 outer 间接引用的整条作用域链上的变量都无法被垃圾回收。这相当于在堆内存里钉死了一块空间用得多了就会导致内存泄漏。2. V8 引擎的闭包优化现代 V8 引擎非常智能。如果外层函数有一百个变量但内部函数只用到了一个V8只会把用到的那个变量放进 Closure 对象中其余没用到的变量在父函数弹栈时依然会被无情销毁。这种精细化的内存控制只有理解了底层原理才能真正体会。闭包是语言设计的必然闭包不是动态注入的补丁它是“函数作为一等公民First-class Function”与“词法作用域Lexical Scope”碰撞后的必然产物。 只要 JavaScript 允许函数作为返回值且作用域由书写位置决定那么通过 outer 引用将父级环境锁死在堆内存中的“闭包机制”就是维持程序逻辑正确的唯一解。
《闭包:一个函数偷偷带走了我家的糖》—— 零基础也能懂的JS闭包
闭包不是魔法是作用域链的必然结果很多和我一样的初学者在一开始学习闭包Closure的时候觉得是JS的某种特异功能。但是实际上闭包在ECMAScript 规范中是一个自然产物。要彻底理解闭包我们必须拆解V8 引擎在执行代码的时候的底层逻辑调用栈call stack执行上下文execution context以及词法环境lexical environment中outer的引用。一. 执行上下文与 outer在 JavaScript 中每当一个函数被调用引擎就会为它创建一份执行上下文Execution Context并压入调用栈。 每个执行上下文中都包含一个词法环境Lexical Environment。这个环境内部有两个重要组成部分环境记录Environment Record存放当前函数内部声明的变量和函数。外部环境引用outer指向它在词法上写代码的位置的外层执行上下文。 正是这个 outer 引用构成了我们常说的作用域链Scope Chain。当引擎在当前函数的环境中找不到某个变量时就会顺着 outer 指向的外部环境一路向上查找直到全局环境。底层铁律outer 的指向在函数定义声明的时候就已经决定了而不是在函数执行调用的时候决定。这就是“词法作用域”。二.从内存视角拆解一个标准闭包我们用一段最经典的闭包代码来看看当它被 V8 引擎执行时内存和调用栈里究竟发生了什么function createCounter() { let count 0; function change() { count; console.log(count); } return change; } const counter createCounter(); counter(); // 11. 执行 createCounter() 时createCounter 的执行上下文被压入调用栈。它的词法环境中变量 count 被初始化为 0同时定义了函数 change。注意此时 change 函数作为一个对象被创建由于它在源码里写在 createCounter 内部V8 引擎在创建它时会赋予它一个隐藏属性 [[Scopes]]这个属性会保持对当前 createCounter 词法环境的引用。2. createCounter() 执行完毕并返回时按照常规逻辑一个函数执行完它的执行上下文就会从调用栈弹出并销毁释放内存。但是它的内部函数 change 被返回了并被全局变量 counter 引用。因为 counter即 change还活着而 counter 的 [[Scopes]] 属性死死勾住了 createCounter 的词法环境。3. V8 引擎的破例Closure 对象的诞生V8 发现 createCounter 虽然退栈了但它里面的 count 变量还在被内部函数引用着。于是垃圾回收机制GC不会清理这段内存。 V8 会把 change 函数用到的外部变量这里是 count打包在堆内存Heap中创建一个专门的对象这个对象就叫Closure闭包。三. 调用闭包函数时的 outer 查找规则现在我们执行 counter()即调用 change 函数V8 创建 change 的执行上下文压入调用栈。此时change 的词法环境被创建它的 outer 引用指向哪里指向它出生时的那个外层环境即保留在堆内存中的 createCounter 的 Closure 空间。3.执行 count引擎先在 change 本地环境中找 count没找到。顺着 outer 链条进入 createCounter 的闭包环境找到了 count将其修改为 1。 当 counter() 执行完change 的上下文弹栈销毁但那个堆内存中的 Closure 闭包空间依然存在。下一次你再调用 counter()它依然顺着 outer 找到同一个 count 变量实现累加。四.为什么要从底层理解闭包如果只停留在比喻层面你很难解释下面这两个高级前端面试必考的“深水区”问题1. 内存泄漏的本质是什么如果闭包函数如上面的 counter一直存活在全局作用域中没有被置为 null那么它通过 outer 间接引用的整条作用域链上的变量都无法被垃圾回收。这相当于在堆内存里钉死了一块空间用得多了就会导致内存泄漏。2. V8 引擎的闭包优化现代 V8 引擎非常智能。如果外层函数有一百个变量但内部函数只用到了一个V8只会把用到的那个变量放进 Closure 对象中其余没用到的变量在父函数弹栈时依然会被无情销毁。这种精细化的内存控制只有理解了底层原理才能真正体会。闭包是语言设计的必然闭包不是动态注入的补丁它是“函数作为一等公民First-class Function”与“词法作用域Lexical Scope”碰撞后的必然产物。 只要 JavaScript 允许函数作为返回值且作用域由书写位置决定那么通过 outer 引用将父级环境锁死在堆内存中的“闭包机制”就是维持程序逻辑正确的唯一解。