深入Js作用域:执行上下文

闲言

最近在看《高性能Js》,里边提到了作用域相关的知识点,回头看了之前我写过关于作用域和闭包的博客,发现写得过于简洁。在翻阅了《Js权威指南》和《你不知道的Js》等相关文章后,有了一些更深刻的感悟,并尽可能详细、清晰地描述相关概念。

执行上下文

Execution Contexts 简称 EC。
每次当控制器转到 ECMAScript 可执行代码(Executable Code)的时候,会创建并进入一个执行上下文中。每个 EC 都包含三个重要属性:

变量对象(Variable Object,VO)
Scope(作用域链,Scope Chain)
this

注:Executable Code 包含了全局代码(不包括任何函数体内的代码)、函数代码(不包括内部函数代码)、Eval 代码

EC 类型

全局执行上下文:Global Context,它是最外围的一个 EC。当执行全局代码时,会创建全局执行上下文。

函数执行上下文:Function Context,每个函数都有自己的 EC,当执行函数代码时,会创建函数执行上下文。

这些 EC 会通过执行上下文栈(Execution context stack,ECS)管理,ECS 底部永远都是全局上下文,而顶部就是当前(活动的)EC。

EC 生命周期

1.创建阶段
在执行 Executable Code 时 EC 被创建。在这个阶段中,EC 会分别创建变量对象 VO,建立作用域链,以及确定 this 指向。

2.EC 中代码执行阶段
创建完成之后,就会开始执行代码,这个时候,会完成 V0 中变量赋值,函数引用,以及执行其他代码。

3.EC 中代码执行完毕
对应 EC 被销毁,保存在其中的变量和函数定义也随之销毁。
注:全局 EC 直到应用程序退出(如关闭网页或浏览器)时才被销毁。

变量对象的修改变化与前两个阶段紧密相关,我们会在变量对象那节进行详细介绍。

过程模拟

声明:此处仅对 EC 进行模拟,EC 内部变化会在相关概念介绍完时,进行一次整体模拟。

我们直接通过伪代码来模拟执行上下文栈:

首先,ESC我们这样表示:

ECStack = [];

Js 解析时,首先执行全局代码

创建 globalContext;

将 globalContext 压入 ECS: ECStack.push(globalContext);

ECStack = [
    globalContext
];

接着,遇到如下代码:

function fun3() {
    console.log('fun3')
}

function fun2() {
    fun3();
}

function fun1() {
    fun2();
}

fun1();

当执行一个函数的时候,就会创建一个 EC,并且压入 ECS,当函数执行完毕的时候,就会将函数的 EC 从 ECS 中弹出。过程如下:

执行fun1 ==> ECStack.push(<fun1> functionContext);

执行fun2 ==> ECStack.push(<fun2> functionContext);

执行fun3 ==> ECStack.push(<fun3> functionContext);

fun3执行完毕 ==> ECStack.pop();

fun2执行完毕 ==> ECStack.pop();

fun1执行完毕 ==> ECStack.pop();

相关代码执行完毕后,ECStack 只会保持着 globalContext,一直到整个应用程序结束。