你可能已經知道的 ES 2018 和 2019

WashingtonHua發表於2019-02-17

本文首發於我的部落格

標準這事兒吧……

ES 2019(ES 10)標準於年前正式釋出,藉此機會,我們來看看都有哪些特性有幸轉正吧。順帶把 ES 2018 的內容也補一下。

ECMAScript 標準的制定過程,自 2015 年大改,至今已經是第 5 個年頭了,想必大家都心裡有數了。與 Java 等語言不同,JS 並非先制定標準再開始使用,恰恰相反,是大家先用著,覺得合適的,才收錄進標準。標準的存在更像是一個“年度優秀特性合集”。對絕大部分開發者來說,一項特性進沒進標準不重要,Babel 支不支援才重要。標準你隨便寫,不用 Babel 算我輸。

那麼接下來,我們就來看看 2018 和 2019 兩個年度的大合集都有些啥吧。

ES2018(ES9)

1)非同步迭代器(Asynchronous Iteration)

總有那麼些時候,我們會想要同步執行一些非同步的操作,比如下面這樣的:

const actions = [
  Promise.resolve(1),
  Promise.resolve(2),
  Promise.resolve(3)
]
複製程式碼

利用 async / await 語法,我們可以很輕鬆的做到這點。

async function process (actions) {
  for (const action of actions) {
    await asyncFunc(action)
  }
}
複製程式碼

上面的寫法,會按順序執行 asyncFunc,上一個結束之後才會開始下一個,每次得到的 action 都是一個非同步操作本身(比如這裡是一個 Promise 物件)。

ES 2018 為我們提供了一種新的方式,在前面程式碼的基礎之上,讓每次得到的 action 直接是非同步操作完成之後的結果(比如這裡是 Promise 被 resolve 之後的結果)。

async function process (actions) {
  for await (const action of actions) {
    asyncFunc(action)
  }
}
複製程式碼

2)Rest/Spread Properties 開始適用於物件

這是一個從 ES 2015 開始就被廣泛使用的特性,只不過 ES 2015 的標準只支援用於陣列,從 ES 2018 開始也支援物件了。

事實上 Map、Set、String 同樣支援 ...,但具體是哪個版本引入的我還真沒數。(反正我已經用了很久了,不管了)

3)Promise.finally

正如它的名字,finally。這也是個用了好久終於進標準的特性。

在處理 Promise 的返回時,我們經常會遇到這樣的情況:無論結果狀態是 resolved 還是 rejected,都執行一樣的邏輯。

早先遇到這種情況,我們不得不在 then()catch() 裡都寫一遍,現在可以一次性寫在 finally() 裡。一個 finally() 就等價於一組回撥函式相同的 then()catch()

雖然名字叫“最終”,但並不代表這是 Promise 執行的終點。finally() 後面還可以繼續跟 then()catch(),無限跟。

4)移除對“在‘帶標籤的模版字面量’中使用非法轉義序列”的限制

從這裡開始的內容比較高階,一般用不到,趕時間的話你可以跳過,直接去看 ES 2019。

這一節的標題有點繞,我們拆開來講。首先是“帶標籤的模版字面量”。

ES 2015 引入了“模板字面量”的特性,相信大家都很熟悉了,長這樣:

const name = 'John'
const greetings = `Hi, ${name}` // 'Hi, John'
複製程式碼

這個特性有一個生僻用法,它允許我們自定義一個字串模板函式,比如下面這樣:

function myTag(strings, ...params) {
  // strings: ['that ', ' is a ', '']

  const name = params[0]
  const age = params[1]
  const title = age > 99 ? 'centenarian' : 'youngster'

  return strings[0] + name + strings[1] + title
}

const person = 'Mike'
const age = 28
const output = myTag`that ${ person } is a ${ age }`
// that Mike is a youngster
複製程式碼

這就是“帶標籤的模版字面量”。儘管我嚴重懷疑這個用法的實用性(或許是覺得這樣更加語義化?普通函式語義也不差啊?),但 ES 2018 還是選擇了對這個特性進行完善。

ES 2016 為這個特性加入了對轉義序列的支援,比如八進位制(\ 開頭)、十六進位制(\x 開頭)、Unicode 字元(\u 開頭),但前提必須是一個有效的轉義序列。如果是無效的序列,會報錯。

latex`\u00A9`   // 合法,表示“版權符號”
latex`\unicode` // 不合法,報錯
複製程式碼

ES 2018 去掉了這個限制,主要是考慮到對一些領域特定語言的支援,比如 LaTeX。(學術界一種常用的標記型語言,類似 HTML,其語法會用到大量形如轉義序列的指令,如\section\frac\sum 等)

但去掉限制只是說不報錯了,模板中的無效轉義序列會被替換為 undefined。比如下面這樣:

function myTag (template, ...params) {
  console.log({ template, params })
}

const foo = 'foo'
const bar = 'bar'
myTag`aaa${foo}\unicode${bar}bbb`
/* {
  template: ['aaa', undefined, 'bbb', raw: ['aaa', '\unicode', 'bbb]],
  params: ['foo', 'bar']
} */
複製程式碼

