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