JS编程建议——78:正确理解执行上下文和作用域链

建议78:正确理解执行上下文和作用域链
执行上下文(execution context)是ECMAScript规范中用来描述 JavaScript 代码执行的抽象概念。所有的 JavaScript 代码都是在某个执行上下文中运行的。在当前执行上下文中调用 function会进入一个新的执行上下文。该function调用结束后会返回到原来的执行上下文中。如果function在调用过程中抛出异常,并且没有将其捕获,有可能从多个执行上下文中退出。在function调用过程中,也可能调用其他的function,从而进入新的执行上下文,由此形成一个执行上下文栈。
每个执行上下文都与一个作用域链(scope chain)关联起来。该作用域链用来在function执行时求出标识符(identifier)的值。该链中包含多个对象,在对标识符进行求值的过程中,会从链首的对象开始,然后依次查找后面的对象,直到在某个对象中找到与标识符名称相同的属性。在每个对象中进行属性查找时,会使用该对象的prototype链。在一个执行上下文中,与其关联的作用域链只会被with语句和catch 子句影响。
在进入一个新的执行上下文时,会按顺序执行下面的操作:
(1)创建激活(activation)对象
激活对象是在进入新的执行上下文时创建出来的,并且与新的执行上下文关联起来。在初始化构造函数时,该对象包含一个名为arguments的属性。激活对象在变量初始化时也会被用到。JavaScript代码不能直接访问该对象,但可以访问该对象的成员(如 arguments)。
(2)创建作用域链
接下来的操作是创建作用域链。每个 function 都有一个内部属性[[scope]],它的值是一个包含多个对象的链。该属性的具体值与 function 的创建方式和在代码中的位置有很大关系(见本建议后面介绍的“function 对象的创建方式”内容)。此时的主要操作是将上一步创建的激活对象添加到 function 的[[scope]]属性对应的链的前面。
(3)变量初始化
这一步对function中需要使用的变量进行初始化。初始化时使用的对象是创建激活对象过程中所创建的激活对象,不过此时称做变量对象。会被初始化的变量包括 function 调用时的实际参数、内部function和局部变量。在这一步中,对于局部变量,只是在变量对象中创建了同名的属性,其属性值为undefined,只有在 function 执行过程中才会被真正赋值。全局JavaScript代码是在全局执行上下文中运行的,该上下文的作用域链只包含一个全局对象。
函数总是在自己的上下文环境中运行,如读/写局部变量、函数参数,以及运行内部逻辑结构等。在创建上下文环境的过程中,JavaScript会遵循一定的运行规则,并按照代码顺序完成一系列操作。这个操作过程如下:
第1步,根据调用时传递的参数创建调用对象。
第2步,创建参数对象,存储参数变量。
第3步,创建对象属性,存储函数定义的局部变量。
第4步,把调用对象放在作用域链的头部,以便检索。
第5步,执行函数结构体内语句。
第6步,返回函数返回值。
针对上面的操作过程,下面进行详细描述。
首先,在函数上下文环境中创建一个调用对象。调用对象与上下文环境是两个不同的概念,也是另一种运行机制。对象可以定义和访问自己的属性或方法,不过这里的对象不是完整意义上的对象,它没有原型,并且不能够被引用,这与Arguments对象的arguments[]数组不是真正意义上的数组一样。
调用对象会根据传递的参数创建自己的Arguments对象,这是一个结构类似数组的对象,该对象内部存储着调用函数时所传递的参数。接着,创建名为arguments的属性,该属性引用刚创建的Arguments对象。
然后,为上下文环境分配作用域。作用域由对象列表或对象链组成。每个函数对象都有一个内部属性(scope),这个属性值也是由对象列表或对象链组成的。 scope属性值构成了函数调用上下文环境的作用域,同时,调用对象被添加到作用域链的头部,即该对象列表的顶部(作用域链的前端)。
实际上,这个头部是针对该函数的作用域链而言的,把调用对象添加到作用域的头部就是把调用对象排在函数作用域链的最上面。例如,在下面这个示例中,当调用函数e()时,将创建函数e()的调用对象和函数e()的作用域,但在调用函数e()之前,会先调用函数g(),并且生成调用函数g()的对象。而调用函数e()的对象会在函数e()的作用域范围内处于头部位置,即排在最前面。代码如下:
function f(){

return e();
function e(){
    return g();
    function g(){
        return 1;
    }
}

}
alert(f()); // 1
接着,正式执行函数体内代码,此时JavaScript会对函数体内创建的变量执行变量实例化操作(即转换为调用对象的属性)。下面进行具体说明。
将函数的形参也创建为调用对象的命名属性,如果调用函数时传递的参数与形参一致,则将相应参数的值赋给这些命名属性,否则会将命名属性赋值为undefined。
对于内部定义函数(注意其与嵌套函数的区分,两者语义不完全重合),会以其声明时所用名称为调用对象创建同名属性,对应的函数则被创建为函数对象,并将其赋值给该属性。
将在函数内部声明的所有局部变量创建为调用对象的命名属性。注意,在执行函数体内的代码并计算相应的赋值表达式之前不会对局部变量进行真正的实例化。
由于arguments属性与函数局部变量对应的命名属性都属于同一个调用对象,因此可以将arguments 作为函数的局部变量来看待。
最后,创建this对象并对其进行赋值。如果赋值为一个对象,则this将指向该对象引用。如果赋值为null,则this就指向全局对象。
创建全局上下文环境的过程与上面的描述稍微不同,因为全局上下文环境没有参数,所以不需要通过定义调用对象来引用这些参数。全局上下文环境会有一个作用域,即全局作用域,它的作用域链实际上只由一个对象组成,即全局对象(window)。全局上下文环境也会有变量实例化的过程,它的内部函数就是涉及大部分 JavaScript 代码的、常规的*函数声明。全局上下文环境也会使用this对象来引用全局对象。
JavaScript作用域可以细分为词法作用域和动态作用域。词法作用域又称为定义作用域,这是从静态角度来说的。在函数没有被调用之前,根据函数结构的嵌套关系来确定函数的作用域。因此词法作用域取决于源代码,通常编译器可以进行静态分析来确定每个标识符实际的引用。
动态作用域也称为执行作用域,这是从动态角度来说的。当函数被调用之后,其作用域会因为调用而发生变化,此时作用域链也会随之调整。
定义作用域就是用来说明函数在定义时存在的嵌套关系。当函数被执行时,作用域可能会发生变化。JavaScript函数运行在它们被定义的作用域中,而不是它们被执行的作用域中。
在 JavaScript 中,function 对象的创建方式有3种:function 声明、function 表达式和使用 Function 构造器。
function a() {}
var a = function() {}
var a = new Function()
通过这3种方法创建出来的 function 对象的scope属性的值有所不同,从而影响 function执行过程中的作用域链,具体说明如下:
使用function语句声明的function对象是在进入执行上下文时的变量初始化过程中创建的。该对象的scope属性的值是它被创建时的执行上下文对应的作用域链。
使用function表达式的function对象是在该表达式被执行的时候创建的。该对象的scope属性的值与使用function声明创建的对象一样。
使用Function构造器声明一个function通常有两种方式,常用格式是var funcName = new Function(p1, p2,..., pn, body),其中 p1,p2,…,pn 表示的是该function的形式参数,body是function的内容,使用该方式的function对象是在构造器被调用的时候创建的。该对象的scope属性的值总是一个只包含全局对象的作用域链。
function对象的length属性可以用来获取声明function时指定的形式参数的个数,而function对象被调用时的实际参数是通过arguments来获取的。

上一篇:【双11背后的技术】阿里视频云ApsaraVideo是怎样让4000万人同时狂欢的


下一篇:Python基础知识(3)