前端要給力之:語句在JavaScript中的值
目錄
這兩天在寫語言精髓那本書的第三版,討論到ES6跟ES5中間對“語句的值”的不同處理。正好Weibo上也有同學對這個問題有興趣,所以專門整理了這篇。
寫博客可以啰嗦點,寫書就不行了。所以這篇文章跟書上能看到的還是會不一樣的。
問題是:語句有值嗎?
很不幸,我們面臨的的確是一門連語句都有值的語言。在JavaScript中,代碼是按語句行(Statement Lists)來解釋的,所以eval()本質上還是執行的語句行,例如:
eval("1+2+3")
實際上并不是在計算表達式,而是在解釋執行“代碼文本”。因為一個文本塊隱含的有一個“文本/文件結束符(EOF)”,它與行結束符(EOL)一樣可以等效于JavaScript的語句分隔符,所以上述代碼等效于:
eval("1+2+3;")
如果你不想了解得這么詳細,那么記住“JavaScript是按語句行執行的”就好了。
那么……這個語句的值到底是什么呢?很不幸,上面這個示例中,語句的值和表達式值是一樣的,也是6。
那么說,你騙我咯?
你看,JavaScript里面有一類語句,叫表達式語句。很不幸,你見到的絕大多數語句都是表達式語句。例如
Object.toString()
這里是一個方法調用(表達式),你在后面加個分號(;),在語法上那就成了一個語句行,于是就成了表達式語句。由于事實上JavaScript也存在單值表達式,所以一個單值也可以是一個語句。這事實上也就是函數或塊首放上個”use strict”一點點也不違和的原因——它是符合JavaScript傳統的語法慣例的:
function foo() {
"use strict";
1;
true;
}
上面的代碼是合法的,函數foo()內有三個單值語句。函數聲明本身也是一個語句,函數聲明(以及所有的顯式聲明語句)是沒有返回值的——在ECMAScript中它被定義為返回Empty。函數調用的返回值是由于調用運算符“()”來決定的,這個運算符要求用return來返回值,如果沒有則視為undefined。這就是JavaScript函數的一些特性的根源了。
“表達式運算”和“語句有值”可以解釋JavaScript語法特性中的許多迷題,是這門語言在設計上的一些基本性質。
有啥米用呢?
因為eval()本質上是執行語句而不是表達式,所以語句如何返回值就成了這個函數的最終特性。需要注意的是:不是eval()在求值,而是JavaScript代碼塊/語句行本身有值,而eval()只是返回這個值而已。如果沒有這個特性,Ajax也就做不成了,因為我們通常用Ajax/JSONP從遠端取個值過來,就是eval()“解析”一下,這里就是用的“執行語句并取值”的特性。
在JavaScript中,語句有值,而語句塊(復合語句)的值就是這個塊中最后一個有值語句的值。按ECMAScript的原文是:
The value of a StatementList is the value of the last value producing item in the StatementList. For example, the following calls to the eval function all return the value 1:
The production IfStatement : if ( Expression ) Statement else Statement is evaluated as follows:
- Let exprRef be the result of evaluating Expression.
- If ToBoolean(GetValue(exprRef)) is true, then
a. Return the result of evaluating the first Statement. - Else,
eval("1;;;;;")
eval("1;{}")
eval("1;var a;")
“ value producing item ”這個說法在ES5中是叫“ value producing Statement ”。但是,我并沒有在ES5/ES6中找到一個明確的說法:哪些語句是生成值的語句呢?
研究這個是不是閑得那個啥疼?
那個啥疼不疼跟這個毛線關系也沒有。研究這個其實是很重要的一件事情,因為下面這行代碼到底怎么解釋,取決于我們這里的研究:
// sourceText at remote
if (x) (
function aa() {}
)
else (
function bb() {}
)
我們假設上面的代碼段來自于遠端,然后我們在通過ajax的方式得到它,稱為”sourceText”,那么下面的代碼到底是什么結果呢?
x = true;
foo = eval(sourceText);
console.log(foo.name); // "name" property define in ES6
先解釋一下其中的aa/bb函數。注意這里的兩個函數在語法上不是“函數聲明語句”,而是“函數表達式”——注意這里用了“()”來強制它們為表達式。這個也不是我亂講,在MDN(Mozilla Developer Network)官方文檔上面就是這么分類的,“function statement”和“function expression”是兩個不同的東西。
“函數聲明語句”是無值的。而函數表達式是有值的,進而“函數表達式語句”也就是有值的。所以sourceText中,如果x是真,則if語句應該返回aa的值,否則該返回bb的值。于是,示例代碼中的:
foo = eval(sourceText);
才有意義,而最終控制臺才會輸出”aa”,表明foo函數來自于aa()函數表達式。現在看來,下面這句話是真的有用了吧:
if語句的值,是其then/else分支中的statementList最后一個有值語句的值。
ES5/ES6有什么差異呢?
這兩天寫書的時候發現一點跟此前理解的不同的地方(正好我又用了Nodejs中舊版的V8),所以實在搞不清楚ECMAScript的定義出了問題,還是V8的實現出了問題。于是乎在微信上抓了Hax要討論。無奈乎那個家伙不理我——所以我決定這周去上海找他算賬,此話容后再講。
有什么不同呢?
問題出在ES5中說,如果then/else分支中沒有語句,也就是statementList為empty,那么if語句結果也就為空。他的定義很簡單,是這么寫的:
The production IfStatement :
if ( Expression ) Statement else Statement
is evaluated as follows:
-
Let exprRef be the result of evaluating Expression.
-
If ToBoolean(GetValue(exprRef)) is true, thena. Return the result of evaluating the first Statement.
-
Else,a. Return the result of evaluating the second Statement.
假定“first Statement”為emptyStatement,結果當然就是empty。而對于empty,JavaScript會忽略這個“語句的值”。這個意思是說:
1;
{};
;
上面三個語句中,第2、3兩行實際都是空語句——它們的值是empty,被忽略。所以整個代碼文本會返回1。那么按照這個規則,下面的代碼:
1; if (true);
// 或
1; if (false);
這兩種情況都應該返回1。這個就是在ES5中的情況了。
然而在ES6里面,這段規范被寫成下面這樣:
// 4~5: let stmtCompletion be the result of first/second Statement
// 6: ReturnIfAbrupt(stmtCompletion).
7: If stmtCompletion .[[value]] is not empty, return stmtCompletion .
8: Return NormalCompletion( undefined ).
這里的意思是說:如果then/else的結果不是empty那么就返回它們,否則,就得返回“undefined”。
于是下面這樣的示例:
1; if (true);
就該返回undefined了。
我當然一早就讀明白了ES6,我當時的問題在于,我用了Nodejs中的舊版V8,以及firefox/chrome的舊版本來做測試——它們聲明支持了ES6。然而在這項特性上表現出來的,仍然是ES5的那個樣子。
于是我就懞逼了:這些聲稱支持ES6的引擎錯了,還是標準沒寫對呢?
正是因為對了解標準比了解指掌還要多的Hax沒有如期出現,所以一向認為
“標準都是人寫的,是人寫的就會錯”
的我選擇了相信…… 同樣也是一堆人(以及也是同樣一堆人)寫的ES5。——如果ES5是對的,那么就是ES6寫錯了。
結論是:ES6是改了規則,但更合理
驗證這個結論的方法是:Chrome的新版中的新V8引擎,以及Firefox的新版本都采用了ES6中的規范。當然,很不幸,你如果用Nodejs來測試,至少當前版本(4.4.2/5.10.1)中還是錯誤的、按照ES5的規范來實現的。
那么為什么我最終會認為ES6就“更合理”一點呢?
還是得回到“語句該不該有值”這個根本問題上來討論。首先,ECMAScript是承認語句有值的,而且也同時承認“某些語句是沒有值/不產生值”的。例如說,空語句就不產生值,函數聲明、變量聲明等等也不產生值 。
——對于成批的語句來說,不產生值則在代碼上下文中對結果值無影響,產生值則影響結果。所以明確”哪些有值,哪些沒有值“是很重要的。而ES5中,這個問題導致if語句的結果有不確定性。既然:
如果then/else中的語句有值,則if有值;如果無值,則if無值。
那么下面的代碼就是不確定語義的:
// sourceText at remote
"hello";
if (x) (
function aa() {}
)
當x是true時,if語句有意義,當x為false時,if語句在上下文中就沒意義了——它對結果值沒有影響。而
【ES5】if語句對結果值的影響存在不確定性
這個結果在語義設計上就是很失敗的。而到了ES6中:
【ES6】if語句總是有結果值的,要么是then/else的結果,要么是undefined
這就使得if有著確定的語義了。
最后,不僅僅是if語句
寫ECMAScript 262的那票人真不是吃閑飯的(除了寫4th的時候),有些問題人家是真想得清楚。比如還是這個語句的值的問題,根本上來說不是“if語句怎么回事”,而是“如何處理語句的值”的問題。我昨晚主要的工作就是整理了所有這些語句在值上的效果,if/for/while/try等等語句在值的處理上驚人的一致,除了這些語句和表達式語句之外,就只有return/yield/throw用來顯式地返回結果了。
所以說,語句在“產生值(value producing)”上面的行為,在ES6中得到了統一。