新聞中心
1. 概述
規(guī)格文件是計(jì)算機(jī)語(yǔ)言的官方標(biāo)準(zhǔn),詳細(xì)描述語(yǔ)法規(guī)則和實(shí)現(xiàn)方法。

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


咨詢
建站咨詢
