你说我坑你,不是我内心复杂,只是你不够了解我 —— JS

JS Execution Context — JS 执行上下文_JS Execution Context

张北草原天路 2017.6

说明

本文根据多篇中外博客文章,加上个人理解整合而成。

因为曾尝试翻译过这些文章,发现并不是很好理解,所以希望换个自己的方式去表达原文的部分观点。如果表达的不准确,还望谅解。

本文写于 15 年,那个时候对 JS 还不是很了解,有计划从编译的角度再写一次。

名词约定
  • Execution Context(EC) 执行上下文
  • Executable Code 可执行代码
  • Execution Context Stack(ECS) 执行上下文栈
  • Variable Objec(VO) 变量对象
  • Activation Object(AO) 活动对象
  • Scope 作用域
  • Scope Chain 作用域链
  • Arguments Object 参数对象
  • Global Code 全局代码
  • Function Code 函数代码
  • Eval Code
Execution Context

每次当解释器转到不同的可执行代码的时候,就会进入一个执行上下文 EC。可以简单的理解执行上下文就是代码的执行环境或者作用域。EC 是个抽象的概念,ECMA-262 使用 EC 和Executable Code 区分。

Executable Code

可以简单的理解:可执行代码就是 JS 中合法的代码,可以被 JS 解释器执行的代码。

可执行代码的分类:

  1. Global code 全局代码

可以理解为是 JS 解释器为 JS 程序提供的默认全局代码,例如 function Object(), eval() 这些内建的函数代码,window 等全局对象等等。(注意:只包含函数定义代码,但是不包括函数体中的代码,请看 Function code 做的解释)。

  1. Eval code

使用 eval 函数执行的代码。

可执行代码的概念与抽象的执行上下文的概念是相关的。在某些时刻,可执行代码与执行上下文是等价的。

在执行上下文提到,当程序执行转移到不同的可执行代码的时候,就会根据当前的可执行代码的类型新建一个对应的执行环境。

根据可执行代码的类型,我自己也给执行上下文分类:

  1. Global Execution Context 全局执行上下文 代码的默认运行环境,程序代码一旦被载入,最先进入的执行环境。只有一个全局执行上下文。
  2. Function Execution Context 函数执行上下文 每当调用一个函数,也就是执行函数体中的代码会新建一个函数执行上下文环境。
  3. Eval Execution Context Eval 执行上下文

当使用 eval 函数执行代码的时候,会新建一个 eval 执行上下文。

为了方便理解,来看一张图:

JS Execution Context — JS 执行上下文_JS Execution Context_02

此图表示一个完整的JS程序。

一共用 4 个执行上下文。紫色的代表全局的上下文;绿色代表 person 函数内的上下文;蓝色以及橙色代表 person 函数内的另外两个函数的上下文。

只存在一个全局的上下文,该上下文能被任何其它的上下文所访问到。也就是说,我们可以在 person 的上下文中访问到全局上下 文中的 sayHello 变量,当然在函 firstName 或者 lastName 中同样可以访问到该变量。

函数上下文的个数是没有任何限制的,每到调用执行一个函数时,解释器就会自动新建出一个函数上下文,换句话说,就是新建一个局部作用域,可以在该局部作用域中声明私有变量等,在外部的上下文中是无法直接访问到该局部作用域内的元素的。

在上述例子的,内部的函数可以访问到外部上下文中的声明的变量,反之则行不通。那么,这到底是什么原因呢?解释器内部是如何处理的呢?请往下看。

Execution Context Stack

一系列的上下文组成了上下文栈。这个栈和数据结构中的栈类似——先进后出(如果学过操作系统线程和栈帧这些概念,那理解起来非常容易——推荐《深入理解操作系统》)。 JS 程序只有一个线程,这意味着 JS 程序同一时间只能做一件事情(关于异步编程,以后再细细道来),知道这个很重要。

把执行上下文栈可视化大概是下图这个样子:

JS Execution Context — JS 执行上下文_JS Execution Context_03

栈顶是当前活动的执行上下文,也就是程序正在栈顶那个执行环境运行。栈底是全局执行上下文,因为程序一运行立即入栈的就是全局执行上下文,我们写的 JS 代码都是在全局执行上下文环境运行的。

通过出栈和入栈,切换当前程序代码的执行环境。

类似于原型链,上下文也有父执行上下文,子执行上下文。子执行上下文可以访问父执行上下文,但父执行上下文不能访问子执行上下文。上下文之间使用 scope 链接起来,我们把它称为 Scope Chain 作用域链。

