# 前言

因为和我对接的后台同学的服务器不在状态,所以我突然多了一天的摸鱼时间,突然想起还有这么个知识点,于是我决定花点时间整理下,最后发现这东西的坑比我想象的大,所以就有了这篇博客

本文同步更新于我的掘金博客

# ES3

因为一些概念已经不通用,所以把文章分成几个部分,先从 ES3 时代开始说起。

中英名词对照:

  • 执行上下文:Execution Contexts
  • 执行栈:Execution Stack
  • 变量对象:variable object
  • 激活对象:Activation Object
  • 作用域链:Scope Chain

# 执行上下文和执行栈

在 JS 代码执行前,JS 引擎会为这部分代码创建一个执行环境,这个环境叫做执行上下文 (Execution Contexts),这个环境中包含了代码运行时需要的数据,执行上下文有两种

  • 全局执行上下文:在所有的 JS 代码开始执行前,JS 引擎会先创建全局执行上下文,全局执行上下文是唯一的,并且始终处于执行栈的底部,一直到程序运行结束才会销毁
  • 函数执行上下文:函数执行上下文会在函数被调用时创建,每一个函数调用都会产生新的执行上下文,即使是重复调用
  • eval 函数执行上下文: 执行在 eval 函数内部的代码也会有它属于自己的执行上下文,但 eval 函数一般不会用,在这里不做分析

这些执行上下文会被放到一个叫 ** 执行栈 (Execution Stack)** 的栈中,下面这段代码演示了他们的关系

let a = 'Hello World!';
function first() {
  console.log('Inside first function');
  second();
  console.log('Again inside first function');
}
function second() {
  console.log('Inside second function');
}
first();
console.log('Inside Global Execution Context');

<div class="vue-app">
<img src="https://sakurablog.oss-cn-beijing.aliyuncs.com/wp-content/uploads/2020/08/image-1597064310277.png" preview="1">
</div>

当这段代码加载到浏览器中时,JS 引擎会创建一个全局执行上下文并将其推入执行栈。 当 first 函数被调用时,JS 引擎会为 first 函数创建一个新的执行上下文,并将其推入当前执行栈的顶部。

当从 first 函数中调用 second 函数时,JS 引擎将为 second 函数创建一个新的执行上下文,并将其推入当前执行栈的顶部。 当 second 函数完成时,它的执行上下文将从执行栈中弹出,代码的运行权也重新回到其下方的执行上下文,即 first 函数的执行上下文。

当 first 完成时,它的执行上下文将从执行栈中弹出,并将控制权移至全局执行上下文。

# 执行上下文的内容

执行上下文中包含四个方面的内容,分别是

  • this 的指向
  • 变量对象 (variable object), 简称 VO
  • 活动对象 (Activation Object), 简称 AO
  • 作用域链 (Scope Chain)

# this 指向

原文:There is a this value associated with every active execution context. The this value depends on the caller and the type of code being executed and is determined when control enters the execution context. The this value associated with an execution context is immutable.

每个处于活动期间的执行上下文的都和一个 this 值相关联,this 的值取决于函数调用方和正在执行的代码的类型,它会在控制权进入执行上下文时确定。 与执行上下文关联的 this 值是不可变的。

这里不展开说明 this 的规则,就放一张图吧
<div class="vue-app">
<img src="https://sakurablog.oss-cn-beijing.aliyuncs.com/wp-content/uploads/2020/08/image-1597067065755.png" preview="1">
</div>

# 变量对象

原文:Every execution context has associated with it a variable object. Variables and functions declared in the source text are added as properties of the variable object. For function code, parameters are added as properties of the variable object.

每个执行上下文都有一个变量对象。 源代码中声明的变量和函数将作为属性被添加到变量对象上。对于函数的执行上下文,参数也会被添加到变量对象中

变量对象也有两种

  • 全局变量对象 (global object) : 全局上下文中的变量对象,在浏览器环境中就是 window,也叫 GO
  • 普通变量对象:函数的执行上下文中的变量对象

在普通 VO 创建时会进行下面的操作

  • 把所有的形参和 arguments 添加到 VO 上,形参值设置为实参值
  • 把函数里所有通过 var 声明的变量添加到 VO 上,值设置为 undefined,如果 VO 中已经有该属性,直接跳过
  • 把通过函数声明的函数添加到 VO 上,如果 VO 中已经存在该属性,进行覆盖

关于操作的第二点,如果 VO 中已经有该属性,直接跳过这个的验证如下

function func(x, y) {
    console.log(x, y, z, arguments);
    var x, z;
    x = 20;
    y = 20;
    z = 20;
    console.log(x, y, z, arguments);
}
func(10,10);

打印出来的结果是这样的

