我們先從一組 用例說起. 看代碼:
null > 0 // false null == 0 // false null >= 0 //true
我們今天討論的主要內容,並不是這個結果可能看起來多么奇怪. 而是為什么會這樣. 之所以特別記一篇隨筆在這里,主要是因為,我在得到了 Brendan Eich 的一些確認后.發現答案和我當初的猜測不一樣. 所以我有反省,自己對ES的一些理解上,是不是有些偏主觀.
開始前,我們先拿ES3,ES5的相關定義說起:
注1 : ES3,ES5對關系運算符 ">" , "<", ">=", "<=" 以及相等運算符的實現,在關鍵部分幾乎相同,所以不再列出ES5的內容.
注2 : 下面這些內容,你只需要簡單注意下,我標紅的部分,其他部分其實無關緊要. 我會對這些內容,在后面的部分有個大概的解釋.
注3 : <= 以及 < 與之類似,不在贅述..
ES3 的 ">" 運算符:
The Greater-than Operator ( > )
The production RelationalExpression : RelationalExpression > ShiftExpression is evaluated as follows:
1. Evaluate RelationalExpression.
2. Call GetValue(Result(1)).
3. Evaluate ShiftExpression.
4. Call GetValue(Result(3)).
5. Perform the comparison Result(4) < Result(2).
6. If Result(5) is undefined, return false. Otherwise, return Result(5).
ES3 的">=" 運算符:
The Greater-than-or-equal Operator ( >= )
The production RelationalExpression : RelationalExpression >= ShiftExpression is evaluated as follows:
1. Evaluate RelationalExpression.
2. Call GetValue(Result(1)).
3. Evaluate ShiftExpression.
4. Call GetValue(Result(3)).
5. Perform the comparison Result(2) < Result(4). (see 11.8.5).
6. If Result(5) is true or undefined, return false. Otherwise, return true.
ES3 的 "==" 運算符 :
The Equals Operator ( == )
The production EqualityExpression : EqualityExpression == RelationalExpression is evaluated as
follows:
1. Evaluate EqualityExpression.
2. Call GetValue(Result(1)).
3. Evaluate RelationalExpression.
4. Call GetValue(Result(3)).
5. Perform the comparison Result(4) == Result(2). (see 11.9.3).
6. Return Result(5).
ES3 關於 內部關系運算的算法實現 :
The Abstract Relational Comparison Algorithm
The comparison x < y, where x and y are values, produces true, false, or undefined (which indicates that
at least one operand is NaN). Such a comparison is performed as follows:
1. Call ToPrimitive(x, hint Number).
2. Call ToPrimitive(y, hint Number).
3. If Type(Result(1)) is String and Type(Result(2)) is String, go to step 16. (Note that this step differs
from step 7 in the algorithm for the addition operator + in using and instead of or.)
4. Call ToNumber(Result(1)).
5. Call ToNumber(Result(2)).
6. If Result(4) is NaN, return undefined.
7. If Result(5) is NaN, return undefined.
8. If Result(4) and Result(5) are the same number value, return false.
9. If Result(4) is +0 and Result(5) is −0, return false.
10. If Result(4) is −0 and Result(5) is +0, return false.
11. If Result(4) is +∞, return false.
12. If Result(5) is +∞, return true.
13. If Result(5) is −∞, return false.14. If Result(4) is −∞, return true.
15. If the mathematical value of Result(4) is less than the mathematical value of Result(5)—note that
these mathematical values are both finite and not both zero—return true. Otherwise, return false.
16.If Result(2) is a prefix of Result(1), return false. (A string value p is a prefix of string value q if q
can be the result of concatenating p and some other string r. Note that any string is a prefix of itself,
because r may be the empty string.)
17. If Result(1) is a prefix of Result(2), return true.
18.Let k be the smallest nonnegative integer such that the character at position k within Result(1) is
different from the character at position k within Result(2). (There must be such a k, for neither string
is a prefix of the other.)
19. Let m be the integer that is the code point value for the character at position k within Result(1).
20. Let n be the integer that is the code point value for the character at position k within Result(2).
21. If m < n, return true. Otherwise, return false.
ES3 關於 內部相等性運算的算法實現 :
The Abstract Equality Comparison Algorithm
The comparison x == y, where x and y are values, produces true or false. Such a comparison is
performed as follows:
1. If Type(x) is different from Type(y), go to step 14.
2. If Type(x) is Undefined, return true.
3. If Type(x) is Null, return true.
4. If Type(x) is not Number, go to step 11.
5. If x is NaN, return false.
6. If y is NaN, return false.
7. If x is the same number value as y, return true.
8. If x is +0 and y is −0, return true.
9. If x is −0 and y is +0, return true.
10. Return false.
11.If Type(x) is String, then return true if x and y are exactly the same sequence of characters (same
length and same characters in corresponding positions). Otherwise, return false.
12. If Type(x) is Boolean, return true if x and y are both true or both false. Otherwise, return false.
13.Return true if x and y refer to the same object or if they refer to objects joined to each other (see
13.1.2). Otherwise, return false.
14. If x is null and y is undefined, return true.
15. If x is undefined and y is null, return true.16.If Type(x) is Number and Type(y) is String,return the result of the comparison x == ToNumber(y).
17.If Type(x) is String and Type(y) is Number,return the result of the comparison ToNumber(x) == y.
18. If Type(x) is Boolean, return the result of the comparison ToNumber(x) == y.
19. If Type(y) is Boolean, return the result of the comparison x == ToNumber(y).
20.If Type(x) is either String or Number and Type(y) is Object,return the result of the comparison x == ToPrimitive(y).
21.If Type(x) is Object and Type(y) is either String or Number,return the result of the comparison ToPrimitive(x) == y.
22. Return false.
以上內容,我們應該關心的部分,也是我之前閱讀時忽略的很重要的部分:
1. 關系運算符 和 相等運算符 並不是一個類別的.
2. 關系運算符,在設計上,總是需要運算元嘗試轉為一個number . 而相等運算符在設計上,則沒有這方面的考慮.
3. 最重要的一點, 不要把 拿 a > b , a == b 的結果 想當然的去和 a >= b 建立聯系. 正確的符合最初設計思想的關系是 a > b 與 a >= b是一組 . a == b 和其他相等運算符才是一組. 比如 a === b , a != b, a !== b .
所以,我們反過來看待這個問題.
null > 0 // null 嘗試轉型為number , 則為0 . 所以結果為 false, null >= 0 // null 嘗試轉為number ,則為0 , 結果為 true. null == 0 // null在設計上,在此處不嘗試轉型. 所以 結果為false.
我個人在之前的閱讀中,並沒有意識到這些問題.而導致,我愚蠢的認為, >= 的結果是設計上的失誤. 原因是,我簡單的認為 :
5. Perform the comparison Result(2) < Result(4). (see 11.8.5).
6. If Result(5) is true or undefined, return false. Otherwise, return true.
a >= b 運算符只是簡單的去對 a < b的結果取反. 我以為這是一個設計上的失誤的另一個理由是 undefined,在標准中,被單拎出來.細心的你,也一定發現了這一點. 對於undefined的設計, undefined > 0 , undefined < 0, undefined == 0 的結果是符合設計上,邏輯的一致性的. 而null是被遺漏的東西.
懷着這樣的想法,我給 es-discuss 寫了信,用不溫和的口吻.質疑這個問題. 驚喜的是, Brendan Eich 居然關注了這個問題.並立刻做出了回復. 悲劇的是,我在反復閱讀該回信內容時,仍然沒有從根本上理解到這個問題.直到今天早上.我重新翻閱了ES3,5.相關章節. 才恍然大悟.
到此,我的反省結束. 同時也感謝 BE 的認真,及時的回復. 作為一個CTO,任然如此關心並積極參與技術社區的發展,實在讓人欽佩.
接着是吐槽的時候了:
雖然前面的例子,我catch到了BE當初的設計思想. 但是從全局的角度來看. 從關系運算符到相等運算符,尤其是相等運算符的設計上. 真的十分混亂不堪. BE在信中提到,他對 == 的現狀也很無奈. 甚至用愚蠢這個詞來形容自己當初的實現(當然他還提到,當初只是為了在10天內設計出js,並跑過qa的測試用例). 即使如此, 但是他仍然表示 null == 0 這個結果是他想要的.
好吧,到了這里,我也有種無力感. 我認為縱觀javascript,對關系運算和相等運算的設計.除了混亂,我想不出還有什么詞來形容它們更恰當. 這一點從,我們生產環境代碼中,大量的類型檢查,和防御性代碼的的存在,就可以證明這一點.
另外一個要吐槽的地方是ES規范本身. 比如前面讓我深深誤解的地方 ,即 a >= b 即 對 a < b 的結果取反. 這一點上,就屬於不明確表述. 我們想想一個典型的js的例子
function case1(a){ if(a == null){ .... } } function case2(a){ if(a == undefined){ ... } } // 上面兩組完全等價, 這就是一種不明確表述. // 我們永遠不知道代碼編寫者的目的到底是同時匹配null 和 undefined還是只匹配其中某一個 function case3(a){ if(a === null || a === undefined){ ... } } // case3 才是最好的表述. 我們明確知道代碼編寫者的意圖. // 即使很多人可能認為這個代碼很愚蠢. 但我堅定的認為這才是好代碼.
所以寫代碼,寫規范,都應該明確表述. 即使表述的很羅嗦,但不會引起歧義或懷疑. 這才是一份好的標准.文檔,代碼. 而避免歧義,和各種混亂不堪的規則,是一門語言最應遵守的設計原則.
最后, 不得不提到,我發出null >= 0 這封信后, Andrea Giammarchi 表示了對我之前看法的支持,他同我最初的看法一樣,認為 null >= 0 的結果應該為 false . 並建議在 ES7 中的嚴格模式中,修改這個結果. 雖然同樣遭到 David Bruant 的反對. 好吧為他和我的這個錯誤看法,默哀一分鍾...