JS引擎

当JavaScript代码被下载下来后,是什么负责解析的呢?
浏览器内核包含两部分组成,一部分用于解析HTML、布局、渲染等相关的工作;另一部分就是负责解析和执行JavaScript代码。
以webkit为例:
执行过程及作用域,闭包 - 图1

V8引擎

V8引擎是由Google开源以C++编写的高性能JavaScriptWebAssemby引擎,它用于ChromeNode.js等中。
执行过程及作用域,闭包 - 图2

Parse模块

Parse模块会将JavaScript代码转换成AST(抽象语法树),这是因为解释器并不直接认识JavaScript代码;如果函数没有被调用,那么是不会被转换成AST的。
Parse的V8官方文档:https://v8.dev/blog/scanner

Ignition模块

Ignition是一个解释器,会将AST转换成ByteCode(字节码)同时会收集TurboFan优化所需要的信息(比如函数参数的类型信息,有了类型才能进行真实的运算);如果函数只调用一次,Ignition会解释执行ByteCode。
Ignition的V8官方文档:https://v8.dev/blog/ignition-interpreter

TurboFan模块

TurboFan是一个编译器,可以将字节码编译为CPU可以直接执行的机器码;如果一个函数被多次调用,那么就会被标记为热点函数,那么就会经过TurboFan转换成优化的机器码,提高代码的执行性能;但是,机器码实际上也会被还原为ByteCode,这是因为如果后续执行函数的过程中,类型发生了变化(比如sum函数原来执 行的是number类型,后来执行变成了string类型),之前优化的机器码并不能正确的处理运算,就会逆向的转换成字节码;
TurboFan的V8官方文档:https://v8.dev/blog/turbofan-jit
V8引擎官方解析过程图

JavaScript执行过程

初始化全局对象

JavaScript引擎在执行代码之前,会在堆内存中创建一个全局对象:Global Object(GO)。该对象所有的作用域。里面会包含Date、Array、String、Number、setTimeout、SetInterval等。(浏览器上还有一个window指向自己)。
GO在内存中的具体表现:
执行过程及作用域,闭包 - 图4

函数执行上下文

执行上下文栈

JavaScript引擎内部会有一个执行上下文栈(Execution Context Stack,ECS)它用于执行代码的调用栈。

全局执行上下文

当JS代码运行时,会创建一个全局执行上下文(Global Excution Context,GEC),它会被优先压入到ECS中。
GEC被压入到ECS中主要包含两部分内容:

  • 在代码执行前,也就是parse转成AST的过程中,会将全局定义的变量、函数等放加入Global Object中,但是不会赋值(这个过程也就是变量作用域的提升)。

    1. console.log(msg) // 输出:undefined
    2. var msg = "Hello World"
  • 在代码执行中,对变量赋值,或者执行其他的函数等操作。

    函数执行上下文

    在执行过程中,遇到一个函数的调用时,就会根据函数体创建一个函数执行上下文(Functional Execution Context,FEC),并且压入执行上下文栈中。当一个函数执行完时,就会将该FEC从ECS中弹出。
    函数执行上下文包含三部分内容:

  • 在解析函数时成AST树结构时,会创建一个Activation Object(AO)

  • 作用域链,由Variable Object(VO)(在函数中就是AO对象)和父级的VO组成。当查找一个变量或引用时,会层层往上找,直到找到最近的
  • this的绑定的值 :::info AO中包含形参、arguments、函数定义和指向函数对象、定义的变量。 :::

    案例

    ```javascript var msg = “hello world”

function foo() { var name = “pokeyoo” console.log(“foo”) }

foo()

执行前的内存表现<br />![](https://cdn.nlark.com/yuque/0/2022/jpeg/21450449/1656767979624-24edd5a5-8643-4776-9e9b-889b44d1fb98.jpeg)<br />执行过程中的内存表现:<br />![](https://cdn.nlark.com/yuque/0/2022/jpeg/21450449/1656767975559-d6bc6339-063b-4262-a3ef-9068ecf1f788.jpeg)
:::info
在函数VO对象初始化过程中,如果遇到函数,不会像变量一样被赋值为undefined,而会被直接处理。
:::
<a name="Arn2M"></a>
### 作用域和作用域链
当进入到一个执行上下文时,执行上下文也会关联一个作用域链(Scope Chain),作用域链是一个对象列表,用于变量标识符的求值;当进入一个执行上下文时,这个作用域链被创建,并且根据代码类型,添加一系列的对象。
:::warning
作用域及作用域链是在函数定义时确定的,跟调用位置没有关系。
:::
```javascript
function foo() {
  var name = "pokeyoo"
  function bar() {
    console.log(name)
  }
  bar()
}

foo()

执行过程中的内存表现:
执行过程及作用域,闭包 - 图5

面试题

var n = 100

function foo() {
  n = 200
}

foo()
console.log(n)
function foo() {
  console.log(n)
  var n = 200
  console.log(n)
}

var n = 100
foo()
var n = 100