<div class="vue-app">
<img src="https://sakurablog.oss-cn-beijing.aliyuncs.com/wp-content/uploads/2020/08/image-1597154360205.png" preview="1">
</div>

arguments 的值和形参有映射关系,当 x,y 被修改时,arguments 里的值也被修改了,所以 x,y 是形参的 x 和 y,如果是下面声明的 x 和 y,那 arguments 里的值不会发生变化

关于操作的第三点,函数声明会覆盖 var 声明的变量,这个验证也很简单

console.log(x);  // ƒ x() {}
var x = 10;
console.log(x); // 10
function x() {}

从打印结果可以看出,function 的声明覆盖了变量的声明

GO 没有普通 VO 创建的第一步,也就是形参设置。GO 创建时的第一步是会挂载 Math, Date 这些内置对象到自身上。

# 活动对象

原文:When control enters an execution context for function code, an object called the activation object is created and associated with the execution context.[...]
The activation object is then used as the variable object for the purposes of variable instantiation.
The activation object is purely a specification mechanism. It is impossible for an ECMAScript program to access the activation object

当控制权进入函数的执行上下文时,将创建一个称为激活对象的对象添加到执行上下文中。然后激活对象将用作变量对象,以实现变量实例化。激活对象纯粹是一种规范机制。ECMAScript 程序不能访问激活对象。

说实话这里的文档我也看不太懂,然后我去 StackOverflow 上找了一下相关的问答

然后点赞最高的回答是这个
<div class="vue-app">
<img src="https://sakurablog.oss-cn-beijing.aliyuncs.com/wp-content/uploads/2020/08/image-1597076216197.png" preview="1">
</div>

我点进原文章看了一下
<div class="vue-app">
<img src="https://sakurablog.oss-cn-beijing.aliyuncs.com/wp-content/uploads/2020/08/image-1597076561346.png" preview="1">
</div>

最后的结论是,在函数执行上下文中,激活对象(AO)会被当做(VO)使用,在全局执行上下文中,因为没有 AO,所以全局对象(GO)会被当成 VO 使用

画张图来看是这样的
VO 和 AO 在函数执行上下文中指向同一块内存空间,全局上下文中没有 AO,所以 GO 单独指向一块内存
<div class="vue-app">
<img src="https://sakurablog.oss-cn-beijing.aliyuncs.com/wp-content/uploads/2020/08/image-1597114383851.png" preview="1">
</div>

# 作用域链

原文:Every execution context has associated with it a scope chain. A scope chain is a list of objects that are searched when evaluating an Identifier. When control enters an execution context, a scope chain is created and populated with an initial set of objects, depending on the type of code.

每个执行上下文都包含一个作用域链,这个作用域链会用于变量的查找。当控制权进入执行上下文时,作用域链就会被创建。

作用域链用于查找变量,在查找变量时,会从作用域链的顶端开始查找,当查找完整个作用域链仍然找不到变量时,就会抛出异常。在函数创建时,会定义一个内部属性 [[scope]] 保存到函数上,这个属性会保存当前执行上下文的作用域链,在函数执行时,[[scope]] 属性会和当前执行上下文的 VO 一起组成当前执行上下文的作用域链

举个例子,有下面的代码

var a = 10;
function func1() {
    var b = 20;
    return function fun2() {
        console.log(b);
        var c = 30
        return function fun3 () {
            console.log(c);
            var d = 40;
        }
    }
}
var func2 = func1();
var func3 = func2();
console.log(func3);

然后在运行结束时,我们可以查看 func3 的情况

<div class="vue-app">
<img src="https://sakurablog.oss-cn-beijing.aliyuncs.com/wp-content/uploads/2020/08/image-1597124086227.png" preview="1">
</div>

如图,func3 实际上是在 func2 中被创建的,所以 func3 [[Scopes]] 保存了 func2 执行时的作用域链,在 func3 执行时,会在这个作用域链的顶端加上 func3 的 VO,由此构建 func3 的作用域链,这就解释了闭包的形成

也就是

Scope = [VO].concat([[Scope]]);

# 执行上下文的结构

执行上下文的结构类似于这种

executionContext:{
    [variable object | activation object]{
        arguments,
		variables|functions : [...]
    },
    scope chain: [VO].concat([[Scope]])
    thisValue: context object
}

# ES5

ES5 调整了执行上下文中的部分概念,去除了 AO,VO 的概念,添加了词法环境 (Lexical Environments) 和变量环境 (VariableEnvironment) 这两个新概念,当然总体思路没有太大变化,如果你看懂了上面 ES3 的部分,这份也不是什么难事。

中英名词对照

  • 词法环境:Lexical Environment
  • 变量环境:VariableEnvironment
  • 环境记录:Environment Records
  • 外部词法环境:The outer environment reference

