規(guī)格文件是計(jì)算機(jī)語言的官方標(biāo)準(zhǔn),詳細(xì)描述語法規(guī)則和實(shí)現(xiàn)方法。
一般來說,沒有必要閱讀規(guī)格,除非你要寫編譯器。因?yàn)橐?guī)格寫得非常抽象和精煉,又缺乏實(shí)例,不容易理解,而且對(duì)于解決實(shí)際的應(yīng)用問題,幫助不大。但是,如果你遇到疑難的語法問題,實(shí)在找不到答案,這時(shí)可以去查看規(guī)格文件,了解語言標(biāo)準(zhǔn)是怎么說的。規(guī)格是解決問題的“最后一招”。
這對(duì)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)下載和在線閱讀。
這個(gè)規(guī)格文件相當(dāng)龐大,一共有26章,A4打印的話,足足有545頁。它的特點(diǎn)就是規(guī)定得非常細(xì)致,每一個(gè)語法行為、每一個(gè)函數(shù)的實(shí)現(xiàn)都做了詳盡的清晰的描述?;旧希幾g器作者只要把每一步翻譯成代碼就可以了。這很大程度上,保證了所有ES6實(shí)現(xiàn)都有一致的行為。
ECMAScript 6規(guī)格的26章之中,第1章到第3章是對(duì)文件本身的介紹,與語言關(guān)系不大。第4章是對(duì)這門語言總體設(shè)計(jì)的描述,有興趣的讀者可以讀一下。第5章到第8章是語言宏觀層面的描述。第5章是規(guī)格的名詞解釋和寫法的介紹,第6章介紹數(shù)據(jù)類型,第7章介紹語言內(nèi)部用到的抽象操作,第8章介紹代碼如何運(yùn)行。第9章到第26章介紹具體的語法。
對(duì)于一般用戶來說,除了第4章,其他章節(jié)都涉及某一方面的細(xì)節(jié),不用通讀,只要在用到的時(shí)候,查閱相關(guān)章節(jié)即可。下面通過一些例子,介紹如何使用這份規(guī)格。
相等運(yùn)算符(==
)是一個(gè)很讓人頭痛的運(yùn)算符,它的語法行為多變,不符合直覺。這個(gè)小節(jié)就看看規(guī)格怎么規(guī)定它的行為。
請(qǐng)看下面這個(gè)表達(dá)式,請(qǐng)問它的值是多少。
0 == null
如果你不確定答案,或者想知道語言內(nèi)部怎么處理,就可以去查看規(guī)格,7.2.12小節(jié)是對(duì)相等運(yùn)算符(==
)的描述。
規(guī)格對(duì)每一種語法行為的描述,都分成兩部分:先是總體的行為描述,然后是實(shí)現(xiàn)的算法細(xì)節(jié)。相等運(yùn)算符的總體描述,只有一句話。
“The comparison
x == y
, wherex
andy
are values, producestrue
orfalse
.”
上面這句話的意思是,相等運(yùn)算符用于比較兩個(gè)值,返回true
或false
。
下面是算法細(xì)節(jié)。
- ReturnIfAbrupt(x).
- ReturnIfAbrupt(y).
- If
Type(x)
is the same asType(y)
, then
Return the result of performing Strict Equality Comparisonx === 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
不是正常值(比如拋出一個(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)算符無關(guān))。因此上面的前11步都得不到結(jié)果,要到第12步才能得到false
。
0 == null // false
下面再看另一個(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ù)組很相似,長度都是3,每個(gè)位置的成員讀取出來都是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ī)格的12.2.5小節(jié)《數(shù)組的初始化》給出了答案。
“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.”
翻譯如下。
"數(shù)組成員可以省略。只要逗號(hào)前面沒有任何表達(dá)式,數(shù)組的
length
屬性就會(huì)加1,并且相應(yīng)增加其后成員的位置索引。被省略的成員不會(huì)被定義。如果被省略的成員是數(shù)組最后一個(gè)成員,則不會(huì)導(dǎo)致數(shù)組length
屬性增加。”
上面的規(guī)格說得很清楚,數(shù)組的空位會(huì)反映在length
屬性,也就是說空位有自己的位置,但是這個(gè)位置的值是未定義,即這個(gè)值是不存在的。如果一定要讀取,結(jié)果就是undefined
(因?yàn)?code>undefined在JavaScript語言中表示不存在)。
這就解釋了為什么in
運(yùn)算符、數(shù)組的hasOwnProperty
方法、Object.keys
方法,都取不到空位的屬性名。因?yàn)檫@個(gè)屬性名根本就不存在,規(guī)格里面沒說要為空位分配屬性名(位置索引),只說要為下一個(gè)元素的位置索引加1。
至于為什么數(shù)組的map
方法會(huì)跳過空位,請(qǐng)看下一節(jié)。
規(guī)格的22.1.3.15小節(jié)定義了數(shù)組的map
方法。該小節(jié)先是總體描述map
方法的行為,里面沒有提到數(shù)組空位。
后面的算法描述是這樣的。
- 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
a. LetPk
beToString(k)
.
b. LetkPresent
beHasProperty(O, Pk)
.
c.ReturnIfAbrupt(kPresent)
.
d. IfkPresent
istrue
, then
d-1. LetkValue
beGet(O, Pk)
.
d-2.ReturnIfAbrupt(kValue)
.
d-3. LetmappedValue
beCall(callbackfn, T, ?kValue, k, O?)
.
d-4.ReturnIfAbrupt(mappedValue)
.
d-5. Letstatus
beCreateDataPropertyOrThrow (A, Pk, mappedValue)
.
d-6.ReturnIfAbrupt(status)
.
e. Increasek
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ù)下面步驟
a. 設(shè)定Pk
等于ToString(k)
,即將K
轉(zhuǎn)為字符串
b. 設(shè)定kPresent
等于HasProperty(O, Pk)
,即求當(dāng)前數(shù)組有沒有指定屬性
c. 如果報(bào)錯(cuò)就返回
d. 如果kPresent
等于true
,則進(jìn)行下面步驟
d-1. 設(shè)定kValue
等于Get(O, Pk)
,取出當(dāng)前數(shù)組的指定屬性
d-2. 如果報(bào)錯(cuò)就返回
d-3. 設(shè)定mappedValue
等于Call(callbackfn, T, ?kValue, k, O?)
,即執(zhí)行回調(diào)函數(shù)
d-4. 如果報(bào)錯(cuò)就返回
d-5. 設(shè)定status
等于CreateDataPropertyOrThrow (A, Pk, mappedValue)
,即將回調(diào)函數(shù)的值放入A
數(shù)組的指定位置
d-6. 如果報(bào)錯(cuò)就返回
e.k
增加1- 返回
A
仔細(xì)查看上面的算法,可以發(fā)現(xiàn),當(dāng)處理一個(gè)全是空位的數(shù)組時(shí),前面步驟都沒有問題。進(jìn)入第10步的b時(shí),kpresent
會(huì)報(bào)錯(cuò),因?yàn)榭瘴粚?duì)應(yīng)的屬性名,對(duì)于數(shù)組來說是不存在的,因此就會(huì)返回,不會(huì)進(jìn)行后面的步驟。
const arr = [, , ,];
arr.map(n => {
console.log(n);
return 1;
}) // [, , ,]
上面代碼中,arr
是一個(gè)全是空位的數(shù)組,map
方法遍歷成員時(shí),發(fā)現(xiàn)是空位,就直接跳過,不會(huì)進(jìn)入回調(diào)函數(shù)。因此,回調(diào)函數(shù)里面的console.log
語句根本不會(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;
}
更多建議: