执行上下文

1. 执行上下文是什么

当执行一段代码的时候,就会进行准备工作,这里的“准备工作”,让我们用个更专业一点的说法,就叫做“执行上下文(execution context)”。

2. 执行上下文是怎样产生的

执行上下文会产生三种执行上下文,分别是:

  1. 全局执行上下文(刚开始执行script标签或者刚开始执行js代码的时候就会产生);
  2. 函数执行上下文;
  3. eval执行上下文(了解一下即可,因为现在不太使用了)。

3. 执行上下文产生的时候发生了什么

每个执行上下文中都会有三个重要的属性:

  1. 变量对象(VO),包含变量、函数声明和函数的形参,该属性只能在全局上下文中访问;
  2. 作用域链(JS采用词法作用域,也就是说变量的作用域是在定义的时候就决定了);
  3. this指向。

4. 执行上下文栈的作用是什么

接下来我们先来看一段代码:

1
2
3
4
5
6
7
8
9
10
function fun3(){
console.log('fun3');
}
function fun2(){
fun3();
}
function fun1(){
fun2();
}
fun1(); // fun3

这段代码是很简单,我们可以很快的就得出打印的内容为fun3。但是引擎没有我们那么智能,它需要知道它是如何进行一个先后顺序来执行的,那么引擎该如何管理创建的那么多执行上下文呢?
JavaScript引擎创建了执行上下文栈(Execution context stack,ECS)来管理执行上下文。
关于上面的代码,接下来我会画一张图来解释执行上下文栈是怎么执行的:
执行上下文栈执行过程

5. 什么是变量对象

变量对象:是与执行上下文相关的数据作用域,存储了在上下文中定义的变量和函数声明。

  1. 全局上下文的变量对象初始化时全局对象;
  2. 函数上下文的变量对象初始化只包含Arguments对象(即实参);
  3. 进入执行上下文时会给变量对象添加形参、函数声明、变量声明等初始的属性值(即就是变量声明的提升);
  4. 代码执行阶段,会再次修改变量对象的属性值。

6. 执行代码的时候分为三步

  1. 步骤一:初始化
    a. 全局上下文,就等价于全局对象window。
    全局对象是预定义的对象,作为JavaScript的全局函数和全局属性的占位符。通过使用全局对象,可以访问所有其他所有预定义的对象、函数和属性。
    在最顶层JavaScript代码中,可以用关键字this引用全局对象。因为全局对象是作用域链的头,这意味着所有非限定性的变量和函数名都会作为该对象的属性来查询。
    例如,当JavaScript代码引用parseInt()函数时,它引用的是全局对象的parseInt属性。全局对象是作用域链的头,还意味着在顶层JavaScript代码中声明的所有变量都将成为全局对象的属性。
    b. 函数上下文。
    在函数上下文中,我们用活动对象(activation object,AO)来表示变量对象。活动对象和变量对象其实是一个东西,只是变量对象是规范上的或者说是引擎实现上的,不可在JavaScript环境中访问,只有到当进入一个执行上下文中,这个执行上下文的变量对象才会被激活(即只有当执行这个函数的时候才会被激活),所以才叫activation object的,而只有被激活的变量对象,也就是活动对象上的各种属性才能被访问。
    活动对象是在进入函数上下文时被创建的,它通过函数的arguments属性初始化(即它初始化的值就是我们的一个实参)。arguments属性值是Arguments对象。

  2. 步骤二:进入执行上下文
    变量对象会包括:
    a. 函数的所有形参(如果是函数上下文)
    (1) 由名称和对应值组成的一个变量对象的属性被创建;
    (2) 没有实参,属性值设为undefined。
    b. 函数声明
    (1)由名称和对应值(函数对象(function-object))组成一个变量对象的属性被创建;
    (2)如果变量对象已经存在相同名称的属性,则完全替换这个属性。
    c. 变量声明
    (1)由名称和对应值(undefined)组成一个变量对象的属性被创建;
    (2)如果变量名称跟已经声明的形式参数或函数相同,则变量声明不会干扰已经存在的这类属性。

  3. 步骤三:代码执行
    在代码执行阶段,会顺序执行代码,根据代码,修改变量对象的值。

看完这些之后,我们来做一个小例子进行一个练习,代码和解析如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
function foo(a){
var b = 2;
function c(){}
var d = function(){};
b = 3;
console.log(e); //报错;Uncaught ReferenceError: e is not defined
}
function bar(){
var e = 1;
// 错误的理解:
// 如果我们说的词法作用域不是以定义函数它的父级来看,而是以执行函数的父级来看的话,
// 它的父级就是bar,而不是Global全局,那么此时控制台打印出来就是e的值为1
foo(1);
}
bar();