# 执行上下文的结构

执行上下文有两个关键属性,词法环境 LexicalEnvironmen 和变量环境 VariableEnvironment

<div class="vue-app">
<img src="https://sakurablog.oss-cn-beijing.aliyuncs.com/wp-content/uploads/2020/08/image-1597129518685.png" preview="1">
</div>

用代码表示是这样的

ExecutionContext = {
  LexicalEnvironment = {...},
  VariableEnvironment = {...}
}

接下来我们来看看 LexicalEnvironment 和 VariableEnvironment 的详细内容

# 词法环境 (Lexical Environment)

原文:A Lexical Environment is a specification type [...]. A Lexical Environment consists of an Environment Record and a possibly null reference to an outer Lexical Environment. Usually a Lexical Environment is associated with some specific syntactic structure of ECMAScript code such as a FunctionDeclaration, a BlockStatement, or a Catch clause of a TryStatement and a new Lexical Environment is created each time such code is evaluated.

词法环境是一种规范类型,由环境记录 (Environment Record) 和对外部词法环境 (outer Lexical Environment) 的引用组成。通常,词汇环境与 ECMAScript 代码的一些特定语法结构相关联,比如函数声明、代码块或 Try 语句的 Catch 子句,每次执行这些代码时都会创建一个新的词汇环境。

也许你暂时看不懂上面的话,没关系,我会先介绍环境记录和外部词法环境,最后结合起来一起讲解

# 环境记录 (Environment Records)

原文 [...] Declarative Environment Records and object Environment Records. Declarative Environment Records are used to define the effect of ECMAScript language syntactic elements such as FunctionDeclarations, VariableDeclarations.[...]

环境记录用于保存词法环境中的变量和函数,你可以把它理解成 VO 和差不多的东西,但是环境记录还另外记录了当前词法环境的 this

<div class="vue-app">
<img src="https://sakurablog.oss-cn-beijing.aliyuncs.com/wp-content/uploads/2020/08/image-1597129220305.png" preview="1">
</div>

也就是说环境记录的结构大概是这样

EnvironmentRecord: {
	// 只有在函数的词法环境中才有
	arguments: [...],
	// 变量和函数
	variables|functions : [...],
	// 绑定的 this
	ThisBinding: <Global Object>,
},

# 外部词法环境的引用 (The outer environment reference)

原文:The outer environment reference is used to model the logical nesting of Lexical Environment values. The outer reference of a (inner) Lexical Environment is a reference to the Lexical Environment that logically surrounds the inner Lexical Environment. An outer Lexical Environment may, of course, have its own outer Lexical Environment. A Lexical Environment may serve as the outer environment for multiple inner Lexical Environments. For example, if a FunctionDeclaration contains two nested FunctionDeclarations then the Lexical Environments of each of the nested functions will have as their outer Lexical Environment the Lexical Environment of the current evaluation of the surrounding function.

外部环境引用为词法环境的逻辑嵌套建模。(内部) 词法环境的外部指称是指在逻辑上围绕着内部词法环境的词法环境。当然,外部词法环境可能有它自己的外部词法环境。一个词法环境可以作为多个内部词法环境的外部环境。例如,如果一个函数声明包含两个嵌套函数声明,那么每个嵌套函数的外部词法环境都是最外面函数的词法环境。

源文档讲的东西比较绕,你可以直接把它当成和作用域链类似的东西,正是因为内部词法环境有了外部词法环境的引用,内部词法环境才能访问外部词法环境里定义的变量和函数

# 词法环境的举例

介绍完了环境记录和外部词法环境的引用,我们可以继续讲词法环境了,我们用一段代码来演示这些关系

let a = 10;
if (a === 10) {
    let b = 20;
	console.log(b);
    if (b === 20) {
        let c = 30;
        console.log(c);
    }
}

执行 console.log (b) 的时候,执行上下文是这样的

ExecutionContext = {
  LexicalEnvironment = {
   // 环境记录
  	EnvironmentRecord: {
		ThisBinding: window,
		b : 20
	},
	// 外部词法环境
	outerEnvironment : {
		// 外部词法环境的环境记录
		EnvironmentRecord: {
			a : 10,
			ThisBinding: window,
		},
		outerEnvironment : null
	}
  },
  VariableEnvironment = {...}
}

在执行 console.log (c) 的时候变成了这样

