天天看點

讀懂 ECMAScript 規格

規格檔案(specification)是計算機語言的官方标準,較長的描述文法規則和實作方法。

讀懂 ECMAScript 規格

一般來說,沒有必要閱讀規格,除非你要寫編譯器。因為規格寫得非常抽象和精煉,又缺乏執行個體,不容易了解,而且對于解決實際的應用問題,幫助不大。但是,如果你遇到疑難的文法問題,實在找不到答案,這時可以去檢視規格檔案,了解語言标準是怎麼說的。規格是解決問題的"最後一招"。

這對javascript語言很有必要。因為它的使用場景複雜,文法規則不統一,例外很多,各種運作環境的行為不一緻,導緻奇怪的文法問題層出不窮,任何文法書都不可能囊括所有情況。檢視規格,不失為一種解決文法問題的最可靠、最權威的終極方法。

本文介紹如何讀懂ecmascript 6的規格檔案。

讀懂 ECMAScript 規格

這個規格檔案相當龐大,一共有26章,a4列印的話,足足有545頁。它的特點就是規定得非常細緻,每一個文法行為、每一個函數的實作都做了詳盡的清晰的描述。基本上,編譯器作者隻要把每一步翻譯成代碼就可以了。這很大程度上,保證了所有es6實作都有一緻的行為。

ecmascript 6規格的26章之中,第1章到第3章是對檔案本身的介紹,與語言關系不大。第4章是對這門語言總體設計的描述,有興趣的讀者可以讀一下。第5章到第8章是語言宏觀層面的描述。第5章是規格的名詞解釋和寫法的介紹,第6章介紹資料類型,第7章介紹語言内部用到的抽象操作,第8章介紹代碼如何運作。第9章到第26章介紹具體的文法。

對于一般使用者來說,除了第4章,其他章節都涉及某一方面的細節,不用通讀,隻要在用到的時候,查閱相關章節即可。下面通過一些例子,介紹如何使用這份規格。

先來看這個例子,請問下面表達式的值是多少。

規格對每一種文法行為的描述,都分成兩部分:先是總體的行為描述,然後是實作的算法細節。相等運算符的總體描述,隻有一句話。

"the comparison <code>x == y</code>, where <code>x</code> and <code>y</code> are values, produces <code>true</code> or <code>false</code>."

上面這句話的意思是,相等運算符用于比較兩個值,傳回<code>true</code>或<code>false</code>。

下面是算法細節。

returnifabrupt(x). returnifabrupt(y). if <code>type(x)</code> is the same as <code>type(y)</code>, then return the result of performing strict equality comparison <code>x === y</code>. if <code>x</code> is <code>null</code> and <code>y</code> is <code>undefined</code>, return <code>true</code>. if <code>x</code> is <code>undefined</code> and <code>y</code> is <code>null</code>, return <code>true</code>. if <code>type(x)</code> is number and <code>type(y)</code> is string, return the result of the comparison <code>x == tonumber(y)</code>. if <code>type(x)</code> is string and <code>type(y)</code> is number, return the result of the comparison <code>tonumber(x) == y</code>. if <code>type(x)</code> is boolean, return the result of the comparison <code>tonumber(x) == y</code>. if <code>type(y)</code> is boolean, return the result of the comparison <code>x == tonumber(y)</code>. if <code>type(x)</code> is either string, number, or symbol and <code>type(y)</code> is object, then return the result of the comparison <code>x == toprimitive(y)</code>. if <code>type(x)</code> is object and <code>type(y)</code> is either string, number, or symbol, then return the result of the comparison <code>toprimitive(x) == y</code>. return <code>false</code>.

上面這段算法,一共有12步,翻譯如下。

如果<code>x</code>不是正常值(比如抛出一個錯誤),中斷執行。 如果<code>y</code>不是正常值,中斷執行。 如果<code>type(x)</code>與<code>type(y)</code>相同,執行嚴格相等運算<code>x === y</code>。 如果<code>x</code>是<code>null</code>,<code>y</code>是<code>undefined</code>,傳回<code>true</code>。 如果<code>x</code>是<code>undefined</code>,<code>y</code>是<code>null</code>,傳回<code>true</code>。 如果<code>type(x)</code>是數值,<code>type(y)</code>是字元串,傳回<code>x == tonumber(y)</code>的結果。 如果<code>type(x)</code>是字元串,<code>type(y)</code>是數值,傳回<code>tonumber(x) == y</code>的結果。 如果<code>type(x)</code>是布爾值,傳回<code>tonumber(x) == y</code>的結果。 如果<code>type(y)</code>是布爾值,傳回<code>x == tonumber(y)</code>的結果。 如果<code>type(x)</code>是字元串或數值或<code>symbol</code>值,<code>type(y)</code>是對象,傳回<code>x == toprimitive(y)</code>的結果。 如果<code>type(x)</code>是對象,<code>type(y)</code>是字元串或數值或<code>symbol</code>值,傳回<code>toprimitive(x) == y</code>的結果。 傳回<code>false</code>。

下面再看另一個例子。

上面代碼中,數組<code>a1</code>的成員是三個<code>undefined</code>,數組<code>a2</code>的成員是三個空位。這兩個數組很相似,長度都是3,每個位置的成員讀取出來都是<code>undefined</code>。

但是,它們實際上存在重大差異。

