;

Understanding JavaScript Execution Context

Execution Context的概念

Understanding JavaScript Execution Context

在JavaScript中,当前代码运行的环境被称为 执行上下文(Execution Context) ,或者 作用域(Scope) 。在客户端中,JavaScript是以单线程的方式运行的,所以某个时间点它只能在下面三个Execution Context中的一个中运行:

  1. Global Execution Context
  2. Functional Execution Context
  3. Eval Execution Context

Understanding JavaScript Execution Context

Functional Execution Context是当执行流程进入函数体内时代码执行环境,Eval Execution Context是通过 eval() 函数执行代码时的代码执行环境。

每次调用函数时都会创建一个Functional Execution Context,而Global Execution Context只有一个。

Execution Context Stack

在客户端中,JavaScript是以单线程的方式运行的,所以某个时间点它只能做一件事情,其他的操作或者事件只能排队等待。

比如下面的脚本

function first() {  
    console.log('Welcome to the first room ...');

    second();
    function second() {
        console.log('Welcome to the second room ...');

        third();
        function third() {
            console.log('Welcome to the third room ...');
        }
    }
}

first();  

当在Global Execution Context中执行 first(); 这句时,会依次调用三个嵌套的函数: first() second() third()

函数调用的过程

Understanding JavaScript Execution Context

函数调用的过程其实是一个入栈的过程,每次调用一个函数就会创建一个Functional Execution Context,然后加入到Execution Context Stack的顶部。

函数执行返回的过程

Understanding JavaScript Execution Context

函数执行返回的过程其实是一个出栈的过程,一旦一个Execution Context完成了执行,它就会被推出栈顶并且将控制权返回给它下面的Execution Context,直到Global Execution Context。

函数执行返回、函数执行、函数调用

函数执行返回 :是指函数内的语句(包括调用其他函数/方法)全部执行完成,并且返回调用该函数的地方

函数执行 :包括执行该函数内部普通语句(即 代码执行 ),也包括调用其他函数/方法(即 函数调用

可以看出,Execution Context Stack具有如下的特点:

  1. 单线程
  2. 同步执行
  3. 只有一个Global Execution Context
  4. 可以有多个Functional Execution Context,每次调用一个函数都会创建一个Functional Execution Context

JavaScript解释器(JavaScript Interpreter)内部如何操作Execution Context

在JavaScript解释器的内部,每一个Execution Context的执行都会经历以下两个阶段:

  1. 创建阶段(Creation Stage)
  2. 激活/运行阶段(Activation Stage / Code Execution Stage)
创建阶段

主要是指,函数被调用了,但是还没有开始执行函数内部的代码。在Execution Context的创建阶段,解释器会做如下的一些工作:

  1. 创建作用域链(Scope Chain)
  2. 创建Activation Object / Variable Object
  3. 确定 this 的值,即Context

如以下代码

function add(num1, num2){  
    return num1 + num2;
}

var result = add(5, 10);  

调用函数 add() 将创建一个Execution Context,如图所示:

Understanding JavaScript Execution Context

在Execution Context的创建阶段,JavaScript解释器(JavaScript Interpreter)会扫描传入函数的参数、函数内的变量声明和函数声明,从而在Execution Context中创建一个与之关联的Variable Object。被扫描到的变量、参数和函数将作为Variable Object的属性存在(见图)。

创建Execution Context的大致的过程可以描述为:

  1. 调用函数(Invoke function)
  2. 在执行函数内的代码之前,创建Execution Context
    1. 初始化作用域链
    2. 创建Variable Object,按顺序执行:
      • 创建 arguments 对象,并且检查该函数的参数,完成对形式参数的赋值
      • 在该函数内扫描函数声明,在Variable Object中创建或覆盖相应的属性
      • 在该函数内扫描变量声明,在Variable Object中创建响应的属性, 并且将值初始化为 undefined 。如果在Variable Object中已经存在与声明的变量同名的属性,则什么都不做,继续扫描下一个变量声明。
  3. 确定 this 的值,即Context

比如

function foo(name) {  
    var a = 'hello';
    var b = function f2() {};
    function f3() {}
}

foo('River He');  

调用 fn('River He') ,创建如下所示的Execution Context:

fooExecutionContext = {  
    scopeChain: {...},
    variableObject: {
        arguments: {
: 'River He',
            length: 1
        },
        name: 'River He',
        f3: pointer to function f3(),
        a: undefined,
        b: undefined
    },
}

可见,在这一阶段除了函数的参数会进行赋值外,对于该函数内部的变量则只会在Variable Object中定义属性名。

激活/运行阶段

对VariableObject中的属性进行赋值,解释执行函数内的代码。到这一阶段,上述例子中的Execution Context会变成:

fooExecutionContext = {  
    scopeChain: {...},
    variableObject: {
        arguments: {
: 'River He',
            length: 1
        },
        name: 'River He',
        f3: pointer to function f3(),
        a: 'hello',
        b: pinter to function f2()
    },
}

Scope Chain

在代码运行期间,标识符解析(即变量名和函数名的查找)是通过当前Execution Context的Scope Chain进行的。

对于当前的Execution Context:

  1. 它本身的Variable Object就是Scope Chain的第一个元素
  2. 在Execution Context Stack中仅次于 当前Execution Context 的那个Execution Context的Variable Object为它的Scope Chain中的第二个元素,依次类推,直到Global Execution Context的Variable Object。

About Hoisting

对于如下的代码,会发生ReferenceError

(function() {
    // ReferenceError: Can't find variable: name
    console.log(name);
}());

而对于一下代码,虽然变量 name 是在 console.log(name) 后声明的,但却不会发生ReferenceError:

(function() {
    // undefined
    console.log(name);

    // declare variable 'name'
    var name = 'River He';
}());

表面看起来好像是变量声明 提前(hoisted) 了,类似这样:

(function() {
    var name;

    // undefined
    console.log(name);

    name = 'River He';
}());

其实不然,这是因为:

  1. 在JavaScript解释器解释执行函数内的代码之前:需要创建Execution Context,在创建Execution Context的时候对于函数内的变量声明 var name = 'River He'; 进行了扫描,并且在Variable Object中创建了相应的属性。
  2. 在JavaScript解释器解释执行函数内的代码的时候:需要对遇到的变量做解析,解析的过程就是依次查找当前Execution Context的Scope Chain中的各个Variable Object的属性。根据第1步,可以找到相应的属性,因而不会发生ReferenceError。

至于为什么是 undefined ,则是因为在Execution Context的创建阶段,对于函数内的变量声明,JavaScript解释器会在Variable Object中创建相应的属性,并且初始化为undefined.

同理,以下的惯用法中,即便变量 name 没有事先声明,也不会发生ReferenceError

var name = name || 'River He';  

对于如下代码

(function() {
    console.log(typeof foo);

    var foo = 'bar';
    function foo() {}
}())

它的输出是 function 而不是 undefined ,这是因为Execution Context创建阶段,函数内的函数声明是优先于变量声明被JavaScript解释器扫描并且在Variable Object中创建相应的属性的。

因此Variable Object中首先有个名为 foo 的属性,并且它的值是一个Function类型对象的引用。在扫描完函数声明后,JavaScript解释器接着扫描变量声明,扫描到 var foo = 'bar'; 的时候,因为Variable Object中已经存在了名为 foo 的属性,因此JavaScript会什么都不做,继续往下扫描。

Global Variable

在Global Execution Context中的全局对象就是Global Execution Context中的Variable Object,并且为 this 所引用。

显式声明

在Global Execution Context中使用了 var 声明的变量就是显式声明的全局变量。显式声明的全局变量本质上是Global Execution Context中Variable Object的一个属性,并且这个属性不能被删除。

// 显式声明全局变量
var name;

// 删除全局对象中的"name"属性,false
console.log(delete this.name);  

这是因为如果一个全局变量能够删除,那么可能会给整个JavaScript程序造成影响。通过将全局对象中与显式声明的全局变量同名的属性的configurable属性设置为false,让显式声明的全局变量不可删除。

var name = 'River He';  
console.log(Object.getOwnPropertyDescriptor(this, 'name'));  

输出

Understanding JavaScript Execution Context

隐式声明的全局变量

无论在Global Execution Context中还是在Functional Execution Context中,没有使用 var 声明的变量都是全局变量。

name = 'River He';  

或者

function fn() {  
    name = 'River He';
}

在Execution Context的创建阶段,这甚至不会被认为是变量声明,而是变量赋值。事实上也确实如此。也就是说,在创建Excution Context的Variable Object,JavaScript解释器扫描函数内的变量声明的时候,并不会为上述例子中的 name 变量在Variable Object中创建相应的属性。

因此,当开始执行Execution Context中的代码,对变量 name 进行解析,查找Scope Chain的时候会一直查找到Global Execution Context。当在Global Execution Context中依然无法查找到时,此时就会在Global Execution Context中的Variable Object中创建相应的属性。

虽然这看起来确实是一个全局变量,但是严格说起来它不是,因为全局对象从设计的角度就不应该是可删除的,而这里的"全局变量"是可以被删除的:

name = 'River He';

// true
console.log(delete this.name);  

查看全局对象中相应属性的 configurable 属性:

name = 'River He';  
console.log(Object.getOwnPropertyDescriptor(this, 'name'));  

输出

Understanding JavaScript Execution Context

在ECMAScript 5的 strict mode 中,通过这样的方式声明全局变量是不合法的。从性能的角度来说,也不应该使用这种方式。

作者:Novtopro Tracker

When the problem is complexity, the cure might just be simplicity.