Answering Baranovskiy’s JavaScript quiz


這是 Nicholas C. Zakas 回答先前 Dmitry Baranovskiy 在他的 blog 上出的五道檢視你是不是真的瞭解 javascript 的小測驗。主要就是在 javascript 的行為和 scope/closure 的觀念下出五道題,並希望大家能在不去 console 跑結果的前提下看看自己是不是真的知道會 alert 出些什麼東西。我覺得很不錯所以將他的原文在這邊翻譯一份中文版。本文開始:

我上週在 Dmitry Baranovskiy 的 blog 上看到了一個 javascript 的小測驗。「你覺得你真的懂 JavaScript 嗎?」這種類型的題目都只問你一個問題:這會 alert 什麼東西出來?這些 code 是用來測試一些 JavaScript 引擎(vm)眉眉角角的屬性和行為。我之前也曾經看過類似的問題,有些人偶爾會拿去當作面試的小測驗。我認為這樣的動作對應徵者非但不太尊重,而且事實上也沒什麼用。因為你不會每天都遇到這樣的問題,所以應該讓這樣的測試成為面試評估的最小考量,那就跟請一個飛行員去解釋飛機飛行的原理一樣無用。

不過我仍然蠻喜歡其中一些程式碼的,因為他能用來解釋一些 JavaScript 語言有趣的現象。以下是對這些範例的深入解析:

Example #1

if (!("a" in window)) {
var a = 1;
}
alert(a);


這看起來很怪的 code 似乎是在說:「如果 window 沒有 “a” 這個 property,那麼就定義一個 “a” 給他,並把 1 這個值指給 a。」然後你會覺得他應該會 alert 出數字 1 來。但事實上,這只會 alert 出一個 “undefined”。要瞭解原因的話,你必須先知道關於 JavaScript 的三件事。

第一,所有全域變數都是 window 的 property。你寫 var a = 1 事實上跟 window.a = 1 是一樣的。你可以使用下述的方式來檢視一個全域變數是不是已經被宣告過了:

"variable-name" in window

第二,所有的變數宣告都會直接被拉( hoisted )到同 scope 中的最前面執行(前置)。我們來看看一個簡單的範例:

alert("a" in window);
var a;

這次 alert 的結果是 “true”,即便他的變數宣告是在這行 alert script 之後也一樣。這是因為 JavaScript 引擎會先看有沒有變數的宣告,有的話就把他們拉到最上面去 run。這個引擎其實是這樣來跑這些 code 的:

var a;
alert("a" in window);

這樣的 code 應該更能說明為什麼會 alert 出 “true” 了。

要搞懂這個 example 你必須瞭解的第三件事就是,只有變數宣告會被前置,初始化動作並沒有。這行 code 同時做了宣告和初始化兩個動作:

var a = 1;

你可以像這樣將宣告和初始化拆成兩步:

var a;    //宣告
a = 1;    //初始化

當 JavaScript 引擎要處理一個把宣告和初始化綁在一起的動作時,會自動把這樣的動作拆成兩個步驟,這樣一來宣告就能被拉到 scope 的最前方來執行。那為什麼初始化不也一起往前拉呢?因為這樣的話會直接影響到一個變數的值,接著就會導致無法預期的結果。

所以,瞭解 JavaScript 的這三個層面,重新檢視原本的 code。它實際上就是如以下的 code 這樣執行的:

var a;
if (!("a" in window)) {
a = 1;
}
alert(a);

看著這個版本的 code 就會讓結果更直觀了。變數是先被宣告,然後 if 判斷式會處理:「如果 a 沒有被宣告過,那麼就初始他的值為 1」,當然,這樣的條件式結果永遠不會是 true,所以這個變數就會維持他原本的值:”undefined”。

Example #2

var a = 1,
b = function a(x) {
x && a(x);
};
alert(a);

這份 code 事實上比他看起來的樣子複雜的多了。這次的結果會 alert 出 “1″ 來,被初始化過的值。但是為什麼呢?這次必須仰賴 JavaScript 三個面向必要的知識:

第一個觀念是變數宣告的前置( 往前拉到 scope 的最前端 ),example #1 也有用到的這點。第二個觀念是 function 宣告的前置。所有 function 的宣告都會跟變數宣告一起被拉到所屬 scope 的最前面。為了更清楚些,一個 function 的宣告會長這樣:

function functionName(arg1, arg2){
//function body
}

這就跟 function expression 產生衝突了,因為是一個變數指派的動作:

var functionName = function(arg1, arg2){
//function body
};

說得明白點,function expression 並沒有被前置。這應該會讓你更清楚,當變數在初始化的時候,如果你將值的指派從一個地方移到另外一個地方,將會徹底的影響並改變他的執行結果。

第三,你必須知道,從這個例子中你會似懂非懂的是, function 的宣告 override 了變數的宣告,而非變數的初始化。讓我們來看看下面這個例子:

function value(){
return 1;
}
var value;
alert(typeof value);    //"function"

儘管變數的宣告在 function 的宣告之後,value 這個變數仍然會是一個 function。其 function 的宣告在這個狀況中是有優先權的。無論如何,加上變數初始化後你會得到一個不同的結果:

function value(){
return 1;
}
var value = 1;
alert(typeof value);    //"number"

現在變數的值已經變成 1 了。變數的初始化 override 了 function 的宣告。