上面的程式碼裡,template 是模板部分被 ${foo} 等變數分割形成的陣列;params 就是 ${foo} 等變數組成的陣列。可以看到,\unicode 由於是無效的轉義序列,被替換為 undefined,但在 template.raw 裡得以保留。

template.raw 是“帶標籤的模版字面量”中 template 引數特有的一個屬性,儲存了未被替換的原始字串。

這樣一來,既避免了報錯,又保留了開發者自行處理這些轉義序列的能力。

5)關於正規表示式的一些改進

5.1)s 標誌(dotAll 模式)

在正規表示式中,點號 . 表示匹配任一單個字元,但這不包含換行符(如:\n\r\f 等)。

現在可以通過在尾部增加 s 標誌的方式,讓它匹配了。

/hello.world/.test('hello\nworld')  // false
/hello.world/s.test('hello\nworld') // true
複製程式碼

5.2)擴充套件 Unicode 匹配範圍

一直以來,要編寫正規表示式來匹配各種 Unicode 字元並不容易,像 \w\W\d 等都只能匹配英文字元和數字,對於除此之外的字元就很難匹配了,例如非英語的文字。

幸運的是,Unicode 為每個符號新增了後設資料屬性,並使用它來對各種符號進行分組和描述。例如,Unicode 資料庫給所有印地語字元(हिन्दी)設定了 Script 屬性,取值為 Devanagari(梵文),還設定了一個 Script_Extensions 屬性,同樣取值為 Devanagari。我們可以通過搜尋 Script=Devanagari 來得到所有印地文字元。

ES 2018 允許正規表示式通過 \p{...} 來擴充套件 Unicode 符號的匹配範圍。例如:

// 擴充套件匹配範圍,允許匹配希臘字元
const reGreekSymbol = /\p{Script=Greek}/u
reGreekSymbol.test('π') // true

// 擴充套件匹配範圍,允許匹配 Emoji
const reEmoji = /\p{Emoji}\p{Emoji_Modifier}/u
reEmoji.test('✌?') //true
複製程式碼

我們還可以通過 \P{...}(注意,大寫 P)來去反,縮小匹配範圍。

5.3)正規表示式命名捕獲組

正規表示式支援通過括號在一個表示式中指定多個捕獲組,就像下面這樣:

const
  reDate = /([0-9]{4})-([0-9]{2})-([0-9]{2})/,
  match  = reDate.exec('2019-02-11'),
  year   = match[1], // 2019
  month  = match[2], // 02
  day    = match[3]; // 11
複製程式碼

這樣的程式碼雖然可以跑通,但閱讀起來比較難懂,而且修改正則有可能會影響到匹配內容的索引。

ES 2018 允許在 ( 後立即使用符號 ?<name> 對捕獲組進行命名,匹配失敗的會返回 undefined,就像下面這樣:

const
  reDate = /(?<year>[0-9]{4})-(?<month>[0-9]{2})-(?<day>[0-9]{2})/,
  match  = reDate.exec('2019-02-11'),
  year   = match.groups.year,  // 2019
  month  = match.groups.month, // 02
  day    = match.groups.day   // 11
複製程式碼

命名捕獲組也可以用在 replace() 中,用 $<name> 進行引用(注意,雖然這裡的語法和模板字面量很像,但並不是)。例如改變日期格式的順序:

const
  reDate = /(?<year>[0-9]{4})-(?<month>[0-9]{2})-(?<day>[0-9]{2})/,
  d      = '2019-02-11',
  usDate = d.replace(reDate, '$<month>-$<day>-$<year>'); // 02-11-2019
複製程式碼

5.4)正規表示式的反向斷言(lookbehind)

正規表示式支援正向斷言(lookahead),例如:

// 正向肯定查詢
/x(?=y)/ // 匹配 x,但僅當 x 後面緊跟著 y 時
/Jack(?=Sprat)/.exec('JackSprat') // 'Jack'
/Jack(?=Sprat)/.exec('JackFrost') // null
/Jack(?=Sprat|Frost)/.exec('JackFrost') // 'Jack'

// 正向否定查詢
/x(?!y)/ // 匹配 x,但僅當 x 後面不緊跟著 y 時
/Jack(?!Sprat)/.exec('JackSprat') // null
/Jack(?!Sprat)/.exec('Jack Sprat') // 'Jack'
複製程式碼

ES 2018 引入了工作方式相同,但是方向相反的反向斷言(lookbehind),語法上的差別就在於 ? 變成了 ?<,例如:

// 反向肯定斷言
/(?<x)y/ // 匹配 y,但僅當它緊跟在 x 後面時
/(?<=\D)\d+/.exec('$123.89')[0] // 123.89

// 反向否定斷言
/(?<!x)y/ // 匹配 y,但僅當它緊跟在 x 後面時
/(?<!\D)\d+/.exec('$123.89')[0] // null
複製程式碼

