規格文件是計算機語言的官方標準,詳細描述語法規則和實現方法。javascript
通常來講,沒有必要閱讀規格,除非你要寫編譯器。由於規格寫得很是抽象和精煉,又缺少實例,不容易理解,並且對於解決實際的應用問題,幫助不大。可是,若是你遇到疑難的語法問題,實在找不到答案,這時能夠去查看規格文件,瞭解語言標準是怎麼說的。規格是解決問題的「最後一招」。java
這對 JavaScript 語言頗有必要。由於它的使用場景複雜,語法規則不統一,例外不少,各類運行環境的行爲不一致,致使奇怪的語法問題層出不窮,任何語法書都不可能囊括全部狀況。查看規格,不失爲一種解決語法問題的最可靠、最權威的終極方法。git
本章介紹如何讀懂 ECMAScript 6 的規格文件。github
ECMAScript 6 的規格,能夠在 ECMA 國際標準組織的官方網站(www.ecma-international.org/ecma-262/6.0/)免費下載和在線閱讀。算法
這個規格文件至關龐大,一共有 26 章,A4 打印的話,足足有 545 頁。它的特色就是規定得很是細緻,每個語法行爲、每個函數的實現都作了詳盡的清晰的描述。基本上,編譯器做者只要把每一步翻譯成代碼就能夠了。這很大程度上,保證了全部 ES6 實現都有一致的行爲。數組
ECMAScript 6 規格的 26 章之中,第 1 章到第 3 章是對文件自己的介紹,與語言關係不大。第 4 章是對這門語言整體設計的描述,有興趣的讀者能夠讀一下。第 5 章到第 8 章是語言宏觀層面的描述。第 5 章是規格的名詞解釋和寫法的介紹,第 6 章介紹數據類型,第 7 章介紹語言內部用到的抽象操做,第 8 章介紹代碼如何運行。第 9 章到第 26 章介紹具體的語法。數據結構
對於通常用戶來講,除了第 4 章,其餘章節都涉及某一方面的細節,不用通讀,只要在用到的時候,查閱相關章節便可。app
ES6 規格使用了一些專門的術語,瞭解這些術語,能夠幫助你讀懂規格。本節介紹其中的幾個。ide
所謂「抽象操做」(abstract operations)就是引擎的一些內部方法,外部不能調用。規格定義了一系列的抽象操做,規定了它們的行爲,留給各類引擎本身去實現。函數
舉例來講,Boolean(value)
的算法,第一步是這樣的。
- Let
b
beToBoolean(value)
.
這裏的ToBoolean
就是一個抽象操做,是引擎內部求出布爾值的算法。
許多函數的算法都會屢次用到一樣的步驟,因此 ES6 規格將它們抽出來,定義成「抽象操做」,方便描述。
ES6 規格將鍵值對(key-value map)的數據結構稱爲 Record,其中的每一組鍵值對稱爲 field。這就是說,一個 Record 由多個 field 組成,而每一個 field 都包含一個鍵名(key)和一個鍵值(value)。
ES6 規格大量使用[[Notation]]
這種書寫法,好比[[Value]]
、[[Writable]]
、[[Get]]
、[[Set]]
等等。它用來指代 field 的鍵名。
舉例來講,obj
是一個 Record,它有一個Prototype
屬性。ES6 規格不會寫obj.Prototype
,而是寫obj.[[Prototype]]
。通常來講,使用[[Notation]]
這種書寫法的屬性,都是對象的內部屬性。
全部的 JavaScript 函數都有一個內部屬性[[Call]]
,用來運行該函數。
F.[[Call]](V, argumentsList)
上面代碼中,F
是一個函數對象,[[Call]]
是它的內部方法,F.[[call]]()
表示運行該函數,V
表示[[Call]]
運行時this
的值,argumentsList
則是調用時傳入函數的參數。
每個語句都會返回一個 Completion Record,表示運行結果。每一個 Completion Record 有一個[[Type]]
屬性,表示運行結果的類型。
[[Type]]
屬性有五種可能的值。
若是[[Type]]
的值是normal
,就稱爲 normal completion,表示運行正常。其餘的值,都稱爲 abrupt completion。其中,開發者只須要關注[[Type]]
爲throw
的狀況,即運行出錯;break
、continue
、return
這三個值都只出如今特定場景,能夠不用考慮。
抽象操做的運行流程,通常是下面這樣。
- Let
resultCompletionRecord
beAbstractOp()
.- If
resultCompletionRecord
is an abrupt completion, returnresultCompletionRecord
.- Let
result
beresultCompletionRecord.[[Value]]
.- return
result
.
上面的第一步是調用抽象操做AbstractOp()
,獲得resultCompletionRecord
,這是一個 Completion Record。第二步,若是這個 Record 屬於 abrupt completion,就將resultCompletionRecord
返回給用戶。若是此處沒有返回,就表示運行結果正常,所得的值存放在resultCompletionRecord.[[Value]]
屬性。第三步,將這個值記爲result
。第四步,將result
返回給用戶。
ES6 規格將這個標準流程,使用簡寫的方式表達。
- Let
result
beAbstractOp()
.ReturnIfAbrupt(result)
.- return
result
.
這個簡寫方式裏面的ReturnIfAbrupt(result)
,就表明了上面的第二步和第三步,即若是有報錯,就返回錯誤,不然取出值。
甚至還有進一步的簡寫格式。
- Let
result
be? AbstractOp()
.- return
result
.
上面流程的?
,就表明AbstractOp()
可能會報錯。一旦報錯,就返回錯誤,不然取出值。
除了?
,ES 6 規格還使用另外一個簡寫符號!
。
- Let
result
be! AbstractOp()
.- return
result
.
上面流程的!
,表明AbstractOp()
不會報錯,返回的必定是 normal completion,老是能夠取出值。
下面經過一些例子,介紹如何使用這份規格。
相等運算符(==
)是一個很讓人頭痛的運算符,它的語法行爲多變,不符合直覺。這個小節就看看規格怎麼規定它的行爲。
請看下面這個表達式,請問它的值是多少。
0 == null
若是你不肯定答案,或者想知道語言內部怎麼處理,就能夠去查看規格,7.2.12 小節是對相等運算符(==
)的描述。
規格對每一種語法行爲的描述,都分紅兩部分:先是整體的行爲描述,而後是實現的算法細節。相等運算符的整體描述,只有一句話。
「The comparison
x == y
, wherex
andy
are values, producestrue
orfalse
.」
上面這句話的意思是,相等運算符用於比較兩個值,返回true
或false
。
下面是算法細節。
- ReturnIfAbrupt(x).
- ReturnIfAbrupt(y).
- If
Type(x)
is the same asType(y)
, then
- Return the result of performing Strict Equality Comparison
x === y
.- If
x
isnull
andy
isundefined
, returntrue
.- If
x
isundefined
andy
isnull
, returntrue
.- If
Type(x)
is Number andType(y)
is String, return the result of the comparisonx == ToNumber(y)
.- If
Type(x)
is String andType(y)
is Number, return the result of the comparisonToNumber(x) == y
.- If
Type(x)
is Boolean, return the result of the comparisonToNumber(x) == y
.- If
Type(y)
is Boolean, return the result of the comparisonx == ToNumber(y)
.- If
Type(x)
is either String, Number, or Symbol andType(y)
is Object, then return the result of the comparisonx == ToPrimitive(y)
.- If
Type(x)
is Object andType(y)
is either String, Number, or Symbol, then return the result of the comparisonToPrimitive(x) == y
.- Return
false
.
上面這段算法,一共有 12 步,翻譯以下。
- 若是
x
不是正常值(好比拋出一個錯誤),中斷執行。- 若是
y
不是正常值,中斷執行。- 若是
Type(x)
與Type(y)
相同,執行嚴格相等運算x === y
。- 若是
x
是null
,y
是undefined
,返回true
。- 若是
x
是undefined
,y
是null
,返回true
。- 若是
Type(x)
是數值,Type(y)
是字符串,返回x == ToNumber(y)
的結果。- 若是
Type(x)
是字符串,Type(y)
是數值,返回ToNumber(x) == y
的結果。- 若是
Type(x)
是布爾值,返回ToNumber(x) == y
的結果。- 若是
Type(y)
是布爾值,返回x == ToNumber(y)
的結果。- 若是
Type(x)
是字符串或數值或Symbol
值,Type(y)
是對象,返回x == ToPrimitive(y)
的結果。- 若是
Type(x)
是對象,Type(y)
是字符串或數值或Symbol
值,返回ToPrimitive(x) == y
的結果。- 返回
false
。
因爲0
的類型是數值,null
的類型是 Null(這是規格4.3.13 小節的規定,是內部 Type 運算的結果,跟typeof
運算符無關)。所以上面的前 11 步都得不到結果,要到第 12 步才能獲得false
。
0 == null // false
下面再看另外一個例子。
const a1 = [undefined, undefined, undefined]; const a2 = [, , ,]; a1.length // 3 a2.length // 3 a1[0] // undefined a2[0] // undefined a1[0] === a2[0] // true
上面代碼中,數組a1
的成員是三個undefined
,數組a2
的成員是三個空位。這兩個數組很類似,長度都是 3,每一個位置的成員讀取出來都是undefined
。
可是,它們實際上存在重大差別。
0 in a1 // true 0 in a2 // false a1.hasOwnProperty(0) // true a2.hasOwnProperty(0) // false Object.keys(a1) // ["0", "1", "2"] Object.keys(a2) // [] a1.map(n => 1) // [1, 1, 1] a2.map(n => 1) // [, , ,]
上面代碼一共列出了四種運算,數組a1
和a2
的結果都不同。前三種運算(in
運算符、數組的hasOwnProperty
方法、Object.keys
方法)都說明,數組a2
取不到屬性名。最後一種運算(數組的map
方法)說明,數組a2
沒有發生遍歷。
爲何a1
與a2
成員的行爲不一致?數組的成員是undefined
或空位,到底有什麼不一樣?
規格的12.2.5 小節《數組的初始化》給出了答案。
「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.」
翻譯以下。
"數組成員能夠省略。只要逗號前面沒有任何表達式,數組的
length
屬性就會加 1,而且相應增長其後成員的位置索引。被省略的成員不會被定義。若是被省略的成員是數組最後一個成員,則不會致使數組length
屬性增長。」
上面的規格說得很清楚,數組的空位會反映在length
屬性,也就是說空位有本身的位置,可是這個位置的值是未定義,即這個值是不存在的。若是必定要讀取,結果就是undefined
(由於undefined
在 JavaScript 語言中表示不存在)。
這就解釋了爲何in
運算符、數組的hasOwnProperty
方法、Object.keys
方法,都取不到空位的屬性名。由於這個屬性名根本就不存在,規格里面沒說要爲空位分配屬性名(位置索引),只說要爲下一個元素的位置索引加 1。
至於爲何數組的map
方法會跳過空位,請看下一節。
規格的22.1.3.15 小節定義了數組的map
方法。該小節先是整體描述map
方法的行爲,裏面沒有提到數組空位。
後面的算法描述是這樣的。
- Let
O
beToObject(this value)
.ReturnIfAbrupt(O)
.- Let
len
beToLength(Get(O, "length"))
.ReturnIfAbrupt(len)
.- If
IsCallable(callbackfn)
isfalse
, throw a TypeError exception.- If
thisArg
was supplied, letT
bethisArg
; else letT
beundefined
.- Let
A
beArraySpeciesCreate(O, len)
.ReturnIfAbrupt(A)
.- Let
k
be 0.- Repeat, while
k
<len
- Let
Pk
beToString(k)
.- Let
kPresent
beHasProperty(O, Pk)
.ReturnIfAbrupt(kPresent)
.- If
kPresent
istrue
, then
- Let
kValue
beGet(O, Pk)
.ReturnIfAbrupt(kValue)
.- Let
mappedValue
beCall(callbackfn, T, «kValue, k, O»)
.ReturnIfAbrupt(mappedValue)
.- Let
status
beCreateDataPropertyOrThrow (A, Pk, mappedValue)
.ReturnIfAbrupt(status)
.- Increase
k
by 1.- Return
A
.
翻譯以下。
- 獲得當前數組的
this
對象- 若是報錯就返回
- 求出當前數組的
length
屬性- 若是報錯就返回
- 若是 map 方法的參數
callbackfn
不可執行,就報錯- 若是 map 方法的參數之中,指定了
this
,就讓T
等於該參數,不然T
爲undefined
- 生成一個新的數組
A
,跟當前數組的length
屬性保持一致- 若是報錯就返回
- 設定
k
等於 0- 只要
k
小於當前數組的length
屬性,就重複下面步驟
- 設定
Pk
等於ToString(k)
,即將K
轉爲字符串- 設定
kPresent
等於HasProperty(O, Pk)
,即求當前數組有沒有指定屬性- 若是報錯就返回
- 若是
kPresent
等於true
,則進行下面步驟
- 設定
kValue
等於Get(O, Pk)
,取出當前數組的指定屬性- 若是報錯就返回
- 設定
mappedValue
等於Call(callbackfn, T, «kValue, k, O»)
,即執行回調函數- 若是報錯就返回
- 設定
status
等於CreateDataPropertyOrThrow (A, Pk, mappedValue)
,即將回調函數的值放入A
數組的指定位置- 若是報錯就返回
k
增長 1- 返回
A
仔細查看上面的算法,能夠發現,當處理一個全是空位的數組時,前面步驟都沒有問題。進入第 10 步中第 2 步時,kPresent
會報錯,由於空位對應的屬性名,對於數組來講是不存在的,所以就會返回,不會進行後面的步驟。
const arr = [, , ,]; arr.map(n => { console.log(n); return 1; }) // [, , ,]
上面代碼中,arr
是一個全是空位的數組,map
方法遍歷成員時,發現是空位,就直接跳過,不會進入回調函數。所以,回調函數裏面的console.log
語句根本不會執行,整個map
方法返回一個全是空位的新數組。
V8 引擎對map
方法的實現以下,能夠看到跟規格的算法描述徹底一致。
function ArrayMap(f, receiver) { CHECK_OBJECT_COERCIBLE(this, "Array.prototype.map"); // Pull out the length so that modifications to the length in the // loop will not affect the looping and side effects are visible. var array = TO_OBJECT(this); var length = TO_LENGTH_OR_UINT32(array.length); return InnerArrayMap(f, receiver, array, length); } function InnerArrayMap(f, receiver, array, length) { if (!IS_CALLABLE(f)) throw MakeTypeError(kCalledNonCallable, f); var accumulator = new InternalArray(length); var is_array = IS_ARRAY(array); var stepping = DEBUG_IS_STEPPING(f); for (var i = 0; i < length; i++) { if (HAS_INDEX(array, i, is_array)) { var element = array[i]; // Prepare break slots for debugger step in. if (stepping) %DebugPrepareStepInIfStepping(f); accumulator[i] = %_Call(f, receiver, element, i, array); } } var result = new GlobalArray(); %MoveArrayContents(accumulator, result); return result; }