var func = eval("(function(){})");
alert(typeof func);
--------
更進一步的問題是,書中對匿名和具名函數在JScript與SpiderMonkey中的表現解釋得不夠
清楚。好的,這篇文章就這個問題深入討論,不單涉及書中的內容,也更深入地講述一
下JS的解釋與執行過程——其實所有的內容在書中都有涉及,但過於分散,不便於專門
地來分析一個具體問題。
首先,應該明確運算式與語句。對於JS來說,eval()總是試圖執行一個語句,因此它必須
先將執行文本理解為語句。如下:
--------
eval("1")
--------
在JS看來,由於eval()必須執行語句,因此"1"不再是直接量運算式,而是直接量運算式語
句,也就是相當於“1;”。這些內容,在“5.2.2 動態執行過程中的語句、運算式與值”
中有詳細解釋。
所以,eval()的傳回值,其實是語句最後一個(有效)子句的傳回值。接下來,我們需
要瞭解“聲明語句”和“運算式”。例如:
--------
function x() {
//....
}
--------
很明顯,這是一個具名函數的“聲明語句”。注意的是,“聲明語句”是不傳回值的。也
就是說,聲明語句是在文法解釋期,由先行編譯器處理的,而在執行期它是沒意義的——沒
有值,也沒有傳回值。例如單純的“var X”,是一個聲明語句,它就不會傳回值,而對於
“var X=100”來說,JS就處理成一個聲明語句,和一個在執行期的指派陳述式,它就有返回
值(後者的值)。
上面的規則對於JScript和SpiderMonkey來說都是一樣的,這沒有區別。有區別的是接下來的
內容。首先,SpiderMonkey承認“函數運算式(function expression)”,為了直接這樣一種
特性,它約在“函數運算式”中出現的“函數名”是無效的。因為“函數名”是“聲明語
句”來陳述的,而“運算式”是比語句更小(或更低級)的一個層級,因此不可能在“表
達式”中出現“語句聲明”,所以只好在運算式中忽略函數名。這樣一來,SpiderMonkey中
下面語句:
--------
x = "1234" + (function X() {});
------
中函數X就沒有標識符的效果,它對錶達式之外的、或者全域的“標識符”都不會構成影
響。更進一步地說:
--------
var X = 100;
x = "1234" + (function X() {});
------
在這樣兩行代碼中,變數X不會被重寫,因為第二行中的函數名X是無效的。關於這些內
容,在書中“5.4.2.1 文法聲明與語句含義不一致的問題”有詳細解釋。
正是在上面這個小節中,還討論到了MS JScript對這個問題的處理。JScript承認在代碼內文
的任意位置出現的函數標識符聲明。也就是說,由於上面的標識符是有效,所以全域變
量中的“X”就會被重寫。但是,正是由於這個緣故,JScript就必須對下面這個問題做解釋:
------
eval("(function(){})");
eval("(function X(){})");
------
請問:這兩行代碼在語義上有沒有不同?由於SpiderMonkey承認函數運算式,因此把兩個
都解釋為運算式的運算元;而JScript要承認第二行代碼中的變數名X,因此只好兩個都解釋
為語句。也就是說,在預設情況下,JScript認為第一行是匿名函數“聲明語句”,第二行則
是具名函式宣告語句。因此,如同前面所說的,“聲明語句”不傳回值,所以在JScript中兩
行代碼都返回undefined,而且第二行代碼聲明了一個變數名X。
關於這個問題,我在腳註中說“函式宣告的語句含義”的確是有些含糊的。無論如何,只需
要簡單地理解為JScript認為這裡是“函數語句聲明”,而SpiderMonkey認為這裡是“函數表達
式聲明”就可以了。
好。到這裡為止,我們大概只解釋清楚了:
------
eval("(function(){})");
eval("(function X(){})");
------
這兩行語句的效果,以及產生這種效果的原因。但是,對於我在書上的例子和腳註說明,仍
然是有疑問的。這來源於這段文字和代碼:
------
不過在JScript中存在一個例外:函數直接量(這裡指匿名函數)不能通過這種方式來獲得。例如下面的代碼:
var func = eval("(function() { })");
// 輸出"undefined"
alert(typeof func);
這種情況下,可以具名函數來得到它(*)。例如:………………
------
先留意這段文字是上下文相關的。我只是想說明如何能向變數"func"賦一個有效值。這涉
及到兩種方法:
------
// 方法1,用匿名函數
var func = eval("(function() { })");
alert(typeof func);
// 方法2,用具名函數
var func;
eval("function func() { }");
alert(typeof func);
------
這段話的意思是“在JScript中使用方法1(用匿名函數的方法)是不行的,需要使用第二種”,
而在腳註中,說SpiderMonkey正好相反,也僅指這個例子而言——在SpiderMonkey中,第
二種方法是無效的,而第一種是有效。
這裡與前述的內容稍有差異的是,方法2並沒有使用“傳回值”,而只是通過eval()語句中
利用函數名聲明的效果,來影響全域變數func。而正是由於SpiderMonkey不承認這個聲明的
標識符,所以是無效的。
同樣的原因,讀者I22141說修改成下面這樣:
------
var func = eval("function func() { }");
------
在SpiderMonkey中則是利用了傳回值,與上面這個樣本已經不是同一個問題了。所以I22141
所問“(那麼你所說的在SpiderMonkey中可以用eval()返回一個匿名函數,而對具名函數卻只
能返回undefined是什麼含義?)”,也是脫離了這個樣本的一個設問。
最後再來講述一個細節問題,這在書上也未有提及,其實也是一個很怪異的事件。首先,在
上面我說,在JScript中:
--------
// 第一種情況
var func = eval("(function(){})");
alert(typeof func); // 顯示"undefined"
--------
這裡顯示undefined是因為JScript將後面的函數解釋為匿名函式宣告,所以沒有值。其實是相對
要牽強一些的。因為我們修改一下:
--------
// 第二種情況
var func = eval("(1, function(){})");
alert(typeof func); // 顯示"function"
--------
就不同了。那麼到底為什麼第一種情況下,JScript就一定是理解為“聲明語句”而不是表達
式呢?我不得確知。我只是從:
--------
// 第三種情況
var X;
eval("(function X(){})");
alert(typeof X); // 顯示"function"
--------
這種情況下存在“語句聲明”的效果來推斷第一種情況的。更為奇特的是,我不清楚為什
麼JScript容許在一個運算式運算子“(...)”中存在一個“語句”——因為理論上說,這種情
況是在文法分析中難於解釋的。我甚至在“5.4.2.1 文法聲明與語句含義不一致的問題”提
到下面的代碼:
--------
// 樣本5:文法聲明階段重寫
// 重寫
(function Object(){
}).prototype.value = 100;
// 顯示值undefined
var obj = new Object();
alert(obj.value);
--------
是因為執行期“(X).prototype.value”中的那個函數X,與語句分析期覆蓋了全域的Object標識
符的函數,不是同一個。但,我仍然不明白,為什麼JScript允許在運算式中存在聲明語句。
與此更為相悖的是,如果要承認這樣的假設,那麼為什麼下面的語句不能執行:
--------
(var x=100);
--------
然而如果不承認,那麼下面的代碼卻又能正常執行:
--------
(function X() {});
--------
OH... 在這個問題的最終答案上,我仍然是迷惑的。