一, 执行环境和执行栈

了解javascript的运行机制,首先必须掌握两个基本的概念。Execution Context(执行环境或执行上下文)和Context Stack (执行栈)

1. 何为执行环境(执行上下文)(Execution Context)

我们知道javascript是单线程语言,也就是同一时间只能执行一个任务。当javascript解释器初始化代码后,默认会进入全局的执行环境,之后每调用一个函数,javascript解释器会创建一个新的执行环境。

    var a = 1;                       // 1.初始化默认进入全局执行环境
    function b() {                   // 3.进入b 的执行环境
        function c() {               // 5. 进入c的执行环境
            ···
        }
        c()                          // 4.在b的执行环境里调用c, 创建c的执行环境
    }
    b()                              // 2. 调用b 创建 b 的执行环境

执行环境的分类:

  • 全局执行环境:简单的理解,一个程序只有一个全局对象即window对象,全局对象所处的执行环境就是全局执行环境。
  • 函数执行环境:函数调用过程会创建函数的执行环境,因此每个程序可以有无数个函数执行环境。
  • Eval执行环境:eval代码特定的环境。

2. 如何单线程运行(Context Stack)

从一个简单的例子开始讲起

function foo(i) {
  if (i < 0) return;
  console.log('begin:' + i);
  foo(i - 1);
  console.log('end:' + i);
}
foo(2);

如何存储代码运行时的执行环境(全局执行环境,函数执行环境)呢,答案是执行栈。而栈遵循的是先进后出的原理,javascript初始化完代码后,首先会创建全局执行环境并推入当前的执行栈,当调用一个函数时,javascript引擎会创建新的执行环境并推到当前执行栈的顶端,在新的执行环境中,如果继续发生一个新函数调用时,则继续创建新的执行环境并推到当前执行栈的顶端,直到再无新函数调用。最上方的函数执行完成后,它的执行环境便从当前栈中弹出,并将控制权移交到当前执行栈的下一个执行环境,直到全局执行环境。当程序或浏览器关闭时,全局环境也将退出并销毁。

傻傻分不清的javascript运行机制-LMLPHP

因此输出的结果为:

begin:2
begin:1
begin:0
end:0
end:1
end:2

3. 如何创建执行环境

我们现在知道每次调用函数时,javascript 引擎都会创建一个新的执行环境,而如何创建这一系列的执行环境呢,答案是执行器会分为两个阶段来完成, 分别是创建阶段和激活(执行)阶段。而即使步骤相同但是由于规范的不同,每个阶段执行的过程有很大的不同。

3.1 ES3 规范

创建阶段:

  • 1.创建作用域链。
  • 2.创建变量对象VO(包括参数,函数,变量)。
  • 3.确定this的值。

激活/执行阶段:

  • 完成变量分配,执行代码。
3.2 ES5 规范

创建阶段:

  • 1.确定 this 的值。
  • 2.创建词法环境(LexicalEnvironment)。
  • 3.创建变量环境(VariableEnvironment)。

激活/执行阶段:

  • 完成变量分配,执行代码。

我们从规范上可以知道,ES3和ES5在执行环境的创建阶段存在差异,当然他们都会在这个阶段确定this 的值
(关于this 的指向问题我们以后会在专门的文章中分析各种this 的指向问题,这里便不做深究)。我们将围绕这两个规范不同点展开。尽管ES3的一些规范已经被抛弃,但是掌握ES3 创建执行环境的过程依然有助于我们理解javascript深层次的概念。

二, Variable Object(VO: 变量对象),Active Object(AO: 活动对象)

2.1 基本概念

VO 和 AO 是ES3规范中的概念,我们知道在创建过程的第二个阶段会创建变量对象,也就是VO,它是用来存放执行环境中可被访问但是不能被 delete 的函数标识符,形参,变量声明等,这个对象在js环境下是不可访问的。而AO 和VO之间区别就是AO 是一个激活的VO,仅此而已。

  • 变量对象(Variable) object)是说JS的执行上下文中都有个对象用来存放执行上下文中可被访问但是不能被delete的函数标示符、形参、变量声明等。它们会被挂在这个对象上,对象的属性对应它们的名字对象属性的值对应它们的值但这个对象是规范上或者说是引擎实现上的不可在JS环境中访问到活动对象

  • 激活对象(Activation object)有了变量对象存每个上下文中的东西,但是它什么时候能被访问到呢?就是每进入一个执行上下文时,这个执行上下文儿中的变量对象就被激活,也就是该上下文中的函数标示符、形参、变量声明等就可以被访问到了

2.2 执行细节

如何创建VO对象可以大致分为四步

  • 1.创建arguments对象
  • 2.扫描上下文的函数声明(而非函数表达式),为发现的每一个函数,在变量对象上创建一个属性——确切的说是函数的名字——其有一个指向函数在内存中的引用。如果函数的名字已经存在,引用指针将被重写。
  • 3.扫描上下文的变量声明,为发现的每个变量声明,在变量对象上创建一个属性——就是变量的名字,并且将变量的值初始化为undefined。如果变量的名字已经在变量对象里存在,将不会进行任何操作并继续扫描。

注意: 整个过程可以大概描述成: 函数的形参=>函数声明=>变量声明, 其中在创建函数声明时,如果名字存在,则会被重写,在创建变量时,如果变量名存在,则忽略不会进行任何操作。

一个简单的例子

function foo(i) {
    var a = 'hello';
    var b = function privateB() {

    };
    function c() {

    }
}

foo(22);

执行的伪代码

