ES6 的規(guī)格文件

2020-06-11 15:13 更新

1. 概述

規(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é)即可。

2. 術(shù)語

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ī)格將它們抽出來,定義成“抽象操作”,方便描述。

Record 和 field

ES6 規(guī)格將鍵值對(key-value map)的數(shù)據(jù)結(jié)構(gòu)稱為 Record,其中的每一組鍵值對稱為field。這就是說,一個 Record 由多個 field 組成,而每個 field 都包含一個鍵名(key)和一個鍵值(value)。

[[Notation]]

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

每一個語句都會返回一個 Completion Record,表示運(yùn)行結(jié)果。每個 Completion Record 有一個 [[Type]]屬性,表示運(yùn)行結(jié)果的類型。

[[Type]] 屬性有五種可能的值。

  • normal
  • return
  • throw
  • break
  • continue

如果[[Type]] 的值是normal ,就稱為 normal completion,表示運(yùn)行正常。其他的值,都稱為 abrupt completion。其中,開發(fā)者只需要關(guān)注 [[Type]]throw 的情況,即運(yùn)行出錯;break 、continuereturn 這三個值都只出現(xiàn)在特定場景,可以不用考慮。

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 ,這是一個 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,總是可以取出值。

4. 相等運(yùn)算符

下面通過一些例子,介紹如何使用這份規(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

5. 數(shù)組的空位

下面再看另一個例子。

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é)。

6. 數(shù)組的 map 方法

規(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 .

翻譯如下。

  1. 得到當(dāng)前數(shù)組的 this 對象
  2. 如果報錯就返回
  3. 求出當(dāng)前數(shù)組的 length 屬性
  4. 如果報錯就返回
  5. 如果 map 方法的參數(shù) callbackfn 不可執(zhí)行,就報錯
  6. 如果 map 方法的參數(shù)之中,指定了 this ,就讓 T 等于該參數(shù),否則 T 為 undefined
  7. 生成一個新的數(shù)組 A ,跟當(dāng)前數(shù)組的 length 屬性保持一致
  8. 如果報錯就返回
  9. 設(shè)定 k 等于 0
  10. 只要 k 小于當(dāng)前數(shù)組的 length 屬性,就重復(fù)下面步驟
    1. 設(shè)定 Pk 等于 ToString(k) ,即將 K 轉(zhuǎn)為字符串
    2. 設(shè)定 kPresent 等于 HasProperty(O, Pk) ,即求當(dāng)前數(shù)組有沒有指定屬性
    3. 如果報錯就返回
    4. 如果 kPresent 等于 true ,則進(jìn)行下面步驟
      1. 設(shè)定 kValue 等于 Get(O, Pk) ,取出當(dāng)前數(shù)組的指定屬性
      2. 如果報錯就返回
      3. 設(shè)定 mappedValue 等于 Call(callbackfn, T, ?kValue, k, O?) ,即執(zhí)行回調(diào)函數(shù)
      4. 如果報錯就返回
      5. 設(shè)定 status 等于 CreateDataPropertyOrThrow (A, Pk, mappedValue) ,即將回調(diào)函數(shù)的值放入 A 數(shù)組的指定位置
      6. 如果報錯就返回
    5. k 增加 1
  11. 返回 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;
}
以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號