原文:http://www.nczonline.net/blog/2010/01/26/answering-baranovskiys-javascript-quiz/
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------
上周我在Dmitry Baranovskiy的一篇博客里看到一些关于JavaScript的测验题,博客标题叫"你觉得你真的懂JavaScript吗"。像这种类型的题目,你只要回答一个问题:结果是什么?这些例子用来测试JavaScript引擎一些诡异的特性行为。我以前见过这种类型的测验题,有些人会拿来当做面试题,我认为这不仅不尊重面试者,而且毫无用处。你不会每天都遇到这种类型的怪异问题,所以用这样的问题来考察面试者,当做是最低的指标,就跟面试空姐时叫人家解释飞机飞行原理一样没用。
不过我仍然喜欢这些例子,它们可以很好的用来解释JavaScript语言的有趣现象,下面我会深入分析每个测验题是怎么执行的。
测验1
1 if(!(‘a‘ in window)){ 2 var a=1; 3 } 4 alert(a);
这段看起来很怪的代码好像是说:"如果window对象没有属性‘a‘,那么就定义一个给它,并把它的值设置为1"。你可能会以为弹出的结果是1,事实上结果是‘undefined‘,要了解原因的话,你需要知道关于JavaScript的三件事。
首先,所有全局变量都是window对象的一个属性,你写‘var a=1;‘相当于写‘window.a=1;‘,你可以用下面的方法来检测一个全局变量是否声明了:
1 ‘variable-name‘ in window
其二,所有变量声明都会提前到当前作用域的顶端,请看下面的代码:
1 alert(‘a‘ in window); 2 var a;
上面的结果是‘true‘,虽然变量声明语句在检测语句之后,但是因为JavaScript引擎首先会扫描变量声明,然后把它们移动当前作用域的顶端,所以最终解析出来的代码如下:
1 var a; 2 alert(‘a‘ in window);
看这段代码,更容易理解为什么结果是‘true‘。
第三,你需要知道,虽然变量声明提前了,但是赋值操作却没有。下面的语句是变量声明和赋值在一起的:
1 var a=1;
你可以把它分为声明和赋值两部分:
1 var a;//声明 2 a=1; //赋值
当JavaScript引擎发现声明和赋值是一起时,就会自动把它们分开,然后把声明提前。为什么赋值不提前呢?因为在代码执行的时候这会影响变量的值并且产生不可预期的结果。
现在了解了JavaScript的这些特性,重新看下案例1的代码,实际上被解析成了如下代码:
1 var a; 2 if(!(‘a‘ in window)){ 3 a=1; 4 } 5 alert(a);
看这段代码后,答案就显而易见了。首先声明变量‘a‘,接着是if语句,判断说:"如果‘a‘没有声明,那么就初始化为1"。当然了,条件判断不可能为‘true‘,所以变量的值还是默认值‘undefined‘。
测验2
1 var a=1, 2 b=function a(x){ 3 x&&a(--x); 4 }; 5 alert(a);
这段代码比表面上看起来更复杂,以上结果为‘1‘,就是初始化的值。但是为什么会这样?这仍然需要了解JavaScript的三个要点。
第一点就是声明提前,在案例1已经讲过了。
第二点,函数声明提前。所有的函数声明跟变量声明一样,都会被提前至当前作用域的顶端,但是要明白函数声明是这样子的:
1 function functionName(arg1,arg2){ 2 //function body 3 }
以下不是函数声明,而是函数表达式,相当于变量赋值:
1 var functionName=function(arg1,arg2){ 2 //function body 3 }
要清楚,函数表达式是没有被提前的。这一点现在要明白,因为变量的初始化和赋值位置的改变,都会显著的影响程序的执行。
第三点,函数声明的优先级高于变量声明,但是不会覆盖变量的赋值。为了理解这句话的意思,请看下面的代码:
1 function value(){ 2 return 1; 3 } 4 var value; 5 alert(typeof value); //‘function‘
即使变量声明在函数声明之后,但变量‘value‘的值依然是一个‘function‘,在这种情况下,函数声明的优先级更高。但是,如果变量初始化了,那么结果又会不同。
1 function value(){ 2 return 1; 3 } 4 var value=1; 5 alert(typeof value); //number
现在变量的值被设置为1,变量的初始化会覆盖函数声明。
回到测验2的代码来,那个函数是一个有名字的函数表达式。带有名字的函数表达式不是函数声明,所以不会覆盖变量声明。然而,你要注意变量b的值是函数表达式,而函数表达式的函数名是a,不同的浏览器处理方法不同。IE浏览器会把它当做是函数声明,所以会被变量的初始化覆盖,那么调用a(--x)就会造成错误。至于其它浏览器,虽然a在函数外还是一个数字,但在函数内还是可以调用a(--x)。基本上,在IE中调用b(2)会抛出JavaScript异常,但在其它浏览器会返回‘undefined‘。
理解上述内容之后,把题目换成更准确和更容易理解的版本:
1 var a=1, 2 b=function(x){ 3 x&&b(--x); 4 }; 5 alert(a);
这样的话,就很清楚了,变量a的值总是1。
测验3
1 function a(x){ 2 return x*2; 3 } 4 var a; 5 alert(a);
如果你理解了上面那个例子,那么这道题就很简单了。有一点要记住的就是函数声明总是优先于变量声明,也就是说变量声明语句被忽略了,除非遇到变量初始化。这里没有初始化,所以弹出结果是函数a的源代码。
同名的变量不会重复声明。
测验4
function b(x,y,a){ arguments[2]=10; alert(a); } b(1,2,3);
这段代码更好理解,只要判断结果是‘3‘或者‘10‘就可以了,所有的浏览器结果都是10。只要知道一个概念你就知道为什么结果是这样了,就是ECMA-262第三版10.1.8章节关于arguments对象的描述:
1 For each non-negative integer, arg, less than the value of the length property, a property is created with name ToString(arg) and property attributes { DontEnum }. The initial value of this property is the value of the corresponding actual parameter supplied by the caller. The first actual parameter value corresponds to arg = 0, the second to arg = 1, and so on. In the case when arg is less than the number of formal parameters for the Function object, this property shares its value with the corresponding property of the activation object. This means that changing this property changes the corresponding property of the activation object and vice versa. 2 太TM难理解了:对于每个非负整数,传进来的参数个数比‘length‘属性值还小,那么就会使用ToString(arg)创建一个属性和属性变量‘{DontEnum}‘。这个属性的初始值为调用者传进来的参数个数。第一个实参对应arg=0,第二个对应arg=1,依次类推。当实参的个数小于形参时,这个属性就会和激活的对象共享值。这意味着,改变属性的值,相应的激活对象的属性也会改变,反过来也一样。
简而言之,arguments对象中的每个实体都是形参的一个拷贝,虽然值是共享了,但是内存空间没有共享。两个内存空间由JavaScript引擎来保存同步,也就是说arguments[2]和a总是拥有相同的值,所以最终a的值是10。
测验5
1 function a(){ 2 alert(this); 3 } 4 a.call(null);
我认为这是5道题中最简单的了,只需要理解两个概念。
首先,你要知道this的值是怎么定义的,当一个方法被对象调用时,this就指向该对象了,例如:
1 var object={} 2 method:function (){ 3 alert(this==object) //true 4 } 5 ; 6 object.method();
在这段代码里,当object.method()被调用的时候,this就指向了object。在全局作用域,this相当于window(也就是全局对象),所以如果一个function的定义不是属于一个对象属性的时候(也就是单独定义的函数),函数内部的this等于window,例如:
1 function method(){ 2 alert(this===window); //true 3 } 4 method();
在这里,this指向的是全局对象window。
了解了上述概念之后,我们再来了解一下call()方法是干什么的。call方法可以让一个方法当做是其它对象的方法被调用,第一个参数是调用者,其它参数是被调用的方法的实参,例如:
1 function method(){ 2 alert(this===window); 3 } 4 method(); //true 5 method.call(document); //false
这里因为method()方法被document调用,所以结果是‘false‘。
一个有趣的问题是当传入call方法的一个参数是null会发生什么,以下是ECMA-262第三版对这个问题的描述:
1 如果传入的第一个参数是‘null‘或‘undefined‘,那么call方法会把全局对象(window)作为this的值,否则this的值就等于ToObject(thisArg)。
所以无论什么时候call()和apply()方法第一个参数传入null,其this的值都是window。根据以上知识,将案例5代码重新改为如下更容易理解:
1 function a(){ 2 alert(this); 3 } 4 a.call(window);
很明显结果就是[object Window]。
总结
Dmitry把这些有趣的测验题放一起,你可以从中学习JavaScript的一些怪异特性,我希望这篇文章可以帮助每个人理解这些例子运行时所必须知道的细节,更重要的是,理解为什么会这样子。重申一下,我反对使用这类测验题当做面试题来考验面试者的能力,我认为这在实际中是没有多大用处的(如果你想知道我是如何面试前端工程师的,请看我前一篇文章)。