ECMAScript3 VS ECMAScript5 - 代码执行流程

##ECMAScript3 代码执行流程

###ES3中可以执行的代码分为下面三种:

  • 全局代码
  • 函数代码
  • eval代码

ECAMScript代码进入上述的每一种代码环境开始执行之前,会创建一个叫做 执行上下文(Execution Context) 的东西,它存在于这个代码环境执行的整个过程,在退出代码环境的时候,也退出了对应的上下文。

简单的来说,一段javascript代码,从上到下,每次进入到上述3种之一的代码,就会创建一个上下文,可执行代码执行完了之后这个上下文也会被销毁,整个程序自然而然可以想象的到,内部维护了一个栈,这个栈的顶端就代表着当前的执行环境,每进入一个新的代码执行环境,就创建并推入上下文栈一个新的上下文,退出的时候从上下文栈中推出。

可执行上下文 我们简称 EC

举个例子

var GLOBAL = 1;     ------ 1
function fn () {  
    var inner = 2;  ------ 3
}
fn();               ------ 2
console.log('end'); ------ 4

代码先执行1,再执行2,再执行3,最后是4,这个不用多说吧。

在执行代码1之前就是进入了一个全局EC,EC栈就是这样的:

 //栈头就指示了当前正在使用的可执行上下文
 栈头:   全局EC

在执行到代码2的时候进入fn函数代码,创建一个 函数EC 并且压入EC栈中,此时全局代码并没有退出,所以在执行3的时候EC栈是:

 栈头:  函数fn的EC
        全局EC

然后函数执行完毕,从函数中出来,函数栈弹出,执行 步骤4的时候EC栈为:

 栈头: 全局EC

最后所有代码执行结束,全局EC也退出,栈被销毁,执行完毕。

###EC包含了三个部分:

  • Scope chain (常说的 作用域链)
  • this (声明了this指向谁)
  • Variable object (变量对象,存储变量值)

我们把创建这三个参数作为第一步,所以对于一个javascript引擎,执行流进入了一段可执行代码,第一步:

1、 创建对应的EC,初始化 this、变量对象、作用域链

下面分别讲解变量对象和作用域链的值是什么,以及有什么用。

####变量对象

变量对象是一个抽象的概念,在不同的可执行环境中对应的真实对象不同,在全局代码、函数代码、eval代码中分别对应:
全局对象 、 激活对象、 Calling context.vo,这不重要,我们看具体的。

变量对象由3部分组成:

  • 变量
  • 声明式函数
  • 函数形参

变量是指通过 var声明的变量。
声明式函数是指 函数声明 而不是通过var指向的函数,那种算在变量里面。
形参就不用讲了。

如果出现重复标识符,优先级 函数声明 > 形参 > var 声明

在EC初始化完成之后,变量对象会有一个赋值的过程,称为变量实例化 ,它就是找出变量对象的3种组成部分赋给变量对象。

注:到此为止代码都没开始执行 ,我们把EC抽象成一个对象,比如下面这段代码

(function(x){
    alert(x);
    alert(y);

    var x = 1;
    var y = 2;
    function x(){}

    alert(x);
})(1)

在代码执行之前,形成 EC,然后变量对象 VO开始实例化,得到的结果:

EC = {
  VO: {
    x: function x(){}, //根据优先级,函数声明保留下来了
    y: undefined //这里的理解很关键,这时候代码还没开始执行,对于变量,只是解析到了标识符,对应的值要在代码执行之后才能被更改。
  }
}

所以在第一行代码alert x和y就对应着变量对象里面的初始值,随着代码的执行,这个值会被更改,比如 x对应的值会被改成1,第二个alert就对应1了

####作用域链
作用域链:
全局作用域链就是全局对象,没啥好说的,看函数作用域链。

fn的SC = [当前EC的VO , fn的[[scope]]属性]

fn的[[scope]]属性是一个内置属性,它就等于 fn创建时候的EC对应的VO。比较绕:

其实fn的SC = [[当前EC的VO , fn被创建时候EC对应的VO],说白了就是它父亲对应的VO,所以作用域链上面就是一个个变量对象。

其中当前的变量对象放在作用域链最前面。

作用域链就可以帮助解析标示符,先从当前变量对象找起,如果当前没有就去找父级的。

例子:

var x = 1;   ---- 1
(function (){
  alert(x);  ----- 2
  var x = 2;
  var y = 3;
})()

//
在执行1之前创建全局EC
GloablEC = {
  VO: {
    x: undefined  
  },
  SC: [GlobalEC.VO]
}

这个从上面讲的知识就能得到。

在执行2之前进入函数代码创建函数EC

fnEC = {
  VO: {
    x: undefined, //这个x和全局x没任何关系,因为上面的变量实例化说了,它只关注这段代码里面的 变量、函数声明和形参,里面有var x了,所以创建一个x
    y: undefined
  },
  SC: [fnEC.VO,GlobalEC.VO] 
}

所以2处的代码alert的时候先去找 fnEC.VO里面有没有x,有,alert出undefined,如果没有才会去找GlobalEC.VO,此时由于已经执行过了 x =1;对应的GlobalEC.VO值就是1.

####this 指向没有太多规律,以后遇到再写

至此,代码执行前第二步完成,所以整个代码执行流程:

  • 进入代码环境,代码执行前:

    • 创建对应的EC,初始化 this、变量对象、作用域链
    • 变量实例化、作用域链赋值
  • 代码开始执行:

    • 根据赋值修改变量对象里面的标识符对应的值
    • 根据作用域链查找标识符对应的值

##ECMAScript5 代码执行流程

###ES5中可执行代码和ES3一样

###ES5中EC也是包含了三个部分:

  • LexicalEnvironment (词法环境)
  • this (声明了this指向谁)
  • VariableEnvironment (变量环境,存储变量值)

其中 变量环境和词法环境在初始化的时候 都指向同一个新创建的 词法环境, 在代码执行过程中,词法环境里面的环境记录会发生改变,但是变量环境对应的值不再改变。

stackoverflow上面有个关于变量环境和词法环境关系的讨论,一般情况下认为它们相等就行了

变量环境和词法环境关系

###进入一段 function代码
1 创建EC

初始化过程和ES3类似,只不过不是创建变量对象和作用域链,改成了词法环境和变量环境。

词法环境 = 环境记录项(类似于变量对象) + 外部词法环境。
创建一个新的词法环境是,环境记录项和ES3类似是当前词法环境的标识符、函数声明、形参等,外部词法环境也是等于 function的[[scope]]属性,这属于对应的是function创建时候的词法环境。

有点类似于 作用域链的感觉。层层嵌套词法环境在标识符解析的时候就能形成作用域链一般的功能。

2 初始化绑定

环境记录项在函数中和ES3一样,收集 函数声明、变量声明和形参

ES5会判断是否是严格模式

ES5会对一些函数的新属性做处理,比如 getter之类的

##结论

  • ES3和ES5在代码执行方面其实换汤不换药,不过换了几个概念。
  • 作用域链在ES5中用词法环境理解就ok,变量对象即存储变量值的在ES5中用环境标示来理解。
  • ES5和ES3一样,在进入可执行代码时会先准备环境,所以形成了有些资料里面所谓的 函数声明前置的现象,因为执行上下文中会先把执行环境里面的 变量函数声明以及形参 准备好,理解了这点,对于代码执行就基本搞懂了,不管alert放在哪儿,都知道应该是什么值。
喝杯咖啡,交个朋友