码迷,mamicode.com
首页 > 编程语言 > 详细

JavaScript中的作用域链原理

时间:2016-04-11 23:54:19      阅读:303      评论:0      收藏:0      [点我收藏+]

标签:

执行环境

 作用域链的形成与执行环境(Execution Environment)相关,在JavaScript当中,产生执行环境有如下3中情形:

1 进入全局环境

2 调用eval函数

3 调用function

  在一个执行环境A上可以创建执行环境B,执行环境B又可以创建执行环境C...,这一系列的执行环境构成执行环境栈,最新创建的执行环境位于栈顶(栈底永远是全局执行环境),当栈顶执行环境结束之后(与之相关的代码执行结束)就会被弹出站外,底下的执行环境就会成为新的栈顶。如下图所示:

技术分享

 

  一个执行环境由3部分组成:LexicalEnvironment,VariableEnvironment,ThisBinding。其中的ThisBinding就是this值,而LexicalEnvironment和VariableEnvironent相当于两个指针,它们指向Lexical Environment(注意不是LexicalEnvironment),Lexical Environment里面包含的就是标识符。

  Lexical Environment又由2部分组成:Environment Record和一个outer指针,其中outer指针指向当前执行环境下面执行环境的Lexical Environment,而Environment Record里面就是绑定的程序中各种标示符。

  Environment Record又可以细分为2种类型:一种叫Declaration Environment Record,另一种是Object Environment Record。这两种类型的Environment Record都记录程序中使用到的标识符,不同之处在于,Declaration Environment Record主要绑定的是变量的声明和函数的声明,而Object Environment Record会关联一个Object,Object Environment Record里面绑定的都是和这个变量相关的属性(比如with语法),这样在JavaScript代码里面访问这个变量的属性时就可以不用显示指定该Object。

  整个关系如下图所示:

技术分享

  需要注意的是,执行环境栈最底层的全局执行环境的outer指针指向null。当查找某一个标示符时,就从最顶端执行环境的Lexical Environment开始查找,如果找不到,就进入到outer指针指向的下一个Lexical Environment里面进行查找(上面的图示是为了演示,实际中outer指针指向的Lexical Environment有可能不是相邻执行环境的Lexical Environment),直到查找到该变量或者outer指针指向的是null,因此,由outer指针连接的Lexical Environment就是当前函数运行时的作用域链。

  在执行环境刚建立的过程中,LexicalEnvironment和VariableEnvironment指向的同一个Lexical Environment,但是如果在该执行环境下遇到了特殊的语法,比如with,那么LexicalEnvironment就会指向新的Lexical Record,当with运行结束,LexicalEnvironment又会指向VariableEnvironment所指的Lexical Environment,如下面代码所示:

function f() {
    var i = 1;

    with(document) {
        write("Hello");
    }

    var j = 2;
}

在执行with语句之前的执行环境如下图所示:

技术分享

执行with语句时执行环境如下图所示(注意没有创建新的执行环境,只是增加了一个Lexical Environment):

技术分享

执行with语句后执行环境如下图所示:

技术分享

 

Environment Record的构成

  不管Environment Record是Declaration Environment Record,还是Object Environment Record,都绑定的是当前执行环境下的标示符的信息,即Key-Value,Key就是标示符名,Value就是对应的值,如下图所示:

技术分享

 

进入函数形成执行环境

假设有如下代码:

function f(a, b, c) {
    var i = 1;
    var j = 2;
    

    //函数声明(function declaration)
    function inner(k, l, m) {
        var i = 1;
        var p = 2;
        var q = 3;
    }

    //函数表达式(function expression)
    var fVar = function(x, y, z) {
        var i = 1;
        var r = 2;
        var s = 3;
    }
}

当在JavaScript当中以f(1, 2, 3)调用这个函数时,就会为这个函数创建新执行环境,在执行该函数内部任何代码之前,会先将函数里面的标示符信息放到Environment Record当中。Environment Record当中的绑定标示符可以分成4部分(这4个部分的顺序在Environment Record当中固定,并且每一部分内部的顺序依据标识符出现的先后):函数参数,函数声明,arguments,变量声明,经过绑定后,函数f的执行环境如下:

技术分享

 

   参数标示符直接绑定的是传给函数的实参值;

  函数声明标示符绑定的是函数对象(这也是函数声明标示符可以先使用,后声明原因,因为在代码运行之前,函数声明标示符就已经加入到了Environment Record中,并且绑定了函数对象);

  arguments绑定的是arguments对象;

  而变量声明(包括函数表达式,也可以看成是变量声明),绑定的都是undefined,只有当代码真正运行到赋值给变量的语句,变量才会持有会变成我们赋予变量的值(这也是在变量声明之前可以使用该变量,但是变量值总是undefined的原因)。

  上图中Environment Record里面的变量i和j是函数f里面的变量,与函数声明inner以及函数表达式fVar里面的变量无关,因为此时只是对它们进行了定义,还没有进行调用。

需要注意的是:

1) Environment Record会绑定arguments标示符的条件是参数名和函数声明的标识符没有命名为arguments的;

2) 如果多个函数使用同一个标识符,Environment Record也只会有一条记录,并且该记录绑定的是最后声明的函数对象,即后声明的函数对象覆盖之前的函数对象,并且如果函数声明标识符合参数标识符重名,那么函数标识符覆盖参数标识符,即如果有如下代码:

function f(a, b, c) {
    alert(a);
    function a() {
       ...
    }
    alert(a);
}

当使用f(1, 2, 3)调用时,两处alert都会显示function a,而不是参数a。

3)如果变量声明标识符与之前参数,函数声明,arguments标识符重名,那么变量标识符不会被绑定,即如果有下面代码:

function f(a, b, c) {
    function i() {
      ...
    }

    alert(i);
    var i = 1;
    alert(i);
}

当使用f(1, 2, 3)调用时,第一处的alert会显示function i,而不是undefined值,因为变量声明i与函数i重名,不会被绑定;第二个alert会显示1,因为运行了var i =1;之后,Environment Record里面标识符i对应的值被赋予了1。

同时,如果多次声明同一个变量,Environment Record里面也只会有一条记录。

  剩下的一个问题是,在调用函数的时候,新的执行环境是如何与外层的执行环境联系到一起的,即新执行环境的outer指针是如何知道该指向哪一个执行环境的。答案就是,在定义函数时,函数对象的内部属性[[scope]]会引用定义自己时所在的Lexical Environment,当发生调用时,outer指针就指向[[scope]]所引用的Lexical Environment。假设有如下代码:

function f1() {
    var f = f2(); 
    f();  
}

function f2() {
    var i = 1;
    return function() {
            i = 2;
    }
}

当调用f2时,执行环境如下图所示:

技术分享

当调用f2结束时,执行环境如下图所示:

技术分享

f2调用结束,f2相关的执行环境从栈顶弹出,而f2执行环境引用的Lexical Environment由于有函数对象f的[[scope]]属性引用,因此不会随着f2的执行环境弹出栈顶而被垃圾回收。

当调用函数f时,执行环境如下图所示:

技术分享

从图上可以看到,从最顶端f的Lexical Environment,沿着outer指针,函数f可以访问到函数f2里面的局部变量i,这就是闭包的原理。

 

参考资料

ECMA-262

JavaScript中的作用域链原理

标签:

原文地址:http://www.cnblogs.com/chaoguo1234/p/5197093.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!