自 ECMAScript 2015 以后,ES 将不再进行大幅度变更,而是改为每年小副更新。大约从每年的 2 月份开始,TC39 会开始整理当年新版的草案,并经过一段时间的修订之后,提交给 ECMA 开始正式生效。

自那之后,转眼之间,到今年已经是第五个年头了。

不久前 ES2019 的草案已经推出,再等一段时间,新的正式版本将又一次呈现在广大的前端程序员面前。

而我则准备从今年开始,在每年的这个时候系统了解一下新出的特性,并顺便总结出一篇文章。

至于这一篇,就是关于今年 ECMAScript 2019 的。

String.prototype.{trimStart, trimEnd}

首先从最简单的一个看起。

在之前的 JS 规范中,我们只有一个 trim 方法用来清除字符串两边的空白字符,不过有时我们只想清除一边。在之前为了满足这一需求,有些 JS 引擎自行提供 trimLefttrimRight 这两个方法,但毕竟这两个方法是 JS 引擎私自实现的,并未被纳入规范。

而且在现有的规范中,String 里还有两个原型方法 padStartpadEnd,那么如果将 trimLefttrimRight 纳入规范,便会有命名不一致的问题(注:这在 API 设计中是一个很严重的问题,应尽量避免,若任由其发展的话,请想想现在的 PHP)。

因此新版规范纳入了这两个方法,但将名字改成了 trimStarttrimEnd。同时为了兼容,trimLefttrimRight 也一并被纳入了规范,但只是作为 trimStarttrimEnd 的别名,其行为完全一致。

不过较为可惜的是这两个方法依然只能用于清除空白字符,如果想要清除其它字符,还是得自行实现或使用 lodash 这种第三方库。

Array.prototype.{flat, flatMap}

接着再来看看数组,这次数组也新增了一对方法:flatflatMap

顾名思义,这两个方法就是用来把数组拍平的:

其中 flat 单纯只是负责把数组拍平,它接收一个可选的数字参数,表示要拍平的嵌套深度,缺省为 1,比如:

