本文首發於我的部落格。
標準這事兒吧……
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)
。(但是根據 MDN,flatMap()
在效率上略勝一籌)
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 支援就好,哈哈哈哈哈哈。