一.概览 2019 年 6 月发布了 ES2019 规范,即 ES10

包括 4 个新特性:

Array.prototype.{flat,flatMap}:用来打平数组

Object.fromEntries:Object.entries的逆运算

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) ** 参考资料** ECMAScript® 2019 Language Specification

ECMAScript 2019: the final feature set

ES proposal: Object.fromEntries()

ES proposal: String.prototype.trimStart / String.prototype.trimEnd

ES proposal: optional catch binding

ES proposal: Function.prototype.toString revision

Well-formed JSON.stringify