規(guī)格文件是計算機(jī)語言的官方標(biāo)準(zhǔn),詳細(xì)描述語法規(guī)則和實(shí)現(xiàn)方法。
一般來說,沒有必要閱讀規(guī)格,除非你要寫編譯器。因?yàn)橐?guī)格寫得非常抽象和精煉,又缺乏實(shí)例,不容易理解,而且對于解決實(shí)際的應(yīng)用問題,幫助不大。但是,如果你遇到疑難的語法問題,實(shí)在找不到答案,這時可以去查看規(guī)格文件,了解語言標(biāo)準(zhǔn)是怎么說的。規(guī)格是解決問題的“最后一招”。
這對 JavaScript 語言很有必要。因?yàn)樗氖褂脠鼍皬?fù)雜,語法規(guī)則不統(tǒng)一,例外很多,各種運(yùn)行環(huán)境的行為不一致,導(dǎo)致奇怪的語法問題層出不窮,任何語法書都不可能囊括所有情況。查看規(guī)格,不失為一種解決語法問題的最可靠、最權(quán)威的終極方法。
本章介紹如何讀懂 ECMAScript 6 的規(guī)格文件。
ECMAScript 6 的規(guī)格,可以在 ECMA 國際標(biāo)準(zhǔn)組織的官方網(wǎng)站(www.ecma-international.org/ecma-262/6.0/)免費(fèi)下載和在線閱讀。
這個規(guī)格文件相當(dāng)龐大,一共有 26 章,A4 打印的話,足足有 545 頁。它的特點(diǎn)就是規(guī)定得非常細(xì)致,每一個語法行為、每一個函數(shù)的實(shí)現(xiàn)都做了詳盡的清晰的描述?;旧?,編譯器作者只要把每一步翻譯成代碼就可以了。這很大程度上,保證了所有 ES6 實(shí)現(xiàn)都有一致的行為。
ECMAScript 6 規(guī)格的 26 章之中,第 1 章到第 3 章是對文件本身的介紹,與語言關(guān)系不大。第 4 章是對這門語言總體設(shè)計的描述,有興趣的讀者可以讀一下。第 5 章到第 8 章是語言宏觀層面的描述。第 5 章是規(guī)格的名詞解釋和寫法的介紹,第 6 章介紹數(shù)據(jù)類型,第 7 章介紹語言內(nèi)部用到的抽象操作,第 8 章介紹代碼如何運(yùn)行。第 9 章到第 26 章介紹具體的語法。
對于一般用戶來說,除了第 4 章,其他章節(jié)都涉及某一方面的細(xì)節(jié),不用通讀,只要在用到的時候,查閱相關(guān)章節(jié)即可。
ES6 規(guī)格使用了一些專門的術(shù)語,了解這些術(shù)語,可以幫助你讀懂規(guī)格。本節(jié)介紹其中的幾個。
所謂“抽象操作”
(abstract operations)就是引擎的一些內(nèi)部方法,外部不能調(diào)用。規(guī)格定義了一系列的抽象操作,規(guī)定了它們的行為,留給各種引擎自己去實(shí)現(xiàn)。
舉例來說,Boolean(value)
的算法,第一步是這樣的。
Let b be ToBoolean(value) .
這里的ToBoolean
就是一個抽象操作,是引擎內(nèi)部求出布爾值的算法。
許多函數(shù)的算法都會多次用到同樣的步驟,所以 ES6 規(guī)格將它們抽出來,定義成“抽象操作”,方便描述。
ES6
規(guī)格將鍵值對
(key-value map)的數(shù)據(jù)結(jié)構(gòu)稱為 Record
,其中的每一組鍵值對稱為field
。這就是說,一個 Record 由多個 field 組成,而每個 field 都包含一個鍵名(key)和一個鍵值(value)。
ES6
規(guī)格大量使用[[Notation]]
這種書寫法,比如 [[Value]] 、 [[Writable]] 、 [[Get]] 、 [[Set]] 等等。它用來指代field
的鍵名。
舉例來說, obj 是一個 Record,它有一個 Prototype 屬性。ES6 規(guī)格不會寫 obj.Prototype ,而是寫 obj.[[Prototype]] 。一般來說,使用 [[Notation]] 這種書寫法的屬性,都是對象的內(nèi)部屬性。
所有的 JavaScript 函數(shù)都有一個內(nèi)部屬性 [[Call]] ,用來運(yùn)行該函數(shù)。
F.[[Call]](V, argumentsList)
上面代碼中, F 是一個函數(shù)對象, [[Call]] 是它的內(nèi)部方法, F.[[call]]() 表示運(yùn)行該函數(shù), V 表示 [[Call]] 運(yùn)行時 this 的值, argumentsList 則是調(diào)用時傳入函數(shù)的參數(shù)。
每一個語句都會返回一個 Completion Record,表示運(yùn)行結(jié)果。每個 Completion Record 有一個 [[Type]]
屬性,表示運(yùn)行結(jié)果的類型。
[[Type]] 屬性有五種可能的值。
如果[[Type]]
的值是normal
,就稱為 normal completion,表示運(yùn)行正常。其他的值,都稱為 abrupt completion。其中,開發(fā)者只需要關(guān)注 [[Type]]
為 throw
的情況,即運(yùn)行出錯;break
、continue
、 return
這三個值都只出現(xiàn)在特定場景,可以不用考慮。
抽象操作的運(yùn)行流程,一般是下面這樣。
Let result be AbstractOp() .
If result is an abrupt completion, return result .
Set result to result.[[Value]] .
return result .
上面的第一步調(diào)用了抽象操作 AbstractOp() ,得到 result ,這是一個 Completion Record。第二步,如果 result 屬于 abrupt completion,就直接返回。如果此處沒有返回,表示 result 屬于 normal completion。第三步,將 result 的值設(shè)置為 resultCompletionRecord.[[Value]] 。第四步,返回 result 。
ES6 規(guī)格將這個標(biāo)準(zhǔn)流程,使用簡寫的方式表達(dá)。
Let result be AbstractOp() .
ReturnIfAbrupt(result) .
return result .
這個簡寫方式里面的 ReturnIfAbrupt(result) ,就代表了上面的第二步和第三步,即如果有報錯,就返回錯誤,否則取出值。
甚至還有進(jìn)一步的簡寫格式。
Let result be ? AbstractOp() .
return result .
上面流程的 ? ,就代表 AbstractOp() 可能會報錯。一旦報錯,就返回錯誤,否則取出值。
除了 ? ,ES 6 規(guī)格還使用另一個簡寫符號 ! 。
Let result be ! AbstractOp() .
return result .
上面流程的 ! ,代表 AbstractOp() 不會報錯,返回的一定是 normal completion,總是可以取出值。
下面通過一些例子,介紹如何使用這份規(guī)格。
相等運(yùn)算符( == )
是一個很讓人頭痛的運(yùn)算符,它的語法行為多變,不符合直覺。這個小節(jié)就看看規(guī)格怎么規(guī)定它的行為。
請看下面這個表達(dá)式,請問它的值是多少。
0 == null
如果你不確定答案,或者想知道語言內(nèi)部怎么處理,就可以去查看規(guī)格,7.2.12 小節(jié)是對相等運(yùn)算符( == )的描述。
規(guī)格對每一種語法行為的描述,都分成兩部分:先是總體的行為描述,然后是實(shí)現(xiàn)的算法細(xì)節(jié)。相等運(yùn)算符的總體描述,只有一句話。
“The comparison x == y , where x and y are values, produces true or false .”
上面這句話的意思是,相等運(yùn)算符用于比較兩個值,返回 true 或 false 。
下面是算法細(xì)節(jié)。
ReturnIfAbrupt(x).
ReturnIfAbrupt(y).
If Type(x) is the same as Type(y), then
Return the result of performing Strict Equality Comparison x === y .
If x is null and y is undefined , return true .
If x is undefined and y is null , return true .
If Type(x) is Number and Type(y) is String, return the result of the comparison x == ToNumber(y) .
If Type(x) is String and Type(y) is Number, return the result of the comparison ToNumber(x) == y .
If Type(x) is Boolean, return the result of the comparison ToNumber(x) == y .
If Type(y) is Boolean, return the result of the comparison x == ToNumber(y) .
If Type(x) is either String, Number, or Symbol and Type(y) is Object, then return the result of the comparison x == ToPrimitive(y) .
If Type(x) is Object and Type(y) is either String, Number, or Symbol, then return the result of the comparison ToPrimitive(x) == y .
Return false .
上面這段算法,一共有 12 步,翻譯如下。
如果 x 不是正常值(比如拋出一個錯誤),中斷執(zhí)行。
如果 y 不是正常值,中斷執(zhí)行。
如果 Type(x) 與 Type(y) 相同,執(zhí)行嚴(yán)格相等運(yùn)算 x === y 。
如果 x 是 null , y 是 undefined ,返回 true 。
如果 x 是 undefined , y 是 null ,返回 true 。
如果 Type(x) 是數(shù)值, Type(y) 是字符串,返回 x == ToNumber(y) 的結(jié)果。
如果 Type(x) 是字符串, Type(y) 是數(shù)值,返回 ToNumber(x) == y 的結(jié)果。
如果 Type(x) 是布爾值,返回 ToNumber(x) == y 的結(jié)果。
如果 Type(y) 是布爾值,返回 x == ToNumber(y) 的結(jié)果。
如果 Type(x) 是字符串或數(shù)值或 Symbol 值, Type(y) 是對象,返回 x == ToPrimitive(y) 的結(jié)果。
如果 Type(x) 是對象, Type(y) 是字符串或數(shù)值或 Symbol 值,返回 ToPrimitive(x) == y 的結(jié)果。
返回 false 。
由于 0 的類型是數(shù)值, null 的類型是 Null(這是規(guī)格4.3.13 小節(jié)的規(guī)定,是內(nèi)部 Type 運(yùn)算的結(jié)果,跟 typeof 運(yùn)算符無關(guān))。因此上面的前 11 步都得不到結(jié)果,要到第 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
上面代碼中,數(shù)組 a1 的成員是三個 undefined ,數(shù)組 a2 的成員是三個空位。這兩個數(shù)組很相似,長度都是 3,每個位置的成員讀取出來都是 undefined 。
但是,它們實(shí)際上存在重大差異。
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) // [, , ,]
上面代碼一共列出了四種運(yùn)算,數(shù)組 a1 和 a2 的結(jié)果都不一樣。前三種運(yùn)算( in 運(yùn)算符、數(shù)組的 hasOwnProperty 方法、 Object.keys 方法)都說明,數(shù)組 a2 取不到屬性名。最后一種運(yùn)算(數(shù)組的 map 方法)說明,數(shù)組 a2 沒有發(fā)生遍歷。
為什么 a1 與 a2 成員的行為不一致?數(shù)組的成員是 undefined 或空位,到底有什么不同?
上面的規(guī)格說得很清楚,數(shù)組的空位會反映在 length 屬性,也就是說空位有自己的位置,但是這個位置的值是未定義,即這個值是不存在的。如果一定要讀取,結(jié)果就是 undefined (因?yàn)?undefined 在 JavaScript 語言中表示不存在)。
這就解釋了為什么 in 運(yùn)算符、數(shù)組的 hasOwnProperty 方法、 Object.keys 方法,都取不到空位的屬性名。因?yàn)檫@個屬性名根本就不存在,規(guī)格里面沒說要為空位分配屬性名(位置索引),只說要為下一個元素的位置索引加 1。
至于為什么數(shù)組的 map 方法會跳過空位,請看下一節(jié)。
規(guī)格的22.1.3.15 小節(jié)定義了數(shù)組的 map
方法。該小節(jié)先是總體描述 map
方法的行為,里面沒有提到數(shù)組空位。
后面的算法描述是這樣的。
Let O be ToObject(this value) .
ReturnIfAbrupt(O) .
Let len be ToLength(Get(O, "length")) .
ReturnIfAbrupt(len) .
If IsCallable(callbackfn) is false , throw a TypeError exception.
If thisArg was supplied, let T be thisArg ; else let T be undefined .
Let A be ArraySpeciesCreate(O, len) .
ReturnIfAbrupt(A) .
Let k be 0.
Repeat, while k < len
Let Pk be ToString(k) .
Let kPresent be HasProperty(O, Pk) .
ReturnIfAbrupt(kPresent) .
If kPresent is true , then
Let kValue be Get(O, Pk) .
ReturnIfAbrupt(kValue) .
Let mappedValue be Call(callbackfn, T, ?kValue, k, O?) .
ReturnIfAbrupt(mappedValue) .
Let status be CreateDataPropertyOrThrow (A, Pk, mappedValue) .
ReturnIfAbrupt(status) .
Increase k by 1.
Return A .
翻譯如下。
仔細(xì)查看上面的算法,可以發(fā)現(xiàn),當(dāng)處理一個全是空位的數(shù)組時,前面步驟都沒有問題。進(jìn)入第 10 步中第 2 步時,kPresent
會報錯,因?yàn)榭瘴粚?yīng)的屬性名,對于數(shù)組來說是不存在的,因此就會返回,不會進(jìn)行后面的步驟。
const arr = [, , ,];
arr.map(n => {
console.log(n);
return 1;
}) // [, , ,]
上面代碼中,arr
是一個全是空位的數(shù)組, map 方法遍歷成員時,發(fā)現(xiàn)是空位,就直接跳過,不會進(jìn)入回調(diào)函數(shù)。因此,回調(diào)函數(shù)里面的 console.log
語句根本不會執(zhí)行,整個map
方法返回一個全是空位的新數(shù)組。
V8 引擎對 map 方法的實(shí)現(xiàn)如下,可以看到跟規(guī)格的算法描述完全一致。
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;
}
更多建議: