好得很程序员自学网

<tfoot draggable='sEl'></tfoot>

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

深入作用域链与闭包

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

试想,我们如果在一个内部的函数使用了外部的变量,是通过 [[outerEnv]] 串起来的 词法环境 ( 各类环境记录 ),即最终在浏览器上的实现,作用域链 [[Scope]] 。

而闭包的触发,是需要在一个 独立的空间 中管理从外部获得的变量。而这个外部变量的获取与 绑定 ,则是需要通过作用域链。

所以理解了作用域链的形成原理,才能更好的深入理解闭包。

作用域链

上节的例子中对于函数中变量记录的阐释并不完备,只是简单的将 VariableEnvironemnt.[[outerEnv]]  指向了外部。仔细思考的同学可能会发现,JavaScript里面万物皆对象,函数这个对象满天飞,如果每次都要解析全局词法来获取某个函数的外部环境,是不是很浪费性能呢?

[[Environment]]

所以其实在函数被声明的时候,就被加上了一个 内部属性 [[Environment]] ,根据规范定义,它也是一个 环境记录 ,其 [[outerEnv]]  指向 声明 函数的词法环境。

10.2 ECMAScript Function Objects)   Internal Slots of ECMAScript Function Objects

Internal Slot Type Description [[Environment]] an  Environment Record The  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作用域链与闭包内容请搜索以前的文章或继续浏览下面的相关文章希望大家以后多多支持!

查看更多关于JavaScript深入理解作用域链与闭包详情的详细内容...

  阅读:35次