JavaScript高级深入浅出:剖析 JS 引擎解析执行变量与函数的过程

380 阅读3分钟

介绍

本文是 JavaScript 高级深入浅出系列的第二篇,本文将深入剖析 JS 引擎对于变量与函数的解析与执行。

正文

1. 对于变量

第一篇中我们已经了解到,在解析 JS 代码时,内部的执行上下文栈 ECS,为了执行全局的代码块,会在内部创建一个全局执行栈 GEC,GEC 在解析阶段会创建一个全局对象:GO,并注入一些全局对象和变量 window,指向 GO 自己。

在 JS 引擎解析 JS 代码时(还未执行代码前),会将全局的所有变量注入到 GO 中,但不会进行赋值,所以值是 undefined

V8 对于变量的解析

开始执行代码阶段,从上到下依次执行,将bar这个字符串最终赋值给 GO 中的foo,所以:

console.log(foo)
var foo = 'bar'

// result: undefined

2. 对于函数

var name = 'alex'
foo()
function foo (num) {
    var n1 = 10
    var n2 = 20
    console.log("bar")
}

2.1 编译阶段

此时,GO 在编译阶段中应该长这样:

var GlobalObject = {
    Number: 'xxx',
    window: GlobalObject,
    name: undefined,
    foo: 0x11  // 函数体的内存地址
}

在编译阶段遇到函数,会开辟一块储存该函数信息的内存空间,其中包含以下信息:

  • 父级作用域 parent scope, foo 函数的父级作用域其实就是全局作用域,也就是 VO 指向 GO
  • 函数执行体

存储函数.png

2.2 执行阶段

编译结束后,将开始从上到下,从左到右依次执行。

第一行:

var name = 'alex',将alex的值放到vo也就是goname变量中

第二行:

foo(),遇到 () 表示为函数的调用,那么就会在全局对象 GO 中找到foo,进而找到存储函数的内存地址。

  • 开始解析函数,会在 ECStack 中创建一块函数执行上下文(Funtional Execution Context, FEC)。
  • 在 FEC 中也有 VO,和 GEC 的 VO 不同的是,这里的 VO 指向的是 AO(Activation Object)。
  • AO 中,储存的就是函数执行体的数据,在解析阶段,会把所有的变量都放在 AO 中,但是并不赋值
foo(0)
function foo (num) {
    var n1 = 10
    var n2 = 20
    console.log("bar")
}

// 解析阶段 ↓ 形参和声明的变量放入 AO 中,单并不赋值
var AO = {
    num: undefined,
    n1: undefined,
    n2: undefined
}
  • 在执行函数内代码阶段,传入的参数将修改 AO 中 num 的值

  • n1,n2 将被赋初值 10 和 20

  • console.log,先在本作用域(也就是函数内)找是否有console,没有就去父级作用域(这里是 GO),找到了 console,调用 log 方法,最终打印出foo

解析函数阶段

函数体内的所有代码执行完毕后,当前函数所属的 FEC 将弹出 ECStack,如果 AO 没有被调用,那么这个 AO 也会被销毁掉。如果后续再次调用foo函数,那么就会再次重复此流程,创建 FEC -> 创建 AO -> 执行 -> 销毁

总结

本文中,你学习到两个知识点:

1. 巩固解析与执行变量的过程

再次巩固第一篇中解析变量的过程,JS 源码 -> 解析阶段 -> JS 引擎内部 ECStack 创建全局执行上下文 GEC -> GEC 创建全局变量 GO ,内部的 VO 其实指向的就是 GO -> 执行代码 赋初值

2. 深入了解析与执行函数的过程

在执行全局代码时遇到了(),即代表调用函数,开始全量解析函数。

解析函数阶段:GEC 创建函数执行栈 FEC -> FEC 创建 AO ,内部的 VO 指向 AO -> AO 中初始化函数体内的变量 -> 执行代码 -> FEC 弹出 GEC , AO 销毁