JavaScript中的作用域链原理

执行环境

 作用域链的形成与执行环境(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

时间: 2024-10-10 17:18:59

JavaScript中的作用域链原理的相关文章

JavaScript中的作用域链和闭包

JavaScript中的作用域链和闭包 2012-06-29 11:41 1878人阅读 评论(46) 收藏 举报 JavaScript中出现了一个以前没学过的概念——闭包.何为闭包?从表面理解即封闭的包,与作用域有关.所以,说闭包以前先说说作用域. 作用域(scope) 通常来说一段程序代码中使用的变量和函数并不总是可用的,限定其可用性的范围即作用域,作用域的使用提高了程序逻辑的局部性,增强程序的可靠性,减少名字冲突. 全局作用域(Global Scope) 在代码中任何地方都能访问到的对象拥

简单说说Javascript中的作用域链

    Javascript中作用域就是变量与函数的可访问范围,即作用域控制着变量与函数的可见性和生命周期.变量的作用域有全局作用域和局部作用域两种.当查找变量的时候,会先从当前上下文的变量对象中查找,如果没有找到,就会从父级执行上下文的变量对象中查找,一直找到全局上下文的变量对象,也就是全局对象.这样由多个执行上下文的变量对象构成的链表就叫做作用域链. 看几个题目: A : 1 var a = 1 2 function fn1(){ 3 function fn2(){ 4 console.lo

js中的作用域链

js中的执行环境: 所谓执行环境(有时也称环境)它是JavaScript中最为重要的一个概念.执行环境定义了变量或函数有权访问的其他数据 ,决定了它们各自的行为.而每个执行环境都有一个与之相关的变量对象,环境中定义的所有变量和函数都保存在这个对象中. js中的作用域链: 每个函数都有自己的执行环境,当代码在执行环境中执行时,就会创建变量对象的作用域链.作用域链保证了对执行环境有权访问所有变量和函数的有序访问.作用域链的前端,始终都是当前执行的代码所在的环境的变量对象,如果环境是一个函数,那么它的

图解JavaScript中的原型链

转自:http://www.jianshu.com/p/a81692ad5b5d typeof obj 和 obj instanceof Type 在JavaScript中,我们经常用typeof obj和obj instanceof Type来识别类型,那么两者的区别在哪?先来看两段代码 <!--typeof obj的方式判断--> <script>    var str = "toby";    console.log(typeof str);// stri

JavaScript中this的工作原理以及注意事项

在JavaScript中,this 的概念比较复杂.除了在面向对象编程中,this 还是随处可用的.这篇文章介绍了this 的工作原理,它会造成什么样的问题以及this 的相关例子. 要根据this 所在的位置来理解它,情况大概可以分为3种: 1.在函数中:this 通常是一个隐含的参数. 2.在函数外(顶级作用域中):在浏览器中this 指的是全局对象:在Node.js中指的是模块(module)的导出(exports). 3.传递到eval()中的字符串:如果eval()是被直接调用的,th

javascript中函数作用域之”提升“

javascript中函数作用域之变量提升 当我们在函数内部用关键字var声明一个变量的时候,此变量的作用域限制在当前函数. 提升:在一个作用域内部,不管一个变量用var声明的位置在哪里,这个变量属于当前整个作用域,并且在当前作用域的任何位置都可以访问它.在javascript中,这种行为/现象称之为"提升",即一个变量在一个作用域的任何位置用var声明,javascript引擎都会把这些用var声明的变量"移动"到当前作用域的开始处. 谈到javascript这种

【翻译】JavaScript中的作用域和声明提前

原文:http://www.adequatelygood.com/JavaScript-Scoping-and-Hoisting.html ===翻译开始=== 你知道下面的JavaScript脚本执行结果是什么吗? 1 var foo=1; 2 function bar(){ 3 if(!foo){ 4 var foo=10; 5 } 6 alert(foo); 7 } 8 bar(); 如果你对弹出的结果是"10"感到惊讶的话,那么下面这段脚本会让你晕头转向的: 1 var a=1

JavaScript中实现DI的原理(二)

JavaScript中实现DI的原理 在JavaScript中实现DI,看起来难,实际上原理很简单,它的核心技术是Function对象的toString().我们都知道,对一个函数对象执行toString(),它的返回值是函数的源码,知道了这一点,接下来就简单的:我获取了函数源码,然后我对函数的声明进行解析,伪码如下: var giveMe = function(config) { }; var registry = {}; var inject = function(func, thisFor

JavaScript中的作用域和作用域链(边学边写)[看着别人的博客纯手敲]

作用域是JavaScript最重要的概念之一,想要学好JavaScript就需要理解JavaScript作用域和作用域的工作原理.今天这篇文章对JavaScript作用域和作用域链简单的介绍,希望能帮助大家更好的学习JavaScript. JavaScript作用域 任何程序设计语言都有作用域的概念,简单的说,作用域就是变量与函数的可访问范围,即作用域控制着变量与函数的可见性和生命周期.在JavaScript中,变量的作用域有全局作用域和局部作用域两种. 1.全局作用域(Global Scope