举个栗子:

(function foo(i) {
if (i === 3) {
return;
} else {
foo(++i);
}
}(0));复制代码

上述 foo 被声明后,通过 () 运算符强制直接运行了。函数代码就是调用了其自身 3 次,每次是局部变量 i 增加 1。每次 foo 函数被自身调用时,就会有一个新的执行上下文被创建。每当一个上下文执行完毕,该上下文就被弹出堆栈,回到上一个上下文,直到再次回到全局上下文。JS Execution Context — JS 执行上下文_JS Execution Context_04

对2和3做个总结:

1.单线程
2.同步执行
3.只有一个全局执行上下文
4.无限制个函数执行上下文复制代码
Execution Context深入

Execution Context以函数上下文为例

每次函数调用都会生成一个新的函数执行上下文。这个生成过程可以分解为两个步骤:

  1. Creation Stage 创建阶段(函数被调用,但函数执行之前)
A.创建 Scope Chain 作用域链
B.创建变量,函数和参数
C.确定 this 的值复制代码
  1. Activation / Code Execution Stage 代码执行阶段
A.解释器执行代码,变量赋值。
可以把函数执行上下文想象成一个拥有三个属性的对象:
executionContextObj = {
scopeChain: { / variableObject + all parent execution context's variableObject / },
variableObject: { / function arguments / parameters, inner variable and function declarations / },
this: {}
}复制代码

函数执行上下文创建详解

(1)找到调用函数的入口
(2)在执行函数代码之前,创建一个执行上下文
(3)进入 creation stage 上下文创建阶段
1. 初始化 Scope Chain 作用域链
2. 创建 variable object 变量对象
2.1 创建 arguments object 对象,使用调用函数传入的实参赋值。2.2 从上往下扫描函数体代码中的函数声明;
A.对于每找到的一个函数声明,在 VO 中创建创建一个使用函数名为名的属性,并赋值(这个阶段,函数定义就被载入内存,所以函数声明在这个阶段可以赋值。注意区别函数声明和函数表达式的不同)
B.如果函数名已经存在了,那就会发生覆盖。也就是,如果有重名函数,后面的会覆盖前面的。
2.3 从上到下扫描函数体代码中的变量声明
A.每找到的一个变量声明,以变量名为属性名在 VO 创建一个属性,并赋值 undefined。
B.如果在 VO 中发生重名,解释器会跳过去,接着扫描。
3. 确定 this 的值。(4)Activation / Code Execution 代码执行阶段
在创建的上下文中从上到下逐行执行函数体代码,并给变量赋值。复制代码

举个栗子:

function foo(i) {
var a = 'hello';
var b = function privateB() { };
function c() { }
}foo(22);复制代码

当执行 foo(22) 的时候,creation state 阶段的执行上下文大概是这个样子:

fooExecutionContext = {
scopeChain: { ... },
variableObject: {
arguments: { 0: 22, length: 1 },
i: 22, c: pointer to function c(),
a: undefined,
b: undefined
},
this: { ... }
}复制代码

当函数执行完成,execution stage 大概是这样:

fooExecutionContext = {
scopeChain: { ... },
variableObject: {
arguments: { 0: 22, length: 1 },
i: 22,
c: pointer to function c(),
a: 'hello',
b: pointer to function privateB()
},
this: { ... }
}复制代码
总结 —— 函数提升
(function() {
console.log(typeof foo);
// function pointer
console.log(typeof bar);
// undefined
var foo = 'hello',
bar = function() { return 'world'; };
function foo() {
return 'hello';
}
}());复制代码
  1. 为什么可以在声明 foo 之前可以使用 foo?

在 execution stage 阶段之前的 creation stage 阶段,解释器已经在 VO 创建了所有的变量,所以在代码执行的时候,foo 已经有值了。

  1. foo 被声明了两次,为什么 foo 的值是 function,而不是 undefined 或者 string?

尽管 foo 被声明了两次,但是在 creation stage 阶段,函数声明优先于变量声明被创建。并且,变量声明不会覆盖函数声明在 VO 的属性。 需要注意的是 console.log(typeof foo) 是 undefined.但是执行 var foo = 'hello' 后,foo 的值就是 ’hello’。

  1. 为什么 bar 是 undefined

bar 是一个变量,它的值是一个函数表达式。在 creation stage 阶段,只是在 VO 中创建了 bar 这个属性,并没有赋值。

思考
  1. 什么是执行上下文,什么是执行上下文栈?
  2. 执行上下文的分类?
  3. 执行上下文被创建的过程?