DS.Lab筆記

来源:互联网 发布:python 二叉树遍历 编辑:程序博客网 时间:2024/06/02 19:41

原文链接:ECMA-262-3 in detail. Chapter 4. Scope chain.



=============================================================


与作用域链(scope chain)关联最大的是内函数(inner function),也就是函数体里定义的函数。前面有一章也已经讲过执行上下文(execution context)了,并且执行上下文可以嵌套。那么,简单来讲,作用域链就是指,最里面的函数的执行上下文上面所保存的它的所有上层执行上下文里面的变量体(variables object)或对函数而言就是触发体(activation object),这样就可以在解析变量时查找变量。比如下面的代码:

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

最里面的函数bar,它的执行上下文里会保存一个作用域链,其包括了bar自己的上下文里的触发体,foo的触发体以及全局的变量体。


那么,加上前面讲过的变量体和this值,现在执行上下文的结构又更新了:

activeExecutionContext = {    VO: {...}, // or AO    this: thisValue,    Scope: [ // Scope chain      // list of all variable objects      // for identifiers lookup    ] };

首先作者打算把作用域链的内容表达成一个数组,就像:

var Scope = [VO1, VO2, ..., VOn];

然后作者指出,在ECMA规范里没有指定这个链的实现细节,不过在实际的实现中,一般来说这个数组里的元素是相互链接的,假如说最前面的是当前的变量体,它会有一个属性指向它后面的那个变量体,也就是它的上一级上下文里的变量体,如此一环一环向上链接下去,这么说来的话,如果我们讲一段代码里的所有的作用域都画在一个图里表示,会得到一个树形结构。而单就某个函数上下文里的作用域而言,它的内容更像是一个单链表。


另外,函数对象有一个普通对象没有的内部属性,就是[[Scope]],从定义来说,一个函数上下文上面的Scope应该是当前上下文里的触发体加上当前函数的[[Scope]]的内容。

Scope = AO + [[Scope]]




=============================================================


函数的生命周期(Function life cycle)

有两步,分别是创建调用执行


=============================================================


函数的创建

这里只有一个重点,就是在创建函数的时候,函数对象上的[[Scope]]内部属性被确定,它的内容是这个函数的上级所有上下文里的变量体,一直到全局上下文的变量体为止。函数对象一旦被创建后,这个值就不会被改变了,也会一直保存到这个函数对象被销毁。

作者的例子是:

var x = 10;function foo() {  var y = 20;  alert(x + y);}foo(); // 30


foo函数对象是在全局作用域里被声明的,它在被创建时,它的[[Scope]]就是:

foo.[[Scope]] = [  globalContext.VO // === Global];


疑问:作者暗示了这个分析只针对函数声明有效,或许其它形式的创建函数会有所不同,这个问题留待观察。


函数的调用执行

这个时候,就到了foo的执行上下文的处理了,前面某章讲过了上下文的处理分两步,第一步是进入上下文,这个步骤里,scope和另外两个属性会被一起创建,就是前面讲过的变量体和this。

scope的值就是把函数对象上的[[Scope]]的值拿过来,在前面插上当前的触发体(注意:总是前面,这点很重要),就成了

Scope = [AO].concat([[Scope]]);


现在讲讲标识符解析(identifier resolution)的过程吧,它的工作原理就是基于作用域链的。前一章讲过了一种ECMA的内部类型叫引用类型(Reference),一个函数体里的一个变量就是个引用类型的标识符,简单讲,这个解析的过程就是从当前沿着作用域链向上,在一个接着一个变量体上寻找名字匹配的属性,如果找到了,就立刻返回,终止查找(所以前面才会强调总是把当前触发体插在最前面很重要,这样它的优先级就最高,总是先在当下的触发体上面查找)。


来看个例子吧:

var x = 10;  function foo() {    var y = 20;    function bar() {    var z = 30;    alert(x +  y + z);  }    bar();}  foo(); // 60


在这段代码里,全局上下文的变量体的情况是:

globalContext.VO === Global = {  x: 10  foo: <reference to function>};


在foo函数对象被创建的时候,它上面的[[Scope]]属性是:

foo.[[Scope]] = [  globalContext.VO];


接下来就是调用执行foo的时候了,这个时候在进入它的执行上下文阶段时,触发体属性被创建,它的情况是:

fooContext.AO = {  y: 20,  bar: <reference to function>};


然后上下文里的Scope属性也被创建,它等于触发体加上foo对象上的[[Scope]]:

fooContext.Scope = fooContext.AO + foo.[[Scope]]


这样就变成:

fooContext.Scope = [  fooContext.AO,  globalContext.VO];


然后进入bar函数对象的创建过程,这个时候它的[[Scope]]属性被创建,其值为(也就是它父上下文的Scope):

bar.[[Scope]] = [  fooContext.AO,  globalContext.VO];


然后bar函数被调用,在进入上下文阶段,它的触发体又会是:

barContext.AO = {  z: 30};


然后bar的执行上下文的Scope就变成:

barContext.Scope = barContext.AO + bar.[[Scope]]


也就是:

barContext.Scope = [  barContext.AO,  fooContext.AO,  globalContext.VO];


在bar函数体里面分别引用了x,y和z,它们的解析过程是:

- "x"
-- barContext.AO // not found
-- fooContext.AO // not found
-- globalContext.VO // found - 10


- "y"
-- barContext.AO // not found
-- fooContext.AO // found - 20


- "z"
-- barContext.AO // found - 30



=============================================================


作用域的特性(Scope features)

下面说一些跟作用域链和函数对象上的[[Scope]]属性有关的语言特性。


闭包(Closures)

概念上讲,闭包等于一个函数对象的[[Scope]]属性和函数自身的代码的组合。所以闭包包括了函数在被创建的上下文的所有上级执行上下文上面的变量体,这个概念被称为:文法语境(lexical environment)。这个语境将会在解析标识符的时候用来寻找相应变量。


下面有两段经典代码演示这个现象(注意:在Chrome (Version 56.0.2924.87)和Firefox (51.0.1 (32-bit))中測試的結果都相同):

var x = 10;function foo() {  alert(x);}(function () {  var x = 20;  foo(); // 10, but not 20})();

function foo() {   var x = 10;  var y = 20;   return function () {    alert([x, y]);  }; } var x = 30; var bar = foo(); // anonymous function is returned bar(); // [10, 20]


作者对这个现象总结两点:

  • [[Scope]]属性的值是在函数被创建的时候决定的,比如例二里,匿名函数是在foo里面被创建的,它的作用域链就是在那时被决定的,注意它没有因为它的执行方式而变化;
  • 这个作用域链里的内容在它被创建的上下文退出后可以依然存在,比如foo函数的执行上下文里的变量体,在foo返回退出后依然存在,可以被bar访问。

疑问:函数声明的情况下很简单,可是在函数表达式的情况下,函数创建究竟发生在什么时候,如果发生在上下文处理的代码执行阶段,那么有没有某些情况下,函数的作用域链会能够被动态决定,比如通过控制逻辑???

答:这个问题在下一章:函数,里面基本有解释,表达式函数是在执行阶段被创建的,并且不会放在变量体上面。而且重点是这种情况下它们的作用域链的创建和函数声明的原理是没有区别的。参见命名函数表达式与SpiderMonkey一段,那里就介绍了IIFE里面的作用域链的算法。


经过函数构造器创建的函数对象的[[Scope]]属性([[Scope]] of functions created via Function constructor)

上面讲到的创建[[Scope]]的规则有一个特例,就是通过函数构造器来创建函数的时候。在这种情况下,被创建的函数对象的[[Scope]]里只有全局对象,如:

var x = 10;function foo() {  var y = 20;  function barFD() { // FunctionDeclaration    alert(x);    alert(y);  }  var barFE = function () { // FunctionExpression    alert(x);    alert(y);  };  var barFn = Function('alert(x); alert(y);');  barFD(); // 10, 20  barFE(); // 10, 20  barFn(); // 10, "y" is not defined}foo();

注意:在Chrome (Version 56.0.2924.87)和Firefox (51.0.1 (32-bit))中測試的結果都相同。


双重维度的作用域链查找(Two-dimensional Scope chain lookup)

其实这个所谓的两个维度,指的是作用域链和原型链,作用域链里面包含的是一个接一个函数触发体,最后到全局对象(因为它就是全局上下文的变量体),而全局对象是有原型属性的,它的原型属性指向Object,所以,如果变量解析在遍历了作用域链之后都没有找到变量,那么会继续沿着全局对象的原型链寻找


下面有两个例子:

function foo() {  alert(x);}  Object.prototype.x = 10;  foo(); // 10


function foo() {    var x = 20;    function bar() {    alert(x);  }    bar();}  Object.prototype.x = 10;  foo(); // 20


但是,也有特殊的情况,在这些情况下,作用域链上面会有除了最尾端的全局对象以外的对象有原型链,比如:

  • 有些实现里,函数触发体也是有原型属性的,指向Object,比如Blackberry的一些版本;
  • 对于命名函数表达式(named function expressions),一个特殊对象会被插入到作用域链里,而它是有原型属性的,所以它所在的原型链会被查找。


另外值得注意的是,这些情况下被创建的对象都是直接从Object那里继承原型链,所以原型链上通常只有一个对象,就是Object。因此,除非你曾经直接修改了Object.prototype,不然也不会有什么直接影响


全局上下文和eval上下文里的作用域链(Scope chain of the global and eval contexts)

全局上下文里的作用域链就只有全局对象自身。

eval上下文里的作用域链和它的调用上下文上的作用域链相同。


在代码执行阶段对作用域链的影响(Affecting on Scope chain during code execution)

两个语句会造成影响:with和catch。影响的方式相同,都是在作用域链前端插入一个新对象


with的做法就是:

Scope = withObject + AO|VO + [[Scope]]


像下面这段代码:

var foo = {x: 10, y: 20};  with (foo) {  alert(x); // 10  alert(y); // 20}


被插入的对象就是foo:

Scope = foo + AO|VO + [[Scope]]


下面来看一个非常有趣的例子:

var x = 10, y = 10;with ({x: 20}) {  var x = 30, y = 30;    alert(x); // 30  alert(y); // 30}alert(x); // 10alert(y); // 30


  • 首先,再次重复,JS没有块作用域(block scope),所以在上下文处理过程中的第一个阶段,进入上下文阶段里,x和y的声明都被提升并放在变量体上面了,于是with代码块里面的x和y的前面的var没有什么作用,它们都变成了简单的赋值操作,就是x=30, y=30;
  • 接下来with语句把它的参数对象(即{x: 20})插入到with代码块的上下文的作用域链的最前端;
  • 当执行到with代码块里的x=30时,引擎从作用域链里寻找x,找到的是第一个对象上面的x,也就被新插入的那个对象,于是就把它的值从20改成30了;
  • 当执行到with代码块里的y=30时,引擎从作用域链里寻找y,结果是在最外面的全局上下文的变量体上找到了y,于是将它的值从10改成了30;
  • 输出x和y的值的时候寻找到的变量和上述修改的一样,所以是30和30;
  • 退出with代码块,它的执行结束了,前面被插入的对象也被移走了;
  • 在with代码块结束后访问x,因为新插入的对象已经不在作用域链里了,所以最终是在全局上下文的变量体上找到了x,可是刚刚修改的x是新插入的对象上的属性,不是这个全局对象上的,所以全局对象上的x属性还是10,于是输出的就是10;
  • 接下来又访问y,同上面的操作一样,在全局对象上找到y,可是它的值在with代码块里被修改成30了,于是输出30。


catch的做法则是:

Scope = catchObject + AO|VO + [[Scope]]


像下面这段代码:

try {  ...} catch (ex) {  alert(ex);}


它实际上在进入catch块时,会自动创建一个对象:

var catchObject = {  ex: <exception object>};


于是被插入的对象就是它,新的作用域链就变成:

Scope = catchObject + AO|VO + [[Scope]]


=============================================================


后记

其实我读完所有的八篇ES3系列的文章后,一时没法完全消化,在看到一些简单的代码后反而会判断出错,比如一个读者在评论里提出的一段代码:

function d() {   var a=5; // local   function e() {      a=4; // global      return (function(){alert(a);});    }   return e;} d()()();  //4alert(a); //nothing


他认为函数e里面的赋值操作因为没有var关键字,应该结果是在全局对象上创建一个a属性,而d函数里的变量a在作用域链里的位置近一些,所以他期望的是d()()()会输出5,在乍一看这段代码,并且刚刚读完这系列的文章后,我也顺着他的思路想错了。反而忘记了平时已经习以为常的逻辑,这种情况下,e函数里面的a当然是会被解读为d里面的a,所以不会有全局变量任何事。


下面的代码更是令我想了一晚上:

var x = 10; function foo() {  alert(x);} (function () {  var x = 20;  foo(); // 10, but not 20})();


我起初会以为,在执行IIFE里的匿名函数时,创建它的执行上下文,创建上下文里的触发体,触发体上包含x,其值为20,而这个触发体是将会被插入到foo()执行时的作用域链前面的,所以我觉得输出理应是20。还有下面一段来自《YDTKJS: Scope & Closures》附录里的一段代码,我也产生同样的疑问:

function foo() {    console.log( a ); // 2  }    function bar() {    var a = 3;    foo();  }    var a = 2;  bar();


以前一段代码为例,其实这个插入的触发体指的是在foo执行的时候它自己的上下文里的触发体,这个触发体会包含的是foo的函数体里定义的东西,foo里面什么都没有定义,只有一行console.log,所以它的触发体是空的,所以才会找不到x,所以就沿着作用域链向上寻找,于是就到了全局对象,就找到了它的x属性,是10。


上面第二个例子也是如此,foo函数本身的函数体里没有定义什么东西,所以foo在bar里面被执行时,它的上下文里的触发体也是空的。

0 0
原创粉丝点击