const arr = [1, 2, [3, 4, [5, 6, [7]]]

arr.flat()
// [1, 2, 3, 4, [5, 6, [7]]

arr.flat(2)
// [1, 2, 3, 4, 5, 6, [7]]

arr.flat(Infinity)
// [1, 2, 3, 4, 5, 6, 7]

flatMap 则是先为数组执行一次 map,再将映射后的结果拍平。它也接收一个参数,但这个参数应当是一个映射函数,比如:

const arr = ['ab', 'cd']

arr.flatMap((word) => Array.from(word))
// ['a', 'b', 'c', 'd']

可以看到,flatMap 其实就是下面这种写法的简写形式:

arr.map((word) => Array.from(word)).flat(1)

这两个方法所提供的功能还是挺常用到的,比如在电商应用中,我们有时会想要聚合展示购物车内所有商品的赠品,在之前我们会这样下面这样 :

const allFreeGifts = []
cart.items.forEach(item => allFreeGifts.push(...item.freeGifts))

那么有了 flat 之后,就可以简化为:

const allFreeGifts = cart.items.map(item => item.freeGifts).flat()

当然,如果用 flatMap,还可以再简化一些:

const allFreeGifts = cart.items.flatMap(item => item.freeGifts)
不过,如果我们用 lodash 这种函数库,那么可以写出最简单的代码:
const freeGifts = _.flatMap(cart.items, 'freeGifts')

Object.fromEntries

这个新方法用于将一组可迭代的键值对转换成一个对象:

Object.fromEntries([
    ['a', 0],
    ['b', 1],
]);

// { a: 0, b: 1 }

当然,类似 Map 这种数据结构也同样支持:

const map = new Map();

map.set('a', 0);
map.set('b', 1);

Object.fromEntries(map);

// { a: 0, b: 1 }

按规范中的示例,结合 Object.entries 可以让我们较为方便地的操纵对象:

const obj = { abc: 1, def: 2, ghij: 3 };

Object.fromEntries(
    Object.entries(obj)
        .filter(([ key, val ]) => key.length === 3)
        .map(([ key, val ]) => [ key, val * 2 ])
);

// { 'abc': 2, 'def': 4 }

但说实话,感觉这种代码的可读性并不好,因为操作对象(obj)的位置太过靠后,导致视线需要在代码上来回跳动,所以还不如用 lodash:

const obj = { abc: 1, def: 2, ghij: 3 };

_.chain(obj)
    .pickBy((val, key) => key.length === 3)
    .mapValues(val => val * 2)
    .value();

// { 'abc': 2, 'def': 4 }

不过,倒是在网上看到过有用它实现浅拷贝的,这也是目前为止,我知道的所有浅拷贝实现中代码最短的了:

Object.fromEntries(Object.entries({ a: 0, b: 1 }));

当然,因为支持 Map,所以也许更有意义点的场景,是将其用在 Object 和 Map 之间的转换中。比如前端使用 Map 结构维护数据,然后在需要将数据发送给服务器端时,可以通过 Object.fromEntries 将其转换为对象以便序列化为 JSON 格式的字符串。只不过这种操作一般都会在底层封装好,平时不太会用的到就是了。

可以省略的 catch 参数部分

这是一个语法上的简化,往常在声明 catch 时,会要求我们必须接收一个错误对象的参数,哪怕我们并不会用到它:

try {
    // ...
}
catch (e) {
    // ...
}

但自 ES2019 之后,我们可以省略这段无用的参数部分:

try {
    // ...
}
catch {
    // ...
}

Symbol.prototype.description

这个就比较简单了。众所周知,我们在创建 Symbol 的时候,可以为其指定一段描述文本:

const key = Symbol('key');
const key = Symbol.for('key');

但若想要从这 symbol 值中获取它的描述时,就有点费劲了。在之前,我们只能通过 toString() 方法间接地实现:

key.toString();
// "Symbol(key)"

而在 ES2019 之后,symbol 的实例中将会暴露一个名为 description 的只读属性,今后只要通过它就可以直接获取了:

key.description;
// "key"

JSON 超集

话说刚学 JavaScript 的那会就知道一件事情:「 JSON 是 JavaScript 的语法子集,可以直接作为 JavaScript 的代码进行执行」 。

有时拿到一段压缩过的 JSON 数据,也会图省事直接粘贴到 Chrome 的控制台里进行查看:

现在才知道,原来 JSON 并不完全是 JavaScript 的子集,在某些时候直接把 JSON 当作代码进行执行也是会出错的。

什么时候哪?当这段 JSON 中的字符串里有未转义的 U+2028 LINE SEPARATORU+2029 PARAGRAPH SEPARATOR 这个两个字符的时候。JSON 允许字符串中直接包含这两个字符,但在 ECMAScript 中,它们会被认为是换行符号,导致语法错误。

话说我这是头一次见到这两个字符,之前也从未见到过这种错误,不过刚才在网上随便搜了下,却看到了不少血淋淋的教训。而且这种细微的不一致性很难被发现,一但出现就要花费大量的时间去排查。

另外,因为这在规范中增加了不必要的复杂性,为相关的开发者带来了不必要的认知负担,也为相关实现引入了额外的处理成本,因此在 ECMAScript 2019 中,增加了相关的支持,允许字符串(单/双引号字符串)中可以直接包含这两个字符。

自此,ECMAScript 真正地成为了 JSON 的超集。

格式良好的 JSON.stringify

有了解过 UTF-16 编码的同学应该知道,所有辅助平面(Supplementary Planes)中的字符在 UTF-16 中都是使用代理对也就是两个码元(四个字节)进行表示的,这对代码单元的码位区间为 D800₁₆..DFFF₁₆,比如字符 “𝌆” 所对应的编码就是 0xD834 0xDF06。同时,这段区间仅供 UTF-16 编码使用,不会编码实际的有效字符。

也就是说,在 D800₁₆..DFFF₁₆ 区间内的码元必需成对出现,不能独自表示一个有效字符。而这个,就是导致该规范所要解决的问题的原因。

在之前,执行 JSON.stringify("\uD834") 时,stringify 方法中会直接输出这个字符,这会导致返回一个非常难看且毫无作用的无效字符 ’”�”’

而在之后,当再遇到这种无效字符时,stringify 会返回它的转义序列 ’”\ud834”’

修订 Function.prototype.toString

简单来说,该规范重新修订了函数实例的 toString 方法,使其返回值尽可能与其定义时保持一致,比如保留函数定义时的注释和其它字符(如空格)。

比如:

function /* this is bar */ bar () {}

在之前调用它的 toString 方法时将返回如下结果:

bar.toString() //'function bar() {}

而在 ES2019 推出之后,将返回:

bar.toString(); // 'function /* this is bar */ bar () {}'

至于更详细点的内容,因为我实在是懒得读那份规范了,所以想知道的,自己去看吧:Function.prototype.toString revision