一。概覽
2019 年 6 月發布了 ES2019 規範,即 ES10
包括 4 個新特性:
-
String.prototype.{trimStart,trimEnd}:規範化字符串 trim 方法(廣泛實現的非規範版本叫
String.prototype.trimLeft/trimRight) -
Symbol.prototype.description:返回 Symbol 的描述信息
以及 6 個語法/語義上的變化:
-
Optional catch binding:允許省略
try-catch結構中catch塊的參數部分 -
Array.prototype.sort:要求排序算法必須是穩定的(相等元素排序前後順序不變) -
Well-formed JSON.stringify:要求
JSON.stringify返回格式良好的 UTF-8 字符串 -
JSON superset:字符串字面量中允許出現
U+2028(LINE SEPARATOR) 和U+2029(PARAGRAPH SEPARATOR) -
Function.prototype.toString revision:要求返回 function 源碼文本,或標準佔位符
P.S.V8 v7.3+、Chrome 73+ 支持 ES2019 所有特性
二。Array.prototype.{flat,flatMap}
flat
Array.prototype.flat( [ depth ] )
即用來打平數組的 flatten 方法,支持一個可選的 depth 參數,表示打平指定層數(預設為 1):
[[1], [[2]], [[[3]]]].flat()
// 得到 [1, [2], [[3]]]
[[1], [[2]], [[[3]]]].flat(Infinity)
// 得到 [1, 2, 3]
簡單實現如下:
const flat = (arr, depth = 1) => {
if (depth > 0) {
const flated = Array.prototype.concat.apply([], arr);
// 或者
// const flated = arr.reduce((a, v) => a.concat(v), []);
const isFullFlated = flated.reduce((a, v) => a && !Array.isArray(v), true);
return isFullFlated ? flated : flat(flated, depth - 1);
}
return arr;
};
flatMap
Array.prototype.flatMap ( mapperFunction [ , thisArg ] )
P.S. 可選參數 thisArg 用作 mapperFunction 中的 this,例如:
[1, 2, 3, 4].flatMap(function(x) {
return this.value ** x;
}, { value: 2 })
// 得到 [2, 4, 8, 16]
作用上,flatMap 與 map 類似,主要區別在於:map 做一對一的映射,而 flatMap 支持一對多(也可以對應 0 個)
例如:
[2, 0, 1, 9].flatMap(x => new Array(x).fill(x))
// 得到 [2, 2, 1, 9, 9, 9, 9, 9, 9, 9, 9, 9]
相當於將每個元素映射成一個數組,最後再打平一層:
// 不考慮性能的話,可以這樣簡單實現
// const flatMap = (arr, f) => arr.map(f).flat();
// 或者
// const flatMap = (arr, f) => arr.reduce((a, v) => a.concat(f(v)), []);
主要有 2 個應用場景:
-
map + filter:返回空數組表示一對零,即 filter
-
一對多映射
例如列出指定目錄下所有非隱藏文件:
// node 12.10.0
const fs = require('fs');
const path = require('path');
// map + filter 結合 一對多映射
const listFiles = dir => fs.readdirSync(dir).flatMap(f => {
if (f.startsWith('.')) return [];
const filePath = path.join(dir, f);
return fs.statSync(filePath).isDirectory() ? listFiles(filePath) : filePath;
});
三。Object.fromEntries
Object.fromEntries ( iterable )
用於將一組鍵值對兒轉換成對象,相當於 Object.entries 逆運算,用來補足數據類型轉換上的缺失(key-value pairs to Object):
const entries = Object.entries({ a: 1, b: 2 });
// 得到 [["a", 1], ["b", 2]]
const obj = Object.fromEntries(entries);
// 得到 {a: 1, b: 2}
類似於 lodash 提供的 _.fromPairs(pairs),簡單實現如下:
const fromEntries = pairs => pairs.reduce((acc, [ key, val ]) => Object.assign(acc, { [key]: val }), {});
P.S. 官方 polyfill 見 es-shims/Object.fromEntries
特殊的:
-
如果存在 key 相同的鍵值對兒,後面的覆蓋之前的
-
支持用 Symbol 作為 key(而
Object.entries會忽略 Symbol key) -
鍵值對兒中非 String/Symbol 類型的 key 會被強制轉成 String
-
參數支持 iterable,不限於數組
-
只支持創建可枚舉的、數據屬性
例如:
// 1. 如果存在 key 相同的鍵值對兒,後面的覆蓋之前的
Object.fromEntries([['a', 1], ['b', 2], ['a', 3]]);
// 得到 {a: 3, b: 2}
// 2. 支持用 Symbol 作為 key(而 `Object.entries` 會忽略 Symbol key)
Object.fromEntries([[Symbol('a'), 1], ['b', 2]]);
// 得到 {b: 2, Symbol(a): 1}
// 3. 鍵值對兒中非 String/Symbol 類型的 key 會被強制轉成 String
Object.fromEntries([[new Error('here'), 1], [{}, 2]]);
// 得到 {['Error: here']: 1, ['[object Object]']: 2}
// 4. 參數支持 iterable,不限於數組
Object.fromEntries(function*(){
yield ['a', 1];
yield ['b', 2];
}());
// 得到 {a: 1, b: 2}
// 5. 只支持創建可枚舉的、數據屬性
Object.getOwnPropertyDescriptors(Object.fromEntries([['a', 1]]))
// 得到 { a: {value: 1, writable: true, enumerable: true, configurable: true} }
四。String.prototype.{trimStart,trimEnd}
算是 trimLeft/trimRight 的標準定義,命名上是為了與 ES2017 的 padStart/padEnd 保持一致
功能上,空白字符及換行符會被 trim 掉:
// 空白字符 https://tc39.github.io/ecma262/#sec-white-space
'\u0009' // <TAB> CHARACTER TABULATION
'\u000B' // <VT> LINE TABULATION
'\u000C' // <FF> FORM FEED
'\u0020' // <SP> SPACE
'\u00A0' // <NBSP> NO-BREAK SPACE
'\uFEFF' // <ZWNBSP> (ZERO WIDTH NO-BREAK SPACE
// ...以及其它 Space_Separator 類下具有 White_Space 屬性的 Unicode 字符
// 換行符 https://tc39.github.io/ecma262/#sec-line-terminators
'\u000A' // <LF> LINE FEED
'\u000D' // <CR> CARRIAGE RETURN
'\u2028' // <LS> LINE SEPARATOR
'\u2029' // <PS> PARAGRAPH SEPARATOR
例如:
'\u0009\u000B\u000C\u0020\u00A0\uFEFF\u000A\u000D\u2028\u2029'.trim().length === 0
'\u0009\u000B\u000C\u0020\u00A0\uFEFF\u000A\u000D\u2028\u2029'.trimStart().length === 0
'\u0009\u000B\u000C\u0020\u00A0\uFEFF\u000A\u000D\u2028\u2029'.trimEnd().length === 0
另外,向後兼容起見,trimLeft/trimRight 仍然保留,定義在規範 Annex B(B Additional ECMAScript Features for Web Browsers,要求 Web 瀏覽器實現)中,但建議使用 trimStart/trimEnd:
The property trimStart is preferred. The trimLeft property is provided principally for compatibility with old code. It is recommended that the trimStart property be used in new ECMAScript code.
二者是別名關係,於是,有趣的事情發生了:
String.prototype.trimLeft.name === 'trimStart'
String.prototype.trimRight.name === 'trimEnd'
五。Symbol.prototype.description
允許通過 Symbol.prototype.description 訪問 [創建 Symbol 時傳入的 description 參數](/articles/symbol-es6 筆記 7/#articleHeader3),例如:
const mySymbol = Symbol('my description for this Symbol');
mySymbol.description === 'my description for this Symbol'
之前只能通過 toString 截取該描述信息:
mySymbol.toString().match(/Symbol\(([^)]*)\)$/)[1]
P.S.description 屬性是只讀的:
Symbol.prototype.description is an accessor property whose set accessor function is undefined.
六。語法/語義變化
Optional catch binding
對於預料之中的異常,通常這樣做:
try {
JSON.parse('');
} catch(err) { /* noop */ }
沒有用到 err 參數,但必須聲明。因為省去參數的話,存在語法解析錯誤:
try {
JSON.parse('');
} catch() { }
// 報錯 Uncaught SyntaxError: Unexpected token )
而 ES2019 允許省略 try-catch 結構中 catch 塊的參數部分:
Allow developers to use try/catch without creating an unused binding
語法上,支持兩種形式的 catch 塊:
// 帶參數部分的 catch 塊
catch( CatchParameter[?Yield, ?Await] ) Block[?Yield, ?Await, ?Return]
// 省略參數部分的 catch 塊
catch Block[?Yield, ?Await, ?Return]
例如:
// node 12.10.0
const parseJSON = (str = '') => {
let json;
try {
json = JSON.parse(str);
} catch {
consle.error('parseJSON error, just ignore it.');
}
};
parseJSON('');
// 輸出 parseJSON error, just ignore it.
理論上,大多數場景中的異常信息都不應該忽略(要麼記錄下來,要麼拋出去,要麼想辦法善後),相對合理的幾種場景 有:
-
assert.throws(func):用於測試驅動庫,斷言執行指定函數會拋出異常(不關心是何種異常) -
瀏覽器特性檢測:只想知道是否支持特定特性
-
善後措施異常:比如
logError()自身出現異常,即便能捕獲到也無計可施了
P.S. 即便在這些場景,決定忽略一個異常時也應該在註釋中說明原因
Array.prototype.sort
要求必須是穩定排序(排序前後相等元素的相對順序保持不變):
The sort must be stable (that is, elements that compare equal must remain in their original order).
例如:
const words = [{ id: 1, value: 'I' }, { id: 3, value: 'am' }, { id: 1, value: 'feeling' }, { id: 4, value: 'lucky' }];
words.sort((a, b) => a.id - b.id);
console.log(words.map(v => v.value).join(' '));
// 期望結果是 I feeling am lucky
// 而不是 feeling I am lucky
Well-formed JSON.stringify
JSON 規範 要求廣泛通用的 JSON 應該用 UTF-8 編碼:
JSON text exchanged between systems that are not part of a closed ecosystem MUST be encoded using UTF-8.
而 JavaScript 中,對於單獨出現的半個 代理對兒,JSON.stringify() 時存在問題:
JSON.stringify('\uD800')
// 得到 '"?"'
實際上,JSON 支持 \u 形式的轉義語法,所以 ES2019 要求 JSON.stringify() 返回格式正確的 UTF-8 編碼字符串:
JSON.stringify('\uD800');
// 得到 '"\\ud800"'
算是對 JSON.stringify() 的 bug 修復
P.S. 關於 JavaScript 中 Unicode 的更多信息,見 JavaScript 中的 Unicode
JSON superset
字面量形式的(未經轉義的)U+2028 和 U+2029 字符在 JSON 中是合法的,而在 JavaScript 字符串字面量中是非法字符:
const LS = "?";
const PS = eval("'\u2029'");
// 報錯 Uncaught SyntaxError: Invalid or unexpected token
ES2019 規範要求字符串字面量支持完整的 JSON 字符集,即JavaScript 作為 JSON 的超集。在支持 ES2019 的環境中,對於雙引號/單引號中的 U+2028 和 U+2029 字符,不再拋出以上語法錯誤(正則表達式字面量中仍然不允許出現這兩個字符)
P.S.[模板字符串](/articles/模板字符串-es6 筆記 3/#articleHeader5) 不存在這個問題:
const LS = `?`;
const PS = eval("`\u2029`");
Function.prototype.toString revision
要求返回 function 源碼文本,或標準佔位符:
implementations must not be required to retain source text for all functions defined using ECMAScript code
具體如下:
-
如果函數是通過 ES 代碼創建的,
toString()必須返回其源碼 -
如果
toString()無法得到合法的 ES 代碼,就返回標準佔位符,佔位符串一定不能是合法的 ES 代碼(eval(佔位符)必定拋出SyntaxError)
P.S. 規範建議的佔位符形式為 "function" BindingIdentifier? "(" FormalParameters ")" "{ [native code] }",參數可以省略,並且內置方法要求給出方法名,例如:
document.createAttribute.toString()
// 輸出 "function createAttribute() { [native code] }"
特殊的:
-
toString()返回的函數源碼並不一定是合法的,可能只在其詞法上下文合法 -
通過
Function構造函數等方式動態創建的函數,也要求toString()返回合適的源碼// 1.
toString()返回值可能只在其詞法上下文合法 class C { foo() { /hello/ } } const source = C.prototype.foo.toString(); eval(source) // 報錯 Uncaught SyntaxError: Unexpected token {// 2. 通過
Function構造函數等方式動態創建的函數也支持 new Function('a', 'b', 'return a + b;').toString() // 輸出 function anonymous(a,b) { return a + b; }
七。總結
flat/flatMap、trimStart/trimEnd 等工具函數都已經納入標準,Object 又增加了一個無關緊要的方法,Symbol 支持直接讀取其描述信息了
此外,語法/語義上還做了一些修正,允許��略 catch 塊的參數部分,要求數組 sort() 必須穩定排序,明確了函數 toString() 的具體實現,完善了 JSON 支持,期望成為 JSON 的超集(JSON ? ECMAScript)
暫無評論,快來發表你的看法吧