// 创建阶段
fooExecutionContext = {
    scopeChain: { ... },
    variableObject: {
        arguments: {
            0: 22,
            length: 1
        },
        i: 22,
        c: pointer to function c()
        a: undefined,
        b: undefined
    },
    this: { ... }
}
// 激活阶段
fooExecutionContext = {
    scopeChain: { ... },
    variableObject: {
        arguments: {
            0: 22,
            length: 1
        },
        i: 22,
        c: pointer to function c()
        a: 'hello',
        b: pointer to function privateB()
    },
    this: { ... }
}

三, LexicalEnvironment(词法环境),VariableEnvironment(变量环境)

3.1 基本概念

词法环境和变量环境是ES5以后提到的概念,官方对词法环境的解释如下。

简单的理解,词法环境是一个包含标识符变量映射的结构。(这里的标识符表示变量/函数的名称,变量是对实际对象【包括函数类型对象】或原始值的引用)。

ES3的VO,AO为什么可以被抛弃?个人认为有两个原因,第一个是在创建过程中所执行的创建作用域链和创建变量对象(VO)都可以在创建词法环境的过程中完成。第二个是针对es6中存储函数声明和变量(let 和 const)以及存储变量(var)的绑定,可以通过两个不同的过程(词法环境,变量环境)区分开来。

3.2 词法环境(lexicalEnvironment)

词法环境由两个部分组成

  • 环境记录(enviroment record),存储变量和函数声明
  • 对外部环境的引用(outer),可以通过它访问外部词法环境

对外部环境的引用关系到作用域链,之后再分析,我们先来看看环境记录的分类。

环境记录分两部分

  • 声明性环境记录(declarative environment records): 存储变量、函数和参数, 但是主要用于函数 、catch词法环境。
    注意:函数环境下会存储arguments的值。而详细的过程可以参考VO 的执行细节,基本大同小异
  • 对象环境记录(object environment records), 主要用于with 和全局的词法环境

伪代码如下

// 全局环境
GlobalExectionContext = {
// 词法环境
  LexicalEnvironment: {
    EnvironmentRecord: {

        ···
    }
    outer: <null>
  }
}
// 函数环境
FunctionExectionContext = {
// 词法环境
  LexicalEnvironment: {
    EnvironmentRecord: {
        // 包含argument

    }
    outer: <Global or outer function environment reference>
  }
}

3.3 变量环境(objectEnvironment)

变量环境也是个词法环境,主要的区别在于lexicalEnviroment用于存储函数声明和变量( let 和 const )绑定,而ObjectEnviroment仅用于存储变量( var )绑定。

3.4 伪代码展示

ES5规范下的整个创建过程可以参考下方的伪代码

let a = 20;
const b = 30;
var c;

function d(e, f) {
 var g = 20;
 return e * f * g;
}

c = d(20, 30);
// 全局环境
GlobalExectionContext = {

  this: <Global Object>,
    // 词法环境
  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Object",  // 环境记录分类: 对象环境记录
      a: < uninitialized >,  // 未初始化
      b: < uninitialized >,
      d: < func >
    }
    outer: <null>
  },

  VariableEnvironment: {
    EnvironmentRecord: {
      Type: "Object",  // 环境记录分类: 对象环境记录
      c: undefined,  // undefined
    }
    outer: <null>
  }
}
// 函数环境
FunctionExectionContext = {

  this: <Global Object>,

  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",  // 环境记录分类: 声明环境记录
      Arguments: {0: 20, 1: 30, length: 2},  // 函数环境下,环境记录比全局环境下的环境记录多了argument对象
    },
    outer: <GlobalLexicalEnvironment>
  },

  VariableEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",  // 环境记录分类: 声明环境记录
      g: undefined
    },
    outer: <GlobalLexicalEnvironment>
  }
}

四,作用域链

前面讲创建过程中,我们留下了一个伏笔,ES3规范中有创建作用域链的过程,而ES5中在创建词法环境或变量环境的过程中,也有生成外部环境的引用的过程。那这个过程有什么作用呢。我们通过一个简单的例子来说明。

function one() {

    var a = 1;
    two();

    function two() {

        var b = 2;
        three();

        function three() {

            var c = 3;
            alert(a + b + c); // 6

        }

    }

}

one();

当执行到three 的执行环境时,此时 a和b 都不在c 的变量内,因此作用域链则起到了引用外部执行环境变量的作用。ES3中创建的作用域链如图:

傻傻分不清的javascript运行机制-LMLPHP

当解释器执行alert(a + b + c),他首先会找自身执行环境下是否有a这个变量的存在,如果不存在,则通过查看作用域链,判断a是否在上一个执行环境内部。它检查是否a存在于内部,若找不到,则沿着作用域链往上一个执行环境找,直到找到,或者到顶级的全局作用域。同理ES6规范中也可以这样分析。

因此这会引入一个javascript一个重要的概念,闭包。从上面对执行环境的解释我们可以这样理解,闭包就是内部环境通过作用域链访问到上层环境的变量。因此也存在无法进行变量回收的问题,只要函数的作用域链在,变量的值便因为闭包无法被回收。

注意: 此作用域链和原型链的作用域链不是同一个概念。

五, 小结

通过对javascript运行机制的介绍,对一些javasript高级概念有了更深的认识,特别是对一些云里雾里的概念区别有了更深刻的认识。不同规范下,不同概念的解释更有利于深挖javascript底层的执行思想。我相信这是理解javascipt语言最重要的一步。

参考资料:

12-24 19:49