上面代碼一共列出了四種運算,數組<code>a1</code>和<code>a2</code>的結果都不一樣。前三種運算(<code>in</code>運算符、數組的<code>hasownproperty</code>方法、<code>object.keys</code>方法)都說明,數組<code>a2</code>取不到屬性名。最後一種運算(數組的<code>map</code>方法)說明,數組<code>a2</code>沒有發生周遊。

為什麼<code>a1</code>與<code>a2</code>成員的行為不一緻?數組的成員是<code>undefined</code>或空位,到底有什麼不同?

"array elements may be elided at the beginning, middle or end of the element list. whenever a comma in the element list is not preceded by an assignmentexpression (i.e., a comma at the beginning or after another comma), the missing array element contributes to the length of the array and increases the index of subsequent elements. elided array elements are not defined. if an element is elided at the end of an array, that element does not contribute to the length of the array."

翻譯如下。

"數組成員可以省略。隻要逗号前面沒有任何表達式,數組的<code>length</code>屬性就會加1,并且相應增加其後成員的位置索引。被省略的成員不會被定義。如果被省略的成員是數組最後一個成員,則不會導緻數組<code>length</code>屬性增加。"

上面的規格說得很清楚,數組的空位會反映在<code>length</code>屬性,也就是說空位有自己的位置,但是這個位置的值是未定義,即這個值是不存在的。如果一定要讀取,結果就是<code>undefined</code>(因為<code>undefined</code>在javascript語言中表示不存在)。

這就解釋了為什麼<code>in</code>運算符、數組的<code>hasownproperty</code>方法、<code>object.keys</code>方法,都取不到空位的屬性名。因為這個屬性名根本就不存在,規格裡面沒說要為空位配置設定屬性名(位置索引),隻說要為下一個元素的位置索引加1。

至于為什麼數組的<code>map</code>方法會跳過空位,請看下一節。

後面的算法描述是這樣的。

let <code>o</code> be <code>toobject(this value)</code>. <code>returnifabrupt(o)</code>. let <code>len</code> be <code>tolength(get(o, "length"))</code>. <code>returnifabrupt(len)</code>. if <code>iscallable(callbackfn)</code> is <code>false</code>, throw a typeerror exception. if <code>thisarg</code> was supplied, let <code>t</code> be <code>thisarg</code>; else let <code>t</code> be <code>undefined</code>. let <code>a</code> be <code>arrayspeciescreate(o, len)</code>. <code>returnifabrupt(a)</code>. let <code>k</code> be 0. repeat, while <code>k</code> &lt; <code>len</code> a. let <code>pk</code> be <code>tostring(k)</code>. b. let <code>kpresent</code> be <code>hasproperty(o, pk)</code>. c. <code>returnifabrupt(kpresent)</code>. d. if <code>kpresent</code> is <code>true</code>, then d-1. let <code>kvalue</code> be <code>get(o, pk)</code>. d-2. <code>returnifabrupt(kvalue)</code>. d-3. let <code>mappedvalue</code> be <code>call(callbackfn, t, «kvalue, k, o»)</code>. d-4. <code>returnifabrupt(mappedvalue)</code>. d-5. let <code>status</code> be <code>createdatapropertyorthrow (a, pk, mappedvalue)</code>. d-6. <code>returnifabrupt(status)</code>. e. increase <code>k</code> by 1. return <code>a</code>.
得到目前數組的<code>this</code>對象 如果報錯就傳回 求出目前數組的<code>length</code>屬性 如果map方法的參數<code>callbackfn</code>不可執行,就報錯 如果map方法的參數之中,指定了<code>this</code>,就讓<code>t</code>等于該參數,否則<code>t</code>為<code>undefined</code> 生成一個新的數組<code>a</code>,跟目前數組的<code>length</code>屬性保持一緻 設定<code>k</code>等于0 隻要<code>k</code>小于目前數組的<code>length</code>屬性,就重複下面步驟 a. 設定<code>pk</code>等于<code>tostring(k)</code>,即将<code>k</code>轉為字元串 b. 設定<code>kpresent</code>等于<code>hasproperty(o, pk)</code>,即求目前數組有沒有指定屬性 c. 如果報錯就傳回 d. 如果<code>kpresent</code>等于<code>true</code>,則進行下面步驟 d-1. 設定<code>kvalue</code>等于<code>get(o, pk)</code>,取出目前數組的指定屬性 d-2. 如果報錯就傳回 d-3. 設定<code>mappedvalue</code>等于<code>call(callbackfn, t, «kvalue, k, o»)</code>,即執行回調函數 d-4. 如果報錯就傳回 d-5. 設定<code>status</code>等于<code>createdatapropertyorthrow (a, pk, mappedvalue)</code>,即将回調函數的值放入<code>a</code>數組的指定位置 d-6. 如果報錯就傳回 e. <code>k</code>增加1 傳回<code>a</code>

仔細檢視上面的算法,可以發現,當處理一個全是空位的數組時,前面步驟都沒有問題。進入第10步的b時,<code>kpresent</code>會報錯,因為空位對應的屬性名,對于數組來說是不存在的,是以就會傳回,不會進行後面的步驟。

上面代碼中,<code>arr</code>是一個全是空位的數組,<code>map</code>方法周遊成員時,發現是空位,就直接跳過,不會進入回調函數。是以,回調函數裡面的<code>console.log</code>語句根本不會執行,整個<code>map</code>方法傳回一個全是空位的新數組。

讀懂 ECMAScript 規格

(完)

繼續閱讀