你可能已經知道的 ES 2018 和 2019
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.ES 2019 為陣列新增兩個函式。
和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 支援就好,哈哈哈哈哈哈。