function foo1() {
  console.log(n)
}

function foo2() {
  var n = 200
  console.log(n)
  foo1()
}

foo2()
console.log(n)
var a = 100

function foo() {
  console.log(a)
  return;
  var a = 200
}

foo()
function foo() {
  var a = b = 100
}

foo()

console.log(a)
console.log(b)

VO和变量环境、记录

上面所述是早起ECMA规范

Every execution context has associated with it a variable object. Variables and functions declared in the source text are added as properties of thevariable object. For function code, parameters are added as properties of the variable object. 每一个执行上下文会被关联到一个变量环境(variable object,Vo),在源代码中的变量和函数声明以及参数会被作为属性添加到VO中。

最新的ECMA规范:

Every execution context has an associated VariableEnvironment. Variables and functions declared in ECMAScript code evaluated in an executioncontext are added as bindings in that VariableEnvironment’s Environment Record.For function code,parameters are also added as bindings to thatEnvironment Record. 每一个执行上下文会关联到一个变量环境(VariableEnvironment)中,在执行代码中变量和函数的声明会作为环境记录(Environment Record)添加到变量环境中。对于函数来说,参数也会被作为环境记录添加到变量环境中。

不管什么样的编程语言,在代码的执行过程中都是需要给它分配内存的,不同的是某些编程语言需要我们自己手动的管理内存,某些编程语言会可以自动帮助我们管理内存。

闭包

闭包的定义

维基百科对闭包的定义

闭包(英语:Closure),又称词法闭包(Lexical Closure)或函数闭包(function closures)。是在支持头等函数的编程语言中,实现词法绑定的一种技术; 闭包在实现上是一个结构体,它存储了一个函数和一个关联的环境(相当于一个符号查找表);闭包跟函数最大的区别在于,当捕捉闭包的时候,它的自由变量会在捕捉时被确定,这样即使脱离了捕捉时的上下文,它也能照常运行;


MDN对闭包的定义

一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包(closure)。 也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域; 在 JavaScript 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来;


总结:

  • 一个普通的函数function,如果它可以访问外层作用于的自由变量,那么这个函数就是一个闭包。
  • 从广义的角度来说:JavaScript中的函数都是闭包。
  • 从狭义的角度来说:JavaScript中一个函数,如果访问了外层作用于的变量,那么它是一个闭包。

    闭包的形成过程

    ```javascript function makeAdder(base) { return function (num) { return base + num; } }

const add10 = makeAdder(10) console.log(add10(5)); // 15

当makeAdder函数执行完毕,正常情况下我们的AO对象会被释放;但是在返回的函数中,有作用域引用了这个AO对象的base,所以它不会被释放。
<a name="dGXuv"></a>
### 闭包的内存泄漏
> 在上面的案例中,如果后续我们不再使用add10函数了,那么该函数对象应该要被销毁掉,并且其引用着的父
> 作用域AO也应该被销毁掉;
> 但是目前因为在全局作用域下add10变量对0xb00的函数对象有引用,而0xb00的作用域中AO(0x200)有引
> 用,所以最终会造成这些内存都是无法被释放的;
> 所以我们经常说的闭包会造成内存泄露,其实就是刚才的引用链中的所有对象都是无法释放的;

<a name="icHcg"></a>
#### 解决闭包的内存泄漏
```javascript
add10 = null

根据GC机制,将add10引用赋值为null,就不会有引用指向makeAdder创建出来的函数,从而会将其销毁。

闭包中未使用外层的属性是否被销毁

function makeAdder(base) {
  const msg = "hello"
    return function (num) {
      return base + num;
  }
}

:::warning 这段代码中,base和msg都属于父级作用域中的AO,base不会被销毁,但msg会被销毁。 :::

内存管理

内存管理的步骤:

  1. 申请:分配你需要的内存
  2. 使用:使用分配的内存存放一些东西
  3. 释放:但不需要使用时,对其进行释放

    JavaScript的内存管理

  • 对于基本数据类型在执行时,会直接在空间中进行分配。
  • 对于引用数据类型在执行时,会在堆空间开辟一块空间,并将该空间的引用地址返回给引用它的变量。

    JavaScript的垃圾回收机制

    垃圾回收的英文是Garbage Collection(GC)。对于哪些不再使用的变量、对象等,都可以称之为垃圾,它需要被回收,以腾出更多的内存空间。

    GC算法——引用计数

    当一个对象有一个引用指向它时,那个这个对象的引用就+1。当一个对象的引用为0时,这个对象就可以被销毁了。
    这个算法有一个很大的弊端,就是不会清除循环引用的变量。
    image.png

    GC算法——标记清除

    这个算法是设置一个根对象(root object),垃圾回收器会定期从这个根开始,找所有从根开始有引用到的对象,对于哪些没有引用到的对象,就认为是不可用的对象。这样解决了循环引用的问题。
    image.png
    JavaScript采用的GC算法就是标记清除, 类似于V8引擎为了进行更好的优化,它在算法的实现细节上也会结合 一些其他的算法。