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

作者:??四霉? 时间:2024-04-19 10:04:07 

深入作用域链与闭包

为什么要把作用域链和闭包放在一起讲呢,它们有什么关联吗?

试想,我们如果在一个内部的函数使用了外部的变量,是通过[[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 声明的,所以会&ldquo;逸出&rdquo;保存到 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();

这里我们新建了两个实例,按照上文的理论,二者的闭包应该是独立的,且所有子函数无论深度以及子函数是否被调用都会共用一个闭包。

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

上图的包含变量 a,b 的子函数并没有调用,但是在闭包中仍然存在。

返回的匿名函数和 bar 有使用到的变量在同一个闭包 foo.Closure 中

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

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

这里我们调用fun1.addNum 修改了 d 的值,但是实例 fun2 的闭包中的 d 仍然是 4 。可以看出两个闭包是独立的。

 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  // 指向外部词法环境
         }
     }
     ...
 }

来源:https://juejin.cn/post/7119856864078233614

标签:JavaScript,作用,域链,闭包
0
投稿

猜你喜欢

  • 脚本测试postman快速导出python接口测试过程示例

    2022-11-03 09:28:10
  • python3使用mutagen进行音频元数据处理的方法

    2023-05-25 04:05:32
  • asp按关键字查询XML的代码

    2011-04-21 11:10:00
  • Oracle 数据库 临时数据的处理方法

    2009-07-02 11:48:00
  • SQL server分页的四种方法思路详解(最全面教程)

    2024-01-16 20:19:52
  • ActionScript3.0是革命性的

    2008-05-01 12:36:00
  • 如何理解python对象

    2022-09-04 06:58:54
  • Python自动化办公之Word文件内容的读取

    2021-04-06 18:18:18
  • python3使用python-redis-lock解决并发计算问题

    2021-05-09 16:04:18
  • mysql和oracle默认排序的方法 - 不指定order by

    2024-01-27 17:44:28
  • Vue.js监听select2的值改变进行查询方式

    2024-04-30 10:42:13
  • 关于《回访确认》的几个问题

    2009-08-24 12:43:00
  • Python+OpenCV实现将图像转换为二进制格式

    2021-06-25 08:10:33
  • 详解Python字符串切片

    2021-09-10 05:10:43
  • Python3.6笔记之将程序运行结果输出到文件的方法

    2023-08-02 08:27:44
  • python异步Web框架sanic的实现

    2021-01-17 01:37:57
  • Windows 平台做 Python 开发的最佳组合(推荐)

    2022-12-08 05:38:51
  • 在MySQL中使用子查询和标量子查询的基本操作教程

    2024-01-15 15:00:08
  • 如何使用共享连接减少空闲的连接数?

    2010-05-16 15:15:00
  • 解读ASP.NET 5 & MVC6系列教程(17):MVC中的其他新特性

    2023-07-11 10:44:59
  • asp之家 网络编程 m.aspxhome.com