• 执行环境、变量对象和作用域链


    执行环境又称执行上下文,英文缩写是EC(Execution Context),每当执行流转到可执行代码时,即会进入一个执行环境。在JavaScript中,执行环境分三种:

    • 全局执行环境 — 这个是最外围的代码执行环境,一旦代码被载入,引擎最先进入的就是这个环境。在浏览器中,全局环境就是window对象,一次所有全局属性和函数都是作为window对象的属性和方法创建的。全局执行环境直到应用程序退出时才会被销毁。

    •  函数执行环境 — 当执行一个函数时,JavaScript引擎进入执行环境。某个执行环境中的代码执行完之后,该环境销毁,保存在其中的所有变量和函数定义也随之销毁。

    •  Eval执行环境 — Eval的执行环境和函数调用的执行环境相同。

    活动的执行环境构成一个栈:栈的底部始终是全局环境,顶部是当前活动的执行环境。当执行流进入一个函数时,函数的环境被压入栈中。而在函数执行完之后,栈将其环境弹出,把控制权返回给之前的执行环境。

    建立一个执行环境分为两个阶段:

    1. 进入上下文阶段:发生函数调用,进入执行环境时,此时具体的函数代码还没有执行。

    2. 执行代码阶段:进行变量赋值,函数引用,以及执行其它代码。

    变量对象的英文缩写是VO(Variable Object),每一个执行环境都对应一个变量对象,这个对象存储着环境中定义的以下内容:

    1. 函数的形参  
      
    2. var声明的变量  
      
    3. 函数声明(但不包含函数表达式)  

    变量对象有两种存在方式,一种就是全局对象(用Global表示),存放着全局属性和函数,我们可以通过this关键字引用到该对象。另外一种是函数执行环境中定义的变量对象,改对象在函数的执行上下文中是不能直接访问的,被称为活动对象,英文缩写为AO(Activation Object)。

    接下来我们来看下再不同的执行环境中,变量对象是怎样初始化的?

    首先是全局环境中的变量对象,这个对象就是全局对象,全局对象是在进入任何执行环境之前就已经创建了的对象。这个对象只存在一份,它的属性在程序中的任何地方都可以访问,全局对象的生命周期终止于程序退出的那一刻。全局对象的初始化阶段,将Math、String等作为自身属性,初始化如下:

    Global = {  
       Math:{...},  
       String:{...},  
       ...  
       ...  
       window:Global // 引用自身  
    }; 

    接下来我们重点研究下函数执行环境中的变量对象,即上文提到的活动对象。活动对象是在进入函数执行环境时创建的,它通过函数的arguments属性初始化:

    AO = {  
       arguments: {...} //参数对象,包括callee, length等属性  
    };  

    理解了变量对象的初始化之后,接下来就是进入执行环境的代码部分了。上文中提到过,执行环境的建立分为两个阶段,第一个阶段就是进入上下文阶段。在该阶段,变量对象包含以下属性:

    1. 函数的所有形参:全局环境中没有形参,这里只是针对函数的执行环境而言。此时由形参名称和对应值构成变量对象的属性。如果没有传递相应的形参值,对应值为undefined。
    2. 所有的函数声明:需要注意的是这里特指函数的声明,函数表达式不算。此时有函数名和对应的函数对象构成变量对象的属性。如果变量对象已经存在同名的属性,则覆盖这个属性。
    3. 所有的变量声明:由var关键字声明的变量,由变量名和对应值组成,作为变量对象的属性。如果变量名与已经声明的形参或函数名相同,则变量声明不会干扰已经存在的这里属性。

    上文中,我们提到过变量声明提前的问题,在这里就反映为在进入上下文阶段,首先将初始化变量声明,构成变量对象的属性,此时该属性的值为undefined。例如下面的例子:

    function test(a, b){  
       console.log(a); // 10  
       console.log(b); // undefined  
       console.log(c); // undefined  
       console.log(d); // function d(){}  
       console.log(e); // undefined  
       console.log(f); //Reference error  
       var c = 10;  
       function d(){}  
       var e = function _e(){};  
       (function f(){});  
    }  
    test(10);  

    我们考虑进入到带有参数10的test函数的执行环境时,在进入上下文阶段,活动对象初始化如下:

    AO(test) = {  
       a: 10,  
       b: undefined,  
       c: undefined,  
       d: 指向函数d,  
       e: undefined  
    };  

    活动对象不包含属性f,这是因为f是一个函数表达式,而不是函数声明,函数表达式不会影响到变量对象。函数_e同样是函数表达式,但是它分配给了变量e,所以赋值语句执行后,就可以通过e访问到函数表达式_e。
    接下来进入到执行环境的第二个阶段,执行代码。在这个阶段开始时,变量对象已经拥有了属性,参考上面的例子,代码执行后变量对象被修改为:

    AO(test) = {  
       a: 10,  
       b: undefined,  
       c: 10,  
       d: 指向函数d,  
       e: 指向函数表达式_e  
    }; 

    理解了以上内容后,我们再来看一个例子:

    function test2(a){  
       console.log(a); // function a(){}  
       var a = 3;  
       console.log(a); // 3  
       function a(){};  
    }  
    test2(20);  

    上文中提到的在进入到执行上下文阶段时,变量对象会被初始化,在初始化阶段,变量声明构成的对象属性是最后被执行的,并且如果变量名和已经声明的函数名或形参同名的话,变量声明不会干扰到已经存在的属性,所以在函数执行环境的第一阶段,变量对象为:

    AO(test2) = {  
       a: 指向函数a  
    }; 

    不过,在紧接着的代码执行阶段,属性a被重新赋值为3。

    另外,需要特别指明的是变量只能通过var关键字来声明,对于类似于a=4这样的赋值语句,如果a没有通过var声明的话,相当于是创建了一个全局对象的属性,而并没有创建新的变量,它之所以可以认为是全局变量对象的属性,仅仅是因为全局对象等同于全局变量对象。参考以下代码:

    function test3(){  
       console.log(a); // undefined  
       console.log(b); // Reference error  
       var a = 3;   
       b = 4;  
    }  
    test3();

    所以在函数执行环境的第一阶段,变量对象为:

    AO(test2) = {  
       a: undefined  
    };  

    因为b不是一个变量,所以在这个阶段,根本就不存在b,只有在代码执行阶段,b才会以全局对象的属性出现。但是还未执行到这里之前,就已经出错了。另外一个需要记住的就是,通过var声明的变量不能通过delete删除,而属性则可以,所以上述例子中的a是不可以通过delete删除的,而b则可以。

    现在我们已经知道,执行环境中的数据作为属性存储在变量对象中,同时也知道,变量对象在在每次进入执行环境时创建,并初始化,在代码执行时,更新属性的值。接下来,将讨论下作用域链的概念。

    作用域链大多数时候和内部函数有关,我们可以创建内部函数,甚至可以从父函数中返回这些函数。示例代码如下:

    var x = 10;  
    function foo(){  
       var y = 20;  
       function bar(){  
          console.log(x + y);  
       }  
       return bar;  
    }  
    foo(); // 30  

    每个环境都有自己的变量对象,作用域链正是内部环境所有变量对象(包括父变量对象)的列表。此链用来在标识符解析中查找变量。作用域链本质上是一个指向变量对象的指针列表,它只引用但不实际包含变量对象。对于上面的例子,bar执行环境中的作用域链包括:bar变量对象、foo变量对象和全局变量对象。

    函数的作用域链是在函数调用时创建,包含这个函数的活动对象和[[scope]]属性。示例如下:

    活动的执行环境 = {  
       AO: 变量对象,  
       this:thisValue,  
       Scope: [变量对象列表] // 作用域链  
    };  

    其中Scope = 被调用函数的活动对象 + 被调用函数的[[scope]]属性。

    这种标识符的解析过程,与函数的生命周期有关。函数的生命周期可以分为创建和激活(调用时)两个阶段。在函数创建时,函数对象的内部存在一个[[scope]]属性,[[scope]]是所有父变量对象的层级链。[[scope]]属性在函数创建时被存储,永远不变,直到函数被销毁。函数可以不被调用,但该属性一直存在。与作用域链相比,作用域链是活动的执行环境的一个属性,而[[scope]]是函数的属性。

    参考以上例子,foo函数在进入全局环境后被创建,此时foo函数拥有了[[scope]]属性,如下图所示:

    同样的,bar函数在进入到foo函数的执行环境时被创建,此时foo函数的活动对象已经被创建,所以bar函数的[[scope]]属性如下图所示:

    然后,在函数调用激活阶段,生成的活动对象和[[scope]]属性共同组成执行环境的作用域链。也就是说将活动对象添加到 [[scope]]链表的最前端,在查找标识符时,首先从自身变量对象开始,逐渐向父变量查找。

    另外需要特别注意的是,通过构造函数创建的函数的[[scope]]属性中仅包含全局对象。

  • 相关阅读:
    操作系统——死锁相关
    Java 实现广度优先搜索和深度优先搜索
    Java 实现常见排序算法
    初次接触JQuery
    Firefox使用stylish自定义网页背景
    使用randoop自动化生成测试用例
    软件测试(五)——使用Selenium IDE进行自动化测试
    软件项目管理(二)——用jenkins持续集成、Maven、Github的使用
    云计算(一)——使用 Hadoop Mapreduce 进行数据处理
    软件测试(四)——图覆盖
  • 原文地址:https://www.cnblogs.com/vadar/p/4283281.html
Copyright © 2020-2023  润新知