JavaScript深入理解作用域链与闭包

作者:小括号

时间:2026-10-25

浏览:440

本文深入解析JavaScript中的作用域链与闭包机制。通过[[wp:a0]]串起的词法环境解释作用域链形成原理,阐述函数实例与闭包的绑定关系。结合V8引擎实现,分析[[wp:a16]]与[[wp:a27]]在循环中的不同表现,揭示闭包作为运行期概念如何保持外部变量状态,并通过代码实例验证闭包的独立性与变量绑定特性。

文章目录

深入作用域链与闭包

为何将作用域链与闭包合并讲解,二者存在何种关联?

若内部函数引用外部变量,该过程通过[[outerEnv]]串接的词法环境(各类环境记录)实现,此即浏览器层面的作用域链[[Scope]]。

闭包的触发要求在一个独立空间中管理从外部获取的变量,而该外部变量的获取与绑定需依赖作用域链。

因此,理解作用域链的形成原理是深入理解闭包的前提。

作用域链

前文对函数中变量记录的阐释尚不完整,仅简单指出VariableEnvironemnt.[[outerEnv]]指向外部。鉴于JavaScript中函数亦为对象,若每次均需解析全局词法以获取函数外部环境,将严重损耗性能。

[[Environment]]

故在函数声明时,其内部属性[[Environment]]即被设定,依据规范,该属性亦为环境记录,其[[outerEnv]]指向声明函数时的词法环境。

10.2 ECMAScript Function Objects) Internal Slots of ECMAScript Function Objects

Internal SlotTypeDescription
[[Environment]]an Environment RecordThe Environment Record that the function was closed over. Used as the outer environment when evaluating the code of the function.

完善环境记录

函数执行期间,所创建的词法环境与变量环境均存储于[[Environment]]中。

 function foo() {
     var a = 1;
     let b = 2;
 }
 foo();

函数执行前创建上下文时,更严谨的解释如下:

 ExecutionContext: {
     [[Environment]](0x00): {
         LexicalEnvironment(0x01): {
             b -> nothing
             [[outerEnv]]: 0x02
         }
         VariableEnvironment(0x02): {
             a -> undefined
             [[outerEnv]]: 0x00
         }
         ...
         [[outerEnv]]: global
     }
 }

闭包

函数实例

为阐释闭包,需先理解函数实例化:声明函数时可使用new Function来实例化函数对象。

  • 函数声明:可视为函数实例化,创建了原型Function的一个实例。
  • 函数表达式:则用于创建函数的实例。

以下示例展示同一函数代码块可生成多个函数实例:

 function foo() {
     return function myFun() {}
 }
 const fun1 = foo()
 const fun2 = foo()
 console.log(fun1 === fun2) // false

此处,myFun为函数表达式,而fun1/fun2代表两个不同的实例。

什么是闭包

基于此概念,作用域链与闭包的关系可表述为:

每生成一个函数实例,其内部均存在一条由环境记录(含函数自身)串成的作用域链;闭包可视为与该函数实例作用域链绑定的映像。

具体实践中(如V8引擎),函数预编译时解析内部词法,无论深度或子函数是否调用,只要内部引用外部变量,便将其存入同一闭包;因变量通过作用域链获取绑定,故闭包可视为作用域链的简化副本。

此外,该闭包可视为父函数的属性,且同一实例中所有子函数共享同一闭包,后文将对此验证。

变量绑定

为何提及绑定?

  • 当外部变量变更时,闭包内对应变量亦随之变更。
  • 在闭包中修改外部变量,其绑定的环境记录中的变量亦会变化。
 let a = 1;
 let b = 2;
 function foo() {
   return function () {
     a += 1;
     b += 10;
     console.log(a, b);
   };
 }
 const bar = foo();
 bar();  // 2 12
 bar();  // 3 22
 a += 10;
 bar();  // 14 32
 a = 0;
 bar();  // 1 42
 cnsole.log(a) // 1 (在闭包中+1,全局环境中的 a 也对应+1)

此绑定亦可解释一道经典面试题(前置知识参考《环境变量》)。

 function foo() {
   for (var i = 0; i < 6; i++) {
     setTimeout(() => {
       console.log(i);
     }, i * 100);
   }
 }
 foo();  // 6 6 6 6 6

