一.概览
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 参数,例如:
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.模板字符串不存在这个问题:
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)