JavaScript - Execution Context

2017-09-29

执行上下文(Execution Context)

在JavaScript中有三种代码运行环境:

  • 全局环境Global Code
    JavaScript代码开始运行的默认环境
  • 函数环境Function Code
    代码进入一个JavaScript函数
  • Eval Code
    使用eval()执行代码

为了表示不同的运行环境,JavaScript中有一个执行上下文(Execution context,EC)的概念。也就是说,当JavaScript代码执行的时候,会进入不同的执行上下文,这些执行上下文就构成了一个执行上下文栈(Execution context stack,ECS)。

变量对象(Variable object)

我们知道 变量和执行上下文相关 那么它就应该知道数据储存在哪里以及如何访问这些数据,这种机制被称为变量对象(Variable Object)简称VO。
处理执行上下文代码分为两个阶段:
①进入执行上下文
②执行代码
当进入执行上下文时(代码执行前),变量对象VO 就会被下列属性填充:

  1. 函数的所有形参(如果是在函数执行上下文中)
    每个形参都对应变量对象中的一个属性,该属性由形参名和对应的实参值构成,如果没有传递实参,那么该属性值就为 undefined
  2. 所有函数声明(FunctionDeclaration, FD)
    每个函数声明都对应变量对象中的一个属性,这个属性由一个函数对象的名称和值构成,如果变量对象中存在相同的属性名,则完全替换该属性。
  3. 所有变量声明(var, VariableDeclaration)
    每个变量声明都对应变量对象中的一个属性,该属性的键/值是变量名和 undefined,如果变量名与已经声明的形参或函数相同,则变量声明不会干扰已经存在的这类属性。

活动对象(Activation object)

只有全局上下文的变量对象允许通过VO的属性名称间接访问;在函数执行上下文中,VO是不能直接访问的,此时由激活对象(简称AO)扮演VO的角色。激活对象 是在进入函数上下文时刻被创建的,它通过函数的arguments属性初始化。

Arguments Objects 是函数上下文里的激活对象AO中的内部对象,它包括下列属性:

  1. callee:指向当前函数的引用
  2. length: 真正传递的参数的个数
  3. properties-indexes:就是函数的参数值(按参数列表从左到右排列)

对于VO和AO的关系可以理解为,VO在不同的Execution Context中会有不同的表现:当在Global Execution Context中,可以直接使用VO;但是,在函数Execution Context中,AO就会被创建。

JS解析器做了什么

当一段JavaScript代码执行的时候,JavaScript解释器会创建执行上下文,其实这里会有两个阶段:

1.创建阶段(进入执行上下文)

  • 创建作用域链Scope chain
  • 创建VO/AO(variables, functions and arguments)
  • 设置this的值

2.激活阶段(执行代码)
设置变量的值、函数的引用,然后解释/执行代码。

对于”创建VO/AO”这一步,JavaScript解释器主要做了下面的事情:

  • 根据函数的参数,创建并初始化参数对象
  • 扫描函数内部代码,查找函数声明

    • 对于所有找到的函数声明,将函数名和函数引用存入VO/AO中
    • 如果VO/AO中已经有同名的函数,那么就进行覆盖
  • 扫描函数内部代码,查找变量声明

    • 对于所有找到的变量声明,将变量名存入VO/AO中,并初始化为”undefined”
    • 如果变量名称跟已经声明的形式参数或函数相同,则变量声明不会干扰已经存在的这类属性

举个栗子

理解概念很枯燥,我们来分析实例。

1
2
3
4
5
6
7
8
9
console.log(a);
var a = 1;
console.log(a);
function a(){alert(2)}
console.log(a);
var a = 3;
console.log(a);
function a() {alert(4);}
console.log(a);

分析流程如下:

进入执行上下文(创建阶段)填充vo对象

  1. 第二行有var定义的变量a,将其保存为a=undefined;

  2. 第4行有function声明和第二行的a同名此时由于函数的等级高于变量,于是就覆盖变量a,此时a= function a (){ alert(2); }

  3. 第六行又发现一个var定义的变量,名称与第四行的函数相同,但等级低,故a= function a (){ alert(2); }不变。

  4. 同理,由于第八行后定义,又为同一等级的函数,所以a= function a (){ alert(4); }

  5. 浏览器解析完成
    此时的
    vo= {
    a:
    }

    执行代码(激活阶段)

  6. 第一行 a应该是打印function a() {alert(4);}

  7. 第二行表达式修改了a的值,使其为1,所以第三行输出a=1

  8. 第四行定义了一个函数,但没有执行,所以第五行输出还为1

  9. 第六行表达式又一次更改了a的值,现在a=3,此时的a为一个数字,第七行输出3

  10. 同理,第八行没有做更改,第九行还是输出3