// 首先进行预编译过程(初始化)注意:下面显示的是伪代码
// foo函数进行一个预编译
// AO = {
// arguments: {
// 0: 1,
// length: 1
// },
// a: undefined->1, 表示从undefined变为1
// b: undefined,
// c: function(){},
// d: undefined,
// }

// 接着就是代码执行阶段
// foo函数执行
// AO = {
// arguments: {
// 0: 1,
// length: 1
// },
// a: 1,
// b: undefined->2->3,
// c: function(){},
// d: undefined->function(){}
// }

// 在分析完成之后,此时就会去找e的值为多少,首先它会先去自己的AO中查找,发现没有找到e属性,然后它又
// 会去词法作用域的父级去找(即定义foo函数的父级,它是一个Global全局),最后发现也没有e属性,然后
// 最后会在控制台报错,提示你没有定义e

7. 什么是作用域链

当查找变量的时候,会先从当前上下文的变量对象中查找,如果没有找到,就会从父级(词法层面上的父级,即定义函数它的父级,而不是执行函数的一个父级)执行上下文的变量对象中查找,一直找到全局上下文的变量对象,也就是全局对象。这样由多个执行上下文的变量对象构成的链表就叫做作用域链。

8. 函数创建和函数激活

  1. 函数创建
    这是因为函数有一个内部属性[[scope]],当函数创建(即函数定义)的时候,就会保存所有父变量对象到其中,你可以理解[[scope]]就是所有父变量对象的层级链,但是注意:[[scope]]并不代表完整的作用域链!!!

  2. 函数激活
    当函数激活时(即执行函数),进入函数上下文,创建VO/AO后,就会将活动对象添加到作用域链的前端。这时候执行上下文的作用域链,我们命名为scope:scope = [AO].concat([[scope]])
    举一个例子,代码如下所示:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    function foo(){
    function bar(){
    // ...
    }
    }

    // foo.[[scope]] = [
    // fooContext.AO,
    // globalContext.VO
    // ];

    // bar.[[scope]] = [
    // barContext.AO,
    // fooContext.AO,
    // globalContext.VO
    // ]

接下来我们举一个实例,然后对上面的进行一个总结和练习,代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
var scope = "global scope";
function checkscope(){
var scope2 = "local scope";
return scope2;
}
console.log(checkscope()); // local scope

// 解析,伪代码:
// 首先执行全局代码,创建全局执行上下文,全局上下文被压入执行上下文栈
// ECStack = [globalContext]
// 1. 全局的一个初始化
// globalContext.VO = {
// this: window,
// scope: [globalContext.VO],
// checkscope: function(){}
// }

// 2. checkscope函数被创建,保存作用域链到内部属性[[scope]]
// checkscope.[[scope]] = [
// globalContext.VO
// ];

// 3. 执行checkscope函数,创建checkscope函数执行上下文,
// checkscope函数执行上下文被压入执行上下文栈
// ECStack = [
// checkscopeContext.AO,
// globalContext.VO
// ]

// 4. checkscope函数并不会立即执行,开始做准备工作,第一步:复制函数[[scope]]属性创建作用域链
// checkscopeContext = {
// scope: checkscope.[[scope]]
// }
// 第二步:用arguments创建活动对象,随后初始化活动对象,加入形参、函数声明、变量声明
// checkscopeContext = {
// AO: {
// arguments: {
// length: 0
// },
// scope2: undefined
// },
// scope: checkscope.[[scope]]
// }
// 第三步:将活动对象压入checkscope作用域链顶端
// checkscopeContext = {
// AO: {
// arguments: {
// length: 0
// },
// scope2: undefined
// },
// scope: [checkscopeContext.AO,globalContext.VO]
// }
//5. 准备工作做完,开始执行checkscope函数,随着函数的执行,修改AO的属性值
// checkscopeContext = {
// AO: {
// arguments: {
// length: 0
// },
// scope2: "local scope"
// },
// scope: [checkscopeContext.AO,globalContext.VO]
// }
// 6. 查找到scope2的值为"local scope",返回后函数执行完毕,如果没有找到的话,就会去
// 它的作用域链找;函数上下文从执行上下文栈中弹出,然后最后在执行上下文栈中只剩全局的globalContext
// ECStack = [
// globalContext.VO
// ];