Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JavaScript 脚本编译与执行过程简述 #266

Open
toFrankie opened this issue Feb 26, 2023 · 0 comments
Open

JavaScript 脚本编译与执行过程简述 #266

toFrankie opened this issue Feb 26, 2023 · 0 comments
Labels
2021 2021 年撰写 JS 与 JavaScript、ECMAScript 相关的文章

Comments

@toFrankie
Copy link
Owner

toFrankie commented Feb 26, 2023

配图源自 Freepik

由于上一篇文章对作用域、执行上下文等写得过于详细,而且参杂了很多内容,看起来并不好理解,因此就简化一下。

在网页里,JavaScript 脚本是插入在 <script> 标签内的(内联或外部引入)。

<script>
  // some JavaScript code...
</script>

<script>
  // some JavaScript code...
</script>

<script src="xxx"></script>

一般情况下,脚本是按顺序一块一块地执行的(一个 <script> 标签为一块)。只有执行完一块,才会执行下一块的。如果当前块有语法错误或其他错误导致当前块终止执行,不会影响下一块的执行。

对于外部引入的 JavaScript 脚本,若设置 <script> 标签的 deferasync 属性会影响脚本的加载顺序,不会按着编写顺序执行。而这些属性对于内联脚本是没有作用的。

要让 JavaScript 代码运行起来,包括编译阶段执行阶段。每一块脚本的运行都包含这两个过程。请注意,不是将“所有块”编译完再执行的。

编译阶段:
  词法分析:将源码分解成很多个有意义的 token
  语法分析:通篇检查当前块的语法是否有误,若无,将 tokens 转换为 AST
  代码生成:将 AST 转换为 JS 引擎可执行代码

执行阶段:
  创建执行上下文
  初始化执行上下文
  代码执行

JS 引擎的优化工作是在编译阶段完成的,例如 V8 引擎。若编译阶段检查出语法有误,将会终止当前块的后续阶段。

待编译阶段完成后,接着进入执行阶段。而执行阶段可以分为两个步骤:

  1. 进入执行上下文
  2. 代码执行

进入执行上下文步骤,可以理解为,真正一行一行执行代码的前期准备工作:

  1. JS 引擎创建一个叫做:执行上下文栈(Execution Context Stack,ECS)的东西。顾名思义,就是用了维护执行上下文的。

  2. 最先进入的肯定是全局代码,这个执行上下文被称为:全局上下文。首先插入 ECS,因此 ECS 栈底永远是全局上下文。后面调用函数时,会进入函数上下文,该上下文也会插入 ECS。

  3. 每进入一个执行上下文,都会做一些初始化工作。

待准备工作完成后,就会进入代码执行步骤,即一行一行地去执行代码。

大家可能疑惑的地方,应该是“前期准备工作”的具体过程是怎样的?

执行上下文栈

进入全局代码,ECS 插入 GlobalContext,后面每调用一个函数就往 ECS 栈顶插入 FunctionalContext,函数执行完 FunctionalContext 又会从栈顶删除......周而复始的过程。ESC 栈底永远保留着 GlobalContext。直到页面销毁,它才会被删除,ESC 的使命也就结束了,被 JS 引擎清空。

ECS = [
  FunctionalContext // 栈顶
  ...
  FunctionalContext
  GlobalContext  // 栈底
]

前面提到上下文:GlobalContext(全局上下文)、FunctionalContext(函数上下文)。

执行上下文

其实 JavaScript 中有三种执行上下文:

全局上下文:
  全局下都是属于全局上下文。

函数上下文:
  顾名思义,就是 JavaScript 函数内的执行上下文

eval 上下文:
  就是 eval('some code...'),尽可能不要用,会影响性能、欺骗词法作用域

每个执行上下文,可以看作是一个 JavaScript 对象,它有三个重要属性:

Variable Object: 
  即变量对象,简称 VO。我们声明的变量、函数就是放在 VO 中的。

Scope Chain: 
  即作用域链,简称 Scope。它包含了当前上下文 VO,以及各父级的 VO。查找变量就是根据这链条去查询的。

