前言
JS 引擎只确保 Execution Context 描述的机制,但不确保采用什么手段去实现。
首先,Execution Context 是 ECMAScript 语言规范中的抽象概念,它旨在建立一种易于描述和实现的 ECMAScript 运行机制,JavaScript 是 ECMAScript 的一种实现,而 JavaScript 引擎,例如 V8、SpiderMonkey、Carakan 等,在实现 JavaScript 运行环境时,并不一定会采用固定方式实现 Execution Context,更不会强调哪些类、方法或函数是与 Execution Context 有关的,也许其实现有关的代码到处都是。
但了解 Execution Context 仍然是有意义的,概念可以帮助我们理解现象。
最后,Execution Context 的流行解释有两种,共同点是 JS 执行都要经历两个阶段:创建执行上下文,执行代码;不同点主要在于创建阶段,一种是偏向于 ECMAScript 中的抽象描述——Execution Context 创建词法环境组件与变量环境组件,另一种未找到来源,猜测是个人构思的实现方式,或早期 JS 引擎的实现方式——Execution Context 创建变量对象与作用域链。
二者殊途同归,方式不同,但效果相同,本文主要介绍后者——变量对象与作用域链说,参考自 JavaScript Execution Context – How JS Works Behind The Scenes
Basic
执行 JS 代码总体分为两步:
- 创建阶段;
- 执行阶段;
执行上下文(Execution Context)是 JS 得以执行的环境,简称 EC,其中包含参数、变量、函数的定义、作用域链以及 this 绑定的对象等,参数、变量的赋值将在执行代码时完成;
其中 EC 有两种:
- Global Execution Context,简称 GEC,默认创建的全局执行上下文;
- Function Execution Context,简称 FEC,为函数创建的函数执行上下文;
显然函数需要按照一定的顺序执行,FEC 也要按照同样的顺序被访问,执行上下文栈(Execution Context Stack,or Execution Stack,Call Stack)就是保证 EC 按顺序访问的栈结构,GEC 总是位于栈底;
创建阶段
创建阶段会构造执行上下文,而执行上下文以对象的形式存在,即 Execution Context Object,简称 ECO,然后主要做三件事:
- 创建变量对象(Variable Object),简称 VO;
- 创建作用域链(Scope Chain);
- 绑定 this 指针;
上述 VO、Scope Chain、this 以属性的形式存在于 ECO 上;
首先创建全局执行上下文对象,它的 VO 作为全局函数和全局属性的宿主,也称为全局对象(Global Object,GO),GO 在浏览器环境中以window
形式出现,Node.js 环境中有相同作用的global
;
创建变量对象
VO 的创建包含如下内容:
- 变量声明,初值为 undefined,已有同名声明则不再赋初值;
- 函数声明,初值为对应函数的引用(Reference of Function),值会覆盖同名声明(变量或函数);
重点,基于上述规则,会有如下现象:
- 按顺序执行代码,首行调用 func()时,变量func 值为 undefined,抛出异常;
1 | // func(); throw an error, func is undefined |
- 按顺序执行代码,首行调用 func()时,函数func 值为 func 的引用,代码正常运行;
1 | func(); // print "hello v8" |
这就是变量提升(Variable Hoisting)与函数提升(Function Hoisting)的区别与根源;
注 1:let 和 const 定义的变量,在其赋值语句出现之前访问该变量,将抛出错误,这是 ECMAScript 规定的——在 let、const 赋值之前,该变量无法访问,这段时间该变量处于暂存死区(Temporal Dead Zone);
注 2:函数参数是通过类数组 arguments 存储的,并不是同变量、函数一同保存在 VO 中;
创建作用域链
JS 中的代码受词法作用域限制,块内部可访问外部变量,反之则不行,Scope Chain 就是对词法作用域的实现,假如以单链表模拟,则每个节点代表执行上下文的 VO,当前执行上下文的 VO 作为头节点,全局执行上下文的 VO 作为尾节点,在解析某个变量时遍历 Scope Chain;
Scope Chain 导致的现象就是闭包(Closure),即当前执行上下文总是可以通过 Scope Chain 访问到父执行上下文中定义的变量;
绑定 this 指针
全局执行上下文的 this 指向 GO,函数执行上下文的 this 指向函数定义时的环境,这句话这样理解:
- 全局函数:在全局定义,this 指向 GO;
- 对象的方法:在对象中定义,this 指向该对象;
例子如下:
1 | var a = 1; |
执行阶段
进入执行阶段,将 GEC 压入 ECS,然后顺序执行代码,执行到赋值语句时就更新 GO 中的变量或函数声明,此时变量才可以取到值。
特别的,执行到函数调用时,为该函数创建 FEC,然后将其压入 ECS,并设为 active,设为 active 的执行上下文最先开始执行,如果函数体包含嵌套的函数调用,则如法炮制——创建 FEC 并入栈,直到没有嵌套的函数调用,执行完栈顶函数后,对应 FEC 出栈,再重复该过程直到所有函数调用完。