ExecutionContext = {
    LexicalEnvironment: {
        // 环境记录
        EnvironmentRecord: {
            ThisBinding: window,
            c: 30
        },
        // 外部词法环境
        outerEnvironment: {
            // 外部词法环境的环境记录
            EnvironmentRecord: {
                b: 20,
                ThisBinding: window,
            },
            outerEnvironment: {
                // 外部词法环境的环境记录
                EnvironmentRecord: {
                    a: 10,
                    ThisBinding: window,
                },
                outerEnvironment: null
            }
        },
        VariableEnvironment = {...}
    }
}

显然,LexicalEnvironment 随着代码的执行(比如进入一个新的语句块时),会不断变化,环境记录也会随着变化,记录当前作用域内声明的变量,外部环境的引用也会随之变化,作为变量查找的依据。

# 变量环境 (VariableEnvironment)

原文: A var statement declares variables that are scoped to the running execution context’s VariableEnvironment. Var variables are created when their containing Lexical Environment is instantiated and are initialized to undefined when created.

变量环境在文档中没有确切的定义,不过变量环境的特性还是很明显的

  • 在代码运行中,词法环境会随着代码运行而变化,而变量环境不会,变量环境只会修改它内部变量的值
  • 用 var 声明的变量会放到变量环境中,用 let 和 const 声明的变量会放到词法环境中

比如下面的代码

var a = 1;
let b = 2;
if (true) {
    var c = 3;
    let d = 4;
    console.log(b);
}

执行上下文在即将进入 if 时是这样的

ExecutionContext:
    LexicalEnvironment:
        b -> nothing
        outerEnvironment: null
    VariableEnvironment:
        a -> undefined, c -> undefined
        outerEnvironment: null
    ...

进入 if 后是这样的

ExecutionContext:
    LexicalEnvironment:
        d -> nothing
        outerEnvironment:
            LexicalEnvironment
                b -> 2
                outerEnvironment: null
    VariableEnvironment:
        a -> 1, c -> undefined
        outerEnvironment: null
    ...

离开 if 后,执行上下文就变成了这样

ExecutionContext:
    LexicalEnvironment
        b -> 2
        outer: null
    VariableEnvironment:
        a -> 1, c -> 3
        outer: null

通过区分词法环境和变量环境,我们可以理解为什么 let,const 和 var 的特性是不同的,为什么 var 可以变量提升进行访问,为什么在声明前访问 let 和 const 会得到一个引用错误

# 概念补充

我之所以把这部分放到最后,是因为前面的新概念已经很多了,要是这部分混在里面一起写估计读起来会比较困难,而且这部分不是很重要,只是一些概念的延伸和细化。

# 词法环境的分类

  • 全局词法环境:没有外部环境的词法环境,全局环境的外部环境引用为 null
  • 函数词法环境:用户在函数中定义的变量被存储在环境记录中。对外部环境的引用可以是全局环境,也可以是包含内部函数的外部函数环境。

# 环境记录的分类

原文:There are two primary kinds of Environment Record values used in this specification: declarative Environment Records and object Environment Records.
Declarative Environment Records are used to define the effect of ECMAScript language syntactic elements such as FunctionDeclarations, VariableDeclarations, and Catch clauses that directly associate identifier bindings with ECMAScript language values.
Object Environment Records are used to define the effect of ECMAScript elements such as WithStatement that associate identifier bindings with the properties of some object.

  • 声明性环境记录:存储声明的变量、函数和参数。
  • 对象环境记录:用于定义在全局执行上下文中出现的变量和函数的关联。在 with 语句中也会创建

声明型环境记录很好理解,我们上面使用的一直都是声明型环境记录,他会存储所有的变量声明和函数声明。

对象环境记录稍微难理解一点,我们来举个例子

let obj = { foo: 42 };
with (obj) {
    foo = foo / 2;
}
console.log(obj); // 21

在这段代码执行到 with 时,会创建一个 Object Environment Records 对象

name        value
------------------------
     foo         42

有了这个对象,在 with 语句中就不用手动指定 foo 为 obj.foo 了,也就是说,Object Environment Records 是用于把对象的属性添加到环境中的,这也就是为什么你能在直接使用 alert (1) 而不用写成 window.alert (1) 的原因,因为代码就是运行在一个有 Object Environment Records 的环境中。

# 参考

ES3 语言标准
ES5 语言标准
面试官:说说执行上下文吧
理解 Javascript 执行上下文和执行栈
Understanding Execution Context and Execution Stack in Javascript
Understanding JavaScript Execution Context and How It Relates to Scope and the this Context
Learn JavaScript Fundamentals-Global Scope
Activation and Variable Object in JavaScript?
Variable Environment vs lexical environment
What really is a declarative environment record and how does it differ from an activation object?
Javascript Closures

# 后记

那么这次的分享就到这里啦欢迎在评论区留言交流(有疑问也欢迎提出,如果我会我一定回复)如果有什么错误,欢迎大佬指正,那么我们下次再见~