This: 
  即 this 对象,这是一个很灵活的对象,跟函数调用相关。
}

执行上下文的初始化工作,就是确定这些属性的一些值。这个“初始化”的过程也常被称为“预编译”,网上很多文章都有这个说法。

举例

上面的执行阶段的过程,举例会更好地说明。

var a = 'global'
function foo() {
  var a = 'local'
  function bar() {
    console.log(a)
  }
  bar()
}
foo() // "local"

当编译过程之后,进入执行阶段。JS 创建一个执行上下文栈 ECStack,总是先进入全局上下文,全局上下文被插入到执行上下文栈:

ECStack = [ GlobalContext ]

接着对 GlobalContext 进行初始化:

GlobalContext = {
  VO: window, 
  Scope: [ GlobalContext.VO ],
  this: GlobalContext.VO
}
// 注意,不同宿主环境顶层对象是不一样的。
// 浏览器下为 window 对象,Node 中为 global 对象。
// 本文以浏览器为例,因此直接写了 window 对象。

初始化的同时 foo 函数也被创建,会保存当前上下文的作用域链到函数内部的 [[scope]] 属性中。该属性可以通过 console.dir(foo) 查看。

foo.[[scope]] = [ GlobalContext.VO ]

当全局上下文初始化完成之后,开始执行全局代码,当进行到 foo() 时,JS 引擎又会创建 foo 函数执行上下文,并插入 ECStack 栈顶。

ECStack = [ GlobalContext, FunctionalContext<foo> ]

跟着,会进入 foo 函数上下文,该上下文也会进行初始化工作:

  1. 复制 foo 函数 [[scope]] 属性创建作用域链。
  2. arguments 创建活动对象 AO。
  3. 初始化活动对象,即 AO 按顺序加入:形参、函数声明、变量声明。若存在同名情况,函数声明会覆盖形参,变量声明会被忽略。
  4. 将 AO 对象加入 foo 作用域链顶端。
FunctionalContext<foo> = {
  AO: {
    arguments: {
      length: 0
    },
    a: undefined,
    bar: ƒ bar()
  },
  Scope: [ GlobalContext.VO, AO ],
  this: undefined // this 是在函数执行的时候才确定下来的,foo 函数的 this 的值跟作用域链没有关系
}

初始化完成,开始执行 foo 函数体代码,AO 对象也会更新。当执行到 bar() 函数时,又将会进入 bar 函数上下文,并进行初始化工作:

ECStack = [ GlobalContext, FunctionalContext<foo>, FunctionalContext<bar> ]
FunctionalContext<bar> = {
  AO: {
    arguments: {
      length: 0
    }
  },
  Scope: [ GlobalContext.VO, FunctionalContext<foo>.VO, AO ],
  this: undefined
}

初始化完成,开始执行 bar 函数代码,在 console.log(a) 中,会查找 a 变量。

按作用域链顺序查找,先从当前上下文的 AO 中查找,若查找不到再往上一层 AO 中查找...直到全局上下文的 VO 中查找,若再查找不到就会抛出引用错误(ReferenceError)。

AO -> FunctionContext<foo>.AO -> GlobalContext.VO

很明显,a 变量将会在 FunctionContext<foo>.AO 中查找到,其值为 "local",因此打印结果就是 "local"

bar() 执行完毕,bar 函数上下文会被从栈顶删除。

ECStack.pop(FunctionalContext<bar>)

又将执行权交还给 foo 函数上下文,它也将会执行完毕,也会从栈顶移除。

ECStack.pop(FunctionalContext<foo>)

然后又交还给全局上下文

ECStack = [ GlobalContext ]

至此,这个程序执行完毕。当页面销毁时,ECStack 才会被清空。

@toFrankie toFrankie added the JS 与 JavaScript、ECMAScript 相关的文章 label Feb 26, 2023
@toFrankie toFrankie added the 2021 2021 年撰写 label Apr 26, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
2021 2021 年撰写 JS 与 JavaScript、ECMAScript 相关的文章
Projects
None yet
Development

No branches or pull requests

1 participant