回到我們的 example code,不管他的 name 的話,這個 function 其實是一個 function expression。被命名後的 function expression 將不被視為 function 的宣告,因此並不會被變數宣告 override。無論如何,你將會注意到,當 function expression 為 a 的時候,一個含有 function expression 的變數卻是 b。瀏覽器在處理這個 a 的方式各有不同。IE 視他為一個 function 的宣告,所以他會被變數宣告 override 掉,這表示他接下來 a(–x) 的動作會產生 error。其他的瀏覽器就會允許 function 中 a(–x) 的動作,而在 function 外的 a 仍然是一個數字。基本上,在 IE 裡面 call b(2) 會出現 error,而在其他的瀏覽器則會回傳一個 “undefined”。

總之,code 像這樣寫會好懂的多:

var a = 1,
b = function(x) {
x && b(x);
};
alert(a);

現在這樣寫就能清楚的知道 a 將會永遠都是 1。

Example #3

function a(x) {
return x * 2;
}
var a;
alert(a);

如果你能完全理解上面的那些例子,那麼這個將顯得相當簡單。你唯一需要瞭解的是,除非經過初始化,否則 function 的宣告會 override 變數的宣告。這裡沒有任何初始化的動作,所以這次將會 alert 出這個 function 的原始碼。

Example #4

function b(x, y, a) {
arguments[2] = 10;
alert(a);
}
b(1, 2, 3);

這份 code 會比較好理解,因為唯一需要回答的問題是,究竟會 alert 出 3 還是 10?無論在哪個瀏覽器下,結果都會是 10。要搞懂這部份只要瞭解一個觀念。ECMA-262,第三版,section 10.1.8 說明了一個 argument objet:

對所有非負整數 “arg”,在(參數)小於 length 的情況下,一個 name 是 ToString(arg) 和 property attribute 是 { DontaEnum } 的 property 會被產生。這個 property 的初始值會是 caller 提供的對應參數。第一個對應的參數值對應到 arg = 0,第二個則是 arg = 1,以此類推。這樣的情況下,當 arg 比 function 中所有的參數數量還少時,此 property 會與 activation object 擁有共同的值。這表示如果改變這個 property 也會改變 activation object 相對應的值與 vice versa。

簡單的說,每個在 arguments 物件中的個體會是每個被命名 argument 的 copy。請注意,雖然值是共用的,但是記憶體空間卻不然。這兩個記憶體空間會由 JavaScript 引擎來保持同步,這表示 arguments[2] 和 a 都同時共有一個值。所以這個值才會是 10。

Example #5

function a() {
alert(this);
}
a.call(null);

事實上我認為這題是五題當中最簡單的。這需要依據兩項 JavaScript 的概念:

第一,你勢必瞭解 “this” 物件的值是如何決定的。在一個 method 被一個物件呼叫的同時,”this” 就會指向這個 method 存在的物件。例:

var object = {
method: function() {
alert(this === object);    //true
}
}
object.method();

在這份 code 中,當 object.method() 被呼叫的時候,”this” 指向這個物件的動作就同時完成了。在全域的 scope 中,”this” 就是 window (此指在瀏覽器中,在非瀏覽器的環境會與 “global” 物件相當),所以這在一個不是 object property 的 function 中也如同 window。例:

function method() {
alert(this === window);    //true
}
method();

這邊將會指向全域 object,也就是 “window”。

具備這樣的知識,你現在可以來處理第二個重要的概念了:「call() 做了什麼。」call() method 視 function 為一個「別的物件的 method」來執行它。第一個 argument 會是 method 中的 “this”,接下來的 arguments 則會依序傳入 function 中。我們來看看:

function method() {
alert(this === window);
}
method();    //true
method.call(document);   //false

這邊 method() function 被呼叫了,所以 “this” 就會是 “document” 本身。所以,會 alert 個 “false” 出來。

ECMA-262 裡有趣的是,第三版描述了當 “null” 被傳入 call() 作第一個 argument 的話會發生什麼事:

如果 “thisArg” 是 null 或 undefined,被呼叫的 function 會傳入 global 來當作 “this” 的值。否則被呼叫的 function 會傳入 ToObject(thisArg) 來當作 “this” 的值。

所以無論何時 null 被傳入 call() (或他的 sibling, apply()),他的預設值都會是 global 物件,也就是 window。瞭解了這些之後,這個範例 code 可以用更淺顯的方式重寫:

function a() {
alert(this);
}
a.call(window);

這樣子的 code 會更直觀,也就是會 alert 出 window 物件轉為字串的結果。

結論

Dmitry 把這些小測驗集結在一起,讓你能學到一些關於 JavaScript 的眉眉角角。我希望這次的寫作能讓大家瞭解每個 example code 在做什麼,最重要的事,為什麼要這樣做。我重申,我反對以這樣的測驗來用在職位的應徵評估裡,就如同我也不覺得他們在實際的使用上會有什麼作用(如果你想知道我怎麼面試前端工程師的,可以看我上一篇文章)。

公告:這篇文章的所有發言和意見都是基於 Nicholas C. Zakas 本人,跟 Yahoo!、Wrox Publishing、O’Reilly Publishing 或其他人都沒有關係。我只為自己發言,而不是為了他們。

, , ,

  1. #1 by George Wing - January 31st, 2010 at 20:55

    翻译得不错!支持!

  2. #2 by Linmic - January 31st, 2010 at 23:21

    謝謝樓上 :D

  3. #3 by DeidreMadden - July 18th, 2010 at 15:12

    That is good that people can get the personal loans moreover, this opens up new chances.

(將不會公開)
  1. No trackbacks yet.