和朋友们聊聊JS的闭包以及V8对闭包的优化

794 阅读5分钟

JS中的闭包

在聊JS中的闭包之前,需要先和朋友们铺垫几个概念,

1.执行上下文

2.执行上下文堆栈

3.词法环境

(这里我们默认大家都已经熟悉了这三个基础知识点,但是后续也会补充一些要点,确保不熟悉的朋友们可以快速熟悉这三个基础点,但不会对这些知识点有过于详细的解释)

执行上下文

四种常见的可成为执行上下文的可执行代码环境:

1.全局环境

2.块级环境

3.函数环境

4.eval环境(不在讨论范围内)

执行上下文堆栈

javascript 是单线程的,也就是在同一时间只执行一个事件。那其他与之关联的执行上下文。会被一个叫做 执行上下文堆栈 的特殊数据结构保存。当代码执行时,它默认会进入 全局上下文,如果在全局上下文中,调用一个函数,就会创建一个函数执行上下文,并将该上下文放到执行上下文堆栈的顶部。

词法环境

在 JavaScript 中,每个运行的函数、代码块 以及整个脚本,都有一个被称为 词法环境(Lexical Environment) 的内部(隐藏)的关联对象。

词法环境对象组成

  • 环境记录(Environment Record) :一个把所有局部变量作为其属性(包括一些额外信息,比如 变量、函数、参数 )的对象。
  • 外部词法环境(outer lexical environment) 的引用 : 通常是嵌套当前代码之外代码的词法环境。

所有的函数在“诞生”时都会记住创建它们的词法环境。从技术上讲,这里没有什么魔法:所有函数都有名为 [[Environment]] 的隐藏属性,该属性保存了对创建该函数的词法环境的引用。

现在基础知识点介绍完了,我们先从执行上下文进栈出栈入手

代码执行过程

执行上下文(Execution Context)有三个组成部分:

  • LexicalEnvironment:是一个词法环境(Lexical Environment)。
  • VariableEnvironment:也是一个词法环境(Lexical Environment),一般和LexicalEnvironment指向同一个词法环境。
  • ThisBinding:这个就是代码里常用的this。

JS引擎是按照可执行代码来执行代码的,每次执行步骤如下:

  • 1:创建一个新的执行上下文(Execution Context)
  • 2:创建一个新的词法环境(Lexical Environment)
  • 3:把LexicalEnvironmentVariableEnvironment指向新创建的词法环境
  • 4:把这个执行上下文压入执行栈并成为正在运行的执行上下文
  • 5:执行代码
  • 6:执行结束后,把这个执行上下文弹出执行栈

先给朋友们解释一下为什么执行上下文中要有两个词法环境: 变量环境组件(VariableEnvironment) 是用来登记var function变量声明,词法环境组件(LexicalEnvironment) 是用来登记let const class等变量声明。

在ES6之前都没有块级作用域,ES6之后我们可以用let const来声明块级作用域,有这两个词法环境是为了实现块级作用域的同时不影响var变量声明和函数声明,具体如下:

  • 1:首先在一个正在运行的执行上下文内,词法环境由LexicalEnvironment和VariableEnvironment构成,用来登记所有的变量声明。
  • 2:当执行到块级代码时候,会先LexicalEnvironment记录下来,记录为oldEnv。
  • 3:创建一个新的LexicalEnvironment(outer指向oldEnv),记录为newEnv,并将newEnv设置为正在执行上下文的LexicalEnvironment。
  • 4:块级代码内的let const会登记在newEnv里面,但是var声明和函数声明还是登记在原来的VariableEnvironment里。
  • 5:块级代码执行结束后,将oldEnv还原为正在执行上下文的LexicalEnvironment。

正常情况下,在执行上下文堆栈栈顶的执行上下文执行完以后,它会被弹出执行栈,它的词法环境链路也就失联了。我们知道每个函数在执行的时候都会创建一个新的执行上下文,同时也会创建它们自己的词法环境,每个函数的词法环境里有一个 [[Environment]]会保存(指向)它上一层的词法环境。 那么栈顶执行上下文执行完以后返回,但其创建的函数的 [[Environment]]里还保留着上个执行上下文的词法环境,那么在执行其创建的函数的时候,是可以访问失联的 [[Environment]]所指向的词法环境,也就是可以拿到被出栈的执行上下文中的变量。这是JS语法中的闭包

v8对闭包的优化

JS语法中的闭包是指一个函数的[[Environment]]中有创建它的执行上下文的词法环境,当该执行上下文出栈后,该子函数因为[[Environment]]指向词法环境,所以词法环境对象没有被回收,所以可以访问其中变量。但是v8对这个机制做了一定的优化,请看下面

image.png

接下来我们看一下它的[[Scopes]]中都保存了哪些变量

image.png 它的[[Scopes]]中只打包了a,b和全局环境(每个函数的[[Scopes]]都会打包全局环境)

我们可以很明显的感受到,函数有一个[[Scopes]]隐藏属性,它通过v8本身的惰性解析和预解析器机制,在创建函数的时候,就能知道函数使用到了哪些本环境中没有的变量,然后打包到[[Scopes]]上。简而言之,就是该属性上只会打包外部引用。

调用 out3 的时候,JS 引擎 会取出 [[Scopes]] 中的打包的 Closure 栈([[Scopes]]的值是一个栈结构),设置成新的作用域链, 这个新的作用域链相当于旧的作用域链的子集。

此时当执行上下文堆栈中栈顶的执行上下文出栈后,v8的这个机制可以减少内存的压力,仅将需要的变量打包给[[Scopes]]即可,被出栈的栈顶执行上下文就可以被销毁了。(其实我也没把握准是否真的被销毁,但后面的执行上下文要使用到的变量已经打包给对应函数的[[Scopes]]属性了,如不销毁,其存在也没有任何意义,欢迎各位朋友们不吝赐教