因i由var声明,故“逸出”至foo的变量环境中;因此setTimeout中匿名函数闭包内的i与foo环境绑定。执行i++(即foo的i++)时,闭包内i随之变化。因所有闭包绑定同一环境记录,故显示相同值;退出循环后仍执行一次i++,故输出6而非5。

反之,考察由let声明的i表现。

 function foo() {
   for (let i = 0; i < 6; i++) {
     setTimeout(() => {
       console.log(i);
     }, i * 100);
   }
 }​
 foo();  // 1 2 3 4 5

此处i由let声明,故保存至最近词法环境(块级作用域)。每次循环形成新块级作用域,因此i保存的环境各异,即每个setTimeout匿名函数闭包内i绑定不同环境记录,可单独管理。

同一个闭包

前文提及,同一函数实例中所有子函数共用一个闭包,现用代码验证。

 function foo() {
   let a = 1;
   const b = 2;
   let c = 3;
   let d = 4;
   function bar() {  // 验证深度以及没有被调用的情况
     console.log(a);
     function barSon() { 
       console.log(b);
     }
   }
   return function () {
     console.log(d);
     return {
       addNum() {
         d = "new" + d;
       },
     };
   };
 }
 const fun1 = foo();
 fun1().addNum();    // 验证不同实例的闭包空间独立
 fun1();

 const fun2 = foo();
 fun2();

新建两个实例,依理论,二者闭包应独立,且所有子函数无论深度及是否调用均共用一个闭包。

深入作用域链与闭包

上图含变量a,b的子函数虽未调用,但在闭包中仍存在。

返回的匿名函数与bar所用变量位于同一闭包foo.Closure中。

深入作用域链与闭包

深入作用域链与闭包

调用fun1.addNum修改d值,但实例fun2闭包内d仍为4,表明两闭包独立。

总结

与静态函数实例对应,闭包为运行期概念,在函数执行过程中处于激活、可访问状态,并在函数实例调用结束后保持数据最终状态,直至闭包销毁。(体现为不断调用同一函数输出不同值,这些值源自上次调用的最终数据状态)。

实际上,若当前函数引用外部变量,会新建环境记录作为闭包(规范定义),其存在于[[Environment]].[[outerEnv]]的可变绑定中。(浏览器中,只要函数内部引用外部变量,其内部属性[[Scope]]中将包含名为Closure的闭包)。

以下为上一例执行完毕后两个函数实例的[[Scope]]。

深入作用域链与闭包

最后举例形象说明闭包在作用域链中的位置。

 function foo() {
   let a = 1;
   const b = 2;
   let c = 3;
   let d = 4;
   function bar() {  // 验证深度以及没有被调用的情况
     console.log(a);
     function barSon() { 
       console.log(b);
     }
   }
   return function myFun() {
     console.log(d);
   };
 }​
 const fun1 = foo();
 // 词法编译时,假设每个空间都有一个的堆内存
 ExecutionContext(foo): {
     [[outerEnv]]: global
     ...
     [[Environment]](0x00): {
         LexicalEnviroemnt(0x01): {
             [[outerEnv]]: 0x00
             ...
             a —> nothing, b -> nothing, c -> nothing, d -> nothing
             bar: {
                 LexicalEnviroemnt(0x02): {...}
                 [[Environment]] : {
                     [[outerEnv]] : 0x10 // 指向闭包
                     ...
                 }
                 barSon: {
                     [[Environment]] : {
                         // 指向上一层函数的词法环境,如果有闭包则会指向上一层函数的闭包
                         [[outerEnv]] : 0x02
                         ...
                     }
                 }
             }
             myFun: {
                 [[Environment]] : {
                     [[outerEnv]] : 0x10 // 指向闭包
                     ...
                 }
             }
         }
         // 即所谓的闭包,这里面变量的来源于外部的环境记录(某种映射)
         EnvironmentRecord(0x10): {
             a -> nothing, b -> nothing, d -> nothing
             [[outerEnv]]: 0x01  // 指向外部词法环境
         }
     }
     ...
 }

至此,关于JavaScript深入理解作用域链与闭包详情的文章介绍完毕,更多相关JS作用域链与闭包内容请搜索本站以前的文章或继续浏览下面的相关文章希望大家以后多多支持本站!

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章