ES2019

一.概览

2019 年 6 月发布了 ES2019 规范,即 ES10

包括 4 个新特性:

以及 6 个语法/语义上的变化:

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]

作用上,flatMapmap类似,主要区别在于: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+2028U+2029字符在 JSON 中是合法的,而在 JavaScript 字符串字面量中是非法字符:

const LS = "
";
const PS = eval("'\u2029'");
// 报错 Uncaught SyntaxError: Invalid or unexpected token

ES2019 规范要求字符串字面量支持完整的 JSON 字符集,即JavaScript 作为 JSON 的超集。在支持 ES2019 的环境中,对于双引号/单引号中的U+2028U+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/flatMaptrimStart/trimEnd等工具函数都已经纳入标准,Object 又增加了一个无关紧要的方法,Symbol 支持直接读取其描述信息了

此外,语法/语义上还做了一些修正,允许省略 catch 块的参数部分,要求数组sort()必须稳定排序,明确了函数toString()的具体实现,完善了 JSON 支持,期望成为 JSON 的超集(JSON ⊂ ECMAScript)

参考资料

发表评论

电子邮件地址不会被公开。 必填项已用*标注

*

code