javascript系列之核心知识点(二)
变量对象 |
变量对象是一个与执行上下文相关联的容器。它是一个和上下文密切结合的特殊对象,含有定义在上下文中的变量和函数声明.注意,函数表达式(和函数声明不同的)不包含在变量对象中。
变量对象是一个抽象的概念。理论上讲,不同的上下文类型表示使用不同的对象。例如在全局上下文,变量对象就是全局上下文自身(这就是为什么我们可以通过全局对象的属性名获取全局变量的原因)
1 var foo = 10; 3 function bar() {} // 函数声明, FD 4 (function baz() {}); // 函数表达式, FE 6 console.log( 7 this.foo == foo, // true 8 window.bar == bar // true 9 ); 11 console.log(baz); // ReferenceError, "baz" is not defined
这样全局上下文的变量对象(VO)将有以下属性:
图7全局变量对象
我们发现函数baz作为一个函数表达式不包含在变量对象中。这就是为什么当我们在函数外面获取它时有一个ReferenceError
注意这与其他语言(c/c++)不同的,在ECMAScript中仅仅只有函数产生一个新的作用域。定义在函数作用域中的变量和内部函数在函数外部是不能直接调用的,而且不会污染全局变量对象
使用eval后我们将会进入一个(eval's)执行上下文,然而,eval在全局变量对象中或者在调用者的变量对象中(例如eval被一个函数调用)使用,函数和他的变量对象是怎么的了?在函数上下文中变量对象表示为活动对象。
活动对象 |
当一个函数被一个调用者激活(调用),一个特殊的对象,所谓的活动对象就产生了。它包含了形参和特殊的arguments
对象(是形参的映射但有index-properties)。在函数上下文中,这个活动对象就当做变量对象来用。
函数的变量对象也是一个简单的变量对象,但它除了包含变量和函数声明,它还包含形参和arguments
对象,而且它叫做活动对象。看下面的例子:
1 function foo(x, y) { 2 var z = 30; 3 function bar() {} // FD 4 (function baz() {}); // FE 5 } 7 foo(10, 20);
我们就有foo函数上下文的活动对象(AO)
图8.活动对象
再一次看到函数表达式baz不包含在变量/活动对象中。这个主题所有巧妙情形(变量提升和函数声明)的完整描述在Chapter 2. Variable object.
我们进入到下一部分。众所周知,在ECMAScript中我们可以使用内部函数获取父函数的变量和全局上下文的变量。与上面讨论原型链一样,这就是所谓的作用域链。
作用域链 |
作用域链就是一个用来寻找出现在上下文代码中标示符的对象列表
规则是很简单的,和原型链相似:如果一个对象在他的作用域中(自己的变量/活动对象中)没找到,就进入到父变量对象中查找,一直下去。
在上下文中,标示符指的是:变量名,函数声明,形参等等。当一个函数在他的代码中用到变量不是局部变量(或者局部函数和形参),这样的变量叫自由变量,准确的寻找这些自由变量时,作用域就派上用场了。
一般情况下,作用域链就是所有父变量对象的列表,加上函数自身的变量/活动对象(在作用域链的最前面),然而作用域链也会包含一些其他的对象。例如,在上下文代码执行时动态添加到作用域链中的对象。比如with中的对象和catch从句。
当解析(查找)一个标示符时,作用域链从活动对象开始查找(如果在变量对象中没找到),逐渐追溯到作用域链的顶部,和原型链是一样的
1 var x=10; 2 (function foo(){ 3 var y=20; 4 (function bar(){ 5 var z=30; 6 //"x"和"y"是自由变量,在bar的作用域链中的下一个对象中寻找 7 console.log(x+y+z); 8 })(); 9 })();
我们通过隐式的__parent__属性(用来指向作用域链中的下一个对象)来推理作用域链对象的联系,这种方法可以在real Rhino code检测。这种技术也在ES5的词法环境中使用(名字叫做outer连接)
。作用域链的另外一种解释就是一个简单数组。使用__parent__我们可以用下面的图来解释这个例子(父变量对象保存在函数的[[Scope]]属性中
):
图9作用域链
在代码执行时,作用域了可能被with和catch从句对象扩展。由于这些都是简单的对象 ,他们也可能有原型(和原型链)。这一事实就是的作用域链查找变成二维的了。(1)考虑作用域链(2)每一个作用域的链接-原型链链接的深处(如果链接流程中有原型)。
1 Object.prototype.x=10; 2 var w=20; 3 var y=30; 4 //在SpiderMonkey的全局对象,例如全局上下文对象的变量对象从"object.prototype"继承,因此当我们指向未定义的全局变量x,会在原型链中找到。 5 console.log(x);//10 6 (function foo(){ 7 //'foo'的局部变量 8 var w=40; 9 var x=100; 10 //"x"会在"Object.prototype"中找到,因为{z:50}继承自它 11 with({z:50}){ 12 console.log(w,x,y,z);//40,10,30,50 13 } 14 //当"with"对象从作用域链删除后,"x"任然在"foo"上下文的变量对象中查找,"w"变量也成了局部变量 15 console.log(x,w);//100,40 16 //如何在我们浏览器宿主环境下获取全局的"w"变量 17 console.log(window.w);//20 18 })
我们有下面的结构图(在我们考虑_parent_链接之前,首先应考虑_proto_链)
图10"with-augment"作用域链
注意,不是所有的实现中,全局对象都继承自Object.prototype。上图中描述的行为(从全局上下文引用“non-defined”的变量x)在SpiderMonkey可以验证
如果所有的父变量存在,从内部函数获取父数据没有什么特别的——我们遍历作用域链解析需要的变量。然而,正如我们上面提到的,一个上下文结束后,他所有 的状态和它自身就销毁了。这时候内部函数也可能从父函数中返回了。此外,这个返回的函数可能在另外一个上下文被激活。如果一些自由变量的上下文已 经"gone"了,这种激活会怎样了?一般情况下,帮助我们解决这个问题的概念叫做闭包(词法上),在ECMAScript中和作用域链的概念直接相关 的。
闭包 |
在ECMAScript中,函数是一等对象。这也就意味着函数可以作为参数传递给其他的函数(这种情况叫做"funargs",“functional arguments”的缩写)。获取"funargs"的函数叫做higher-order函数,函数也会在其他函数中返回。那么返回函数的函数叫做function valued functions(返回函数值的函数)
函数参数问题的第一种形式就是向上查找的问题。当一个使用了上面提到的自由变量的函数从另一个函数返回时。为了能够获取父上下文的变量,即使父上下文已经终结。内部函数在创建时把它的父作用域链保存在[[Scope]]属性中。当这个函数被激活时,结合活动对象和[[Scope]]属性,这个函数上下文的作用域链就形成了。
1 Scope chain = Activation object + [[Scope]]
再次注意一件主要的事情-在创建时刻-函数保存了父作用域链。因为这个被保存的作用域链在后期的函数调用中,将会用于变量查找
function foo(){ var x=10; return function bar(){ console.log(x); }; } //"foo"返回一个函数,这个返回的函数使用了自由变量“x” var returnedFunction=foo(); //全局变量“x” var x=20; //执行已返回的函数 returnedFunction();//10,不是20
这种作用域叫做静态(词法)作用域。我们看到在返回的bar函数——保存的[[Scope]]属性
中找到了变量x。一般的思想,在上面的例子中会有一个动态的作用域保存x的变量值为20,而不是10.然而ECMAScript不存在动态作用域。
函数参数问题的第二种形式就是向下查找的问题。在这种情况下,父作用域可能是存在的,但可能是以一个模糊的标示符保存的。问题是:标示符的值应该使用哪 一个作用域——静态保存在函数创建时的作用域还是在执行时动态形参的作用域(调用者的作用域)?为了避免这种模棱两可的作用域并产生闭包,决定使用静态作 用域:
1 //global x 2 var x=10; 3 //全局函数 4 function foo(){ 5 console.log(x); 6 } 7 (function(funArg){ 8 //local x 9 var x=20; 10 //这里是不模糊的,因为我们使用的全局变量x,静态的保存在foo函数的[[Scope]]中,而不是激活函数的调用者作用域中的x 12 funArg();//10,不是20 13 })(foo);
我们可以得出这样的结论:在语言中的闭包必须需要静态作用域。然而,有些语言提供了静态和动态作用域以供编程者选择-使用闭包或者不使用。由于ECMAScript只有静态作用域(例如我们解决的两种函数参数问题)。ECMAScript使用函数的[[Scope]]属性
完整的支持闭包,下面给出闭包的准确定义:
闭包就是代码块(ECMAScript中,这个代码块是函数)和静态保存的父作用域的结合。因此通过这个保存的作用域链,函数可以很容易的找到自由变 量。注意,每一个函数在创建的时候都保存了[[Scope]],理论上,ECMAScript中所有的函数都是闭包。
还需要记住的一个事实,许多函数可能有一个相同的(这真的是一个很常规的情形,比如有两个内部/全局函数)父作用域,这种情况下储存在[[Scope]]属性的变量在所有拥有同一个父作用域链的函数是共享的。一个闭包变量的改变会影响另个闭包读取的变量:
1 function baz(){ 2 var x=1; 3 return { 4 foo:function foo(){return ++x;}, 5 bar:function bar(){return --x;} 6 }; 7 } 8 var closures=baz(); 9 console.log(closures.foo(),//2 10 closures.bar()//1)
这段代码可以用下图来阐释:
图10 共享的[[Scope]]
在循环中构建一些函数的迷惑也是和这个特点相关的。在构造的函数中使用循环计数器,一些编程者获得了一些意外的结果,当在函数中使用了同一个计数器时。现在清楚为什么了吧-因为所有的这些函数都有同样的[[Scope]],在这个
[[Scope]]中计数器是最后一次计数值。
1 var data = []; 3 for (var k = 0; k < 3; k++) { 4 data[k] = function () { 5 alert(k); 6 }; 7 } 9 data[0](); // 3, but not 0 10 data[1](); // 3, but not 1 11 data[2](); // 3, but not 2
这里有一些技巧来解决这些问题。一种技巧就是在作用域链中使用一个附加对象:
var data=[]; for(var k=0;k<3;k++){ data[k]=(function(x){ return function(){ alert(x); }; })(k) } data[0]();//0 data[1]();//1 data[2]();//2
我们进入到下一部分,考虑执行上下文的最后一个属性this。
this值 |
this是和执行上下文相关的特殊对象。因此他可以命名为上下文对象(上下文执行时激活的对象)
任何对象都可以作为上下文的this值。我需要再次阐明一些与上下文相关的描述,特别是this值,它经常错误的描述为变量对象的属性。请再次记住:this是执行上下文的属性,而不是变量对象的属性
这个特性是很重要的,因为和变量不同,this从不参与标示符的解析过程。例如,在代码中的执行上下文直接获取this而不需要查找作用域链。this值仅仅决定于进入的上下文。
在全局上下文中,this是全局对象本身(也就是说this等于全局对象):
1 var x = 10; 3 console.log( 4 x, // 10 5 this.x, // 10 6 window.x // 10 7 );
在函数上下文中,this值在每个函数调用中都可能不同的。这里的this由调用者通过调用表达式提供(函数的调用方式)。例如下文中foo函数是被调用者,被全局上下文调用。我们看下面的例子,对同一段函数代码。不同的调用方式提供不同的this值
1 //foo 函数的代码是不变的。但在每一次激活时,this都是不一样的 2 function foo(){ 3 alert(this); 4 } 5 //调用者激活了foo函数并提供this值 6 foo();//全局环境下的变量对象 7 foo.prototype.constructor();// foo.prototype 8 var bar={ 9 baz:foo 10 }; 11 bar,baz();//bar 12 (bar.baz)(); // also bar 13 (bar.baz = bar.baz)(); // but here is global object 14 (bar.baz, bar.baz)(); // also global object 15 (false || bar.baz)(); // also global object 16 var otherFoo = bar.baz; 17 otherFoo(); // again global object