ES 2019(ES 10)

1)JSON 成為 ECMAScript 的完全子集

從學習 JSON 的第一課起,我們就被告知 JSON 應該是專為 JavaScript 而存在的,因此 JSON 是 JavaScript 的子集這一點應該毫無爭議啊,這算什麼新特性!?

然而細心的開發者卻發現,有兩個符號是例外:行分隔符(U + 2028)和段分隔符(U + 2029)。在 JSON.parse() 中使用這兩個會報語法錯誤。

ES 2019 把這兩個也收入囊中,從今往後,JSON 真正成為 ECMAScript 的完全子集,一個都不少。

2)更友好的 JSON.stringify()

過去,對於一些超出 Unicode 範圍的轉義序列,JSON.stringify() 會輸出未知字元。

JSON.stringify('\uDF06\uD834'); // '"��"'
JSON.stringify('\uDEAD'); // '"�"'
複製程式碼

現在,JSON.stringify() 會為其重新轉義,顯示為有效的 Unicode 序列。

JSON.stringify('\uDF06\uD834'); // '"\\udf06\\ud834"'
JSON.stringify('\uDEAD'); // '"\\udead"'
複製程式碼

這和 ES 2018 中對“帶標籤的模板字面量”的修正,似乎有些許聯絡。結合歷代 ECMAScript 標準,ECMAScript 在處理 Unicode 的問題上著實下了不少功夫。

3)Function.prototpye.toString() 顯示更加完善

對一個函式使用 toString() 會返回函式定義的內容。

過去,返回的內容中 function 關鍵字和函式名之間的註釋,以及函式名和引數列表左括號之間的空格,是不會被打出來的。ES 2019 現在回精確返回這些內容,函式怎麼定義的,這就就怎麼顯示。

4)Array.prorptype.flat()Array.prorptype.flatMap()

ES 2019 為陣列新增兩個函式。

flat() 用於對陣列進行降維,它可以接收一個引數,用於指定降多少維,預設為 1。降維最多降到一維。

const array = [1, [2, [3]]]
array.flat() // [1, 2, [3]]
array.flat(1) // [1, 2, [3]],預設降 1 維
array.flat(2) // [1, 2, 3]
array.flat(3) // [1, 2, 3],最多降到一維
複製程式碼

flatMap() 允許在對陣列進行降維之前,先進行一輪對映,用法和 map() 一樣。然後再將對映的結果降低一個維度。可以說 arr.flatMap(fn) 等效於 arr.map(fn).flat(1)。(但是根據 MDNflatMap() 在效率上略勝一籌)

flatMap() 也可以等效為 reduce()concat() 的組合,下面這個案例來自 MDN,但是……這不是一個 map 就能搞定的事麼?

var arr1 = [1, 2, 3, 4];

arr1.flatMap(x => [x * 2]);
// 等價於
arr1.reduce((acc, x) => acc.concat([x * 2]), []);
// [2, 4, 6, 8]
複製程式碼

flat()flatMap() 都是返回新的陣列,原陣列不變。

5)String.prototype.trimStart()String.prototype.trimEnd()

ES 2019 為字串也新增了兩個函式:trimStart()trimEnd()。用過 trim() 的朋友都知道了,這兩個函式各自負責只去掉單邊的多餘空格。trim() 是兩邊都去。

6)Object.fromEntries()

從名字就能看出來,這是 Object.entries() 的逆過程。

7)Symbol.prototype.description

Symbol 是 ES 2015 引入的新的原始型別,通常在建立 Symbol 時我們會附加一段描述。過去,只有把這個 Symbol 轉成 String 才能看到這段描述,而且外層還套了個 'Symbol()' 字樣。ES 2019 為 Symbol 新增了 description 屬性,專門用於檢視這段描述。

const sym = Symbol('The description');
String(sym) // 'Symbol(The description)'
sym.description // 'The description'
複製程式碼

8)可選的 catch 繫結

try...catch 的語法大家都很熟悉了,過去,catch 後面必須有一組括號,裡面用一個變數(通常叫 e 或者 err)代表錯誤資訊物件。現在這部分是可選的了,如果異常處理部分不需要錯誤資訊,我們可以把它省略,像寫 if...else 一樣寫 try...catch

try {
  throw new Error('Some Error')
} catch {
  handleError() // 這裡沒有用到錯誤資訊,可以省略 catch 後面的 (e)。
}
複製程式碼

遺憾

ES 2019 收錄了非常多好用的特性,但還是有很多我們非常熟悉,甚至已經用了好久的特性沒能進入標準,比如:

  • Stage 3(明年見?)
    • Dynamic Import
    • 私有屬性
  • Stage 2(加油?)
    • 裝飾器
  • Stage 1(你們慢慢討論,我們先用為敬)
    • Observable
    • Promise.try
    • String.prototype.replaceAll
    • do

不過這不重要,標準只是官宣,只要 Babel 支援就好,哈哈哈哈哈哈。

相關文章