ECMAScript 2016+ 新特性介绍

ECMAScript 新特性处理流程

  1. 初稿(Strawman )阶段

    TC39 会员提交想法或建议,Stage 0 Proposals 中包含了所有处于初稿阶段的建议

  2. 建议(Proposal)阶段

    一份新特性的正式建议文档,包括了相应的实例和 API,以及 polyfills;Stage 1 Proposals 中包含了处于建议阶段的建议

  3. 草案(Draft)阶段

    草案是规范的第一个版本,与最终标准中包含的特性不会有太大差别;Stage 2 Proposals 包含所有草案

  4. 候选(Candidate)阶段

    候选阶段,建议基本完成,此时将从实现过程和用户使用两方面获取反馈来进一步完善建议;Stage 3 Proposals 包含了所有候选建议

  5. 完成(Finished)阶段

    建议已准备就绪,即将添加到标准中;Stage Finished Proposals 包含了即将或已加入标准的特性

被遗弃的草案(Inactive Proposals)

ECMAScript 版本特性

ECMAScript 2016

PDF 版规范:ECMA-262 7th edition

Array.prototype.includes()

1
Array.prototype.includes(searchElement [, fromIndex])

用于检查数组是否可以包含某个元素,效果类似于 Array.prototype.indexOf()

1
2
3
const nums = [1, 2, 3];

nums.includes(0) <=> nums.indexOf(0) > 0 <=> ~nums.indexOf(0)

但有一点不同的是,Array.prototype.includes() 可以检查是否包含 NaN,但 Array.prototype.indexOf() 不能。

兼容性:IE 及 Edge 12-13 不支持

Polyfill

Exponentiation Operator

指数运算符,效果同 Math.pow(),该运算符是右结合的,也就是从右往左计算

1
2
3
4
5
6
2 ** 3 <=> Math.row(2, 3) => 8

2 ** 3 ** 2 <=> 2 ** (3 ** 2)

let a = 2;
a ** = 3 <=> a ** a ** a

兼容性:IE 及 Edge 12~13 不支持

ECMAScript 2017

PDF 版规范:ECMA-262 8th edition

Object.values()

获取对象自身可枚举属性的属性值,返回包含这些属性值的数组,值的顺序与 for...in 循环的顺序相同

1
Object.values(obj)

兼容性:IE 及 Edge 12~13 不支持

Polyfill

Object.entries()

获取对象自身可枚举属性的键值对,返回包含键值对的数组,值的顺序与 for...in 循环的顺序相同

1
Object.entries(obj)
1
2
3
4
5
const personInfo = { name: 'Lee', age: '18' };

for(let [key, value] of Object.entries(personInfo)) {
console.info(`${key}: ${value}`);
}

兼容性:IE 及 Edge 12~13 不支持

Polyfill

String.padStart()

用指定字符从左侧开始填充当前字符串,直至当前字符串达到指定长度为止;默认以空字符串(" ")填充

1
str.padStart(targetLength [, fillString])
1
2
3
4
5
'a'.padStart(3) => '  a'

'a'.padStart(2, 'demo') => 'da'

'abc'.padStart(1) => 'abc'

兼容性:IE 及 Edge 12~14 不支持

Polyfill

String.padEnd()

用指定字符从右侧开始填充当前字符串,直至当前字符串达到指定长度为止;默认以空字符串(" ")填充

1
str.padEnd(targetLength [, fillString])
1
2
3
4
5
'a'.padStart(3) => '  a'

'a'.padStart(2, 'demo') => 'da'

'abc'.padStart(1) => 'abc'

兼容性:IE 及 Edge 12~14 不支持

Polyfill

Object.getOwnPropertyDescriptors()

获取对象的所有自有属性的描述(数据属性描述),如果没有自有属性,则返回空对象

1
Object.getOwnPropertyDescriptors(o)

该方法的引入主要是解决 Object.assign() 无法拷贝对象的访问器属性的问题:

1
const assign = (o) => Object.assign({}, Object.getPrototypeOf(o), Object.getOwnPropertyDescriptors(o));

该方法也可以实现对象的继承:

兼容性:IE 及 Edge 12~15 不支持

Polyfill

Trailing commas in function parameter lists and calls

该特性允许在函数参数列表和调用参数列表最后加逗号,以及也允许在对象、数组字面量最后添加逗号。

1
2
3
4
5
6
7
8
function demo(param1, param2,)
demo('A', 'B');

let o = { name: 'n', value: 'v', };
{ name, value,} = 0;

let arr = [1, 2, 3,];
[a, b,] = arr;

这种做法可以在需要改变最后一个元素项位置或补充新的元素项时,不用再添加或删除逗号了。

reset 参数后面不允许添加逗号

JSON 相关操作中不允许对象字面量后面有逗号

兼容性:IE 6-8 版本不支持 Trailing comma in Object literals、IE 6-11 及 Edge 不支持 Trailing comma in functions

Async functions

Async function 用于定义一个返回 AsyncFunctiton 对象的异步函数;异步函数指事件异步执行的函数,返回一个包含结果的 Promise

1
2
3
4
5
6
7
8
9
10
11
12
13
// 异步函数声明
async function demo() {}

const demo = async function() {}
const demo = async () => {}

const o = {
async demo() {}
}

// 返回
demo().then(res => {});
demo().catch(err => {});

await 配合 Async function 使用,且只能用于 Async functionawait 会暂停 Async function 的执行,等待 Promise 对象结束 pending 状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
const demo = async () => {
const num = await Promise.resolve(10);
console.log(num); // 10
}

const demo = async () => {
try {
const num = await Promise.reject(new Error('demo'));
console.log(num);
} catch(err) {
console.error(err);
}
}

async/await 能够简化多个 Promise 的同步行为。

Shared memory and atomics

Shared Array Buffer 允许主线程与多个 worker 之间快速的共享数据,Atomic 提供了一系列静态方法来对 Shared Array Buffer 进行原子操作,保证共享数据的安全;需要深入的理解,可以参考 Shared memory and atomics

1
2
3
4
5
6
7
8
9
10
11
12
// demo.js
const worker = new Worker('worker.js');

const iab = new Int32Array(new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT * 10));
worker.postMessage(iab);

setTimeout(() => {
Atomics.store(iab, 0, 100);

// Atomics.wake(iab, 0, 1);
Atomics.notify(iab, 0, 1);
}, 1000);
1
2
3
4
5
6
7
8
9
10
11
12
// worker.js

let iab = null;
self.onmessage = ({ data }) => {
iab = data;
console.info(iab);

Atomics.wait(iab, 0, 0);

// 1s 后
console.info(iab);
};

兼容性:目前仅有 Chrome 68+、Safari 10.1+ 支持

Atomics.wake() 重命名为 Atomics.notify()

ECMASCript 2018

PDF 版规范:ECMA-262 9th edition

Lifting template literal restriction

ES6 新增了 \u 修饰符,用于处理大于 \uFFFF 的 Unicode 字符,换句话说就是能正确处理四个字节的 UTF-16 编码,在标签模板中会按下面的规则进行转义:

  • \u 开头的 Unicode 字符,如:\u0061
  • \u{} 开头的 Unicode 字符,如:\u{61}
  • \x 开头的十六进制,如:\x256
  • \ 加数字开头的八进制,如:\64

但由于标签模板支持嵌套其他语言,如:LaTeXDSLs,标签模板在对字符串转义时可能会出现无法转义或转以后无法插入到其他语言中的情况。

1
2
3
4
5
6
7
8
9
function latex(strings) {}

let document = latex`
\newcommand{\fun}{\textbf{Fun!}} // 正常工作
\newcommand{\unicode}{\textbf{Unicode!}} // 报错
\newcommand{\xerxes}{\textbf{King!}} // 报错

Breve over the h goes \u{h}ere // 报错
`

为了解决上述问题,ES9 放宽了对标签模板中的转义规则限制(只有在标签模板中有效);转义过程中如果遇到不合法的字符,则返回 undefined,并且从 raw 属性中可以获取原始字符串。

1
2
3
4
5
const tag = (str, ...keys) => {
console.log(str[0], str.raw[0]);
}

tag`\unicode`;

s (dotall) flag for regular expressions

在正则中,点(.)是个特殊字符,可以代表除 UTF-16 字符和行终止符(line terminator character)外的任意单个字符。

行终止符包括下面四种:

  • U+000A 换行符(\n
  • U+000D 回车符(\r
  • U+2028 行分隔符
  • U+2029 段分隔符
1
2
3
/a.b/.test('a\nb'); // false

/a.b/.test('a\rb'); // false

如果希望匹配任意单个字符,可以通过 [^] 进行变通

1
2
3
/a[^]b/.test('a\nb'); // true

/a[^]b/.test('a\rb'); // true

ES9 中支持了 dotall 模式,增加 s 修饰符来表示 ‘.’ 代表所有单个字符;可以通过 dotAll 属性判断是否处于 dotall 模式。

1
2
3
4
5
6
const re = /a.b/s;
// const re = new RegExp('a.b', 's');

re.test('a\nb'); // true
re.dotAll; // true
re.flags; // 's'

另外,dotall 模式不会影响多行匹配模式,两者是完全独立的;s 修饰符影响 . 的匹配行为,m 修饰符影响 ^$ 的匹配行为;两者可以一起使用,互不干扰。

1
2
3
4
5
/^b$/.test('a\nb'); // false

/^b$/m.test('a\nb'); // true

/^b.$/sm.test('a\nb\nc'); // true

兼容性:目前仅有 Chrome 62+、Safari 12+ 支持

RegExp named capture groups

以往我们用组匹配在日期场景下进行操作时,需要通过 $n 引用对应捕获组捕获的内容。

1
2
3
const re = /(\d{4})-(\d{2})-(\d{2})/;

'2018-06-01'.replace(re, '$1/$2/$3'); // '2018/06/01'

使用 $n 时会碰到括号序号发生变化时,对应的 $n 的值也需要跟着一起变;ES9 中引入了具名组匹配(Named capture groups),允许为每一个组匹配指定一个名字。通过具名组匹配(语法:?<name> ),可以很好解决这个问题。

1
2
3
4
5
6
7
const re = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;

'2018-06-01'.replace(re, '$<year>/$<month>/$<day>'); // 2018/06/01

const result = re.exec('2018-06-01');
const { year, month, day } = result.groups;
// const { groups: { year, month, day }} = result;

如果需要在正则表达式中引用某个具名组匹配,可以通过数字引用(\n)或者组名引用(\k<name>)。

1
2
3
4
const re = /^(?<word>[a-z]+)!\k<word>!\1$/;

re.test('abc!abc!abc'); // true
re.test('abc!abc!ab'); // false

兼容性:目前仅有 Chrome 64+、Safari 11.1+ 支持

RegExp Lookbehind Assertions

JavaScript 中的正则表达式只支持先行断言(Lookahead)和先行否定断言(Negative lookahead),不支持后行断言(Lookbehind)和后行否定断言(Negative lookbehind),ES9 开始支持后行断言。

  • 后行断言:指 x 只有在 y 后面才匹配(/(?<=y)x/,如:匹配币种后面的金额
  • 后行否定断言:指 x 只有不在 y 后面才匹配(/(?<!y)x/
1
2
3
/(?<=\¥)\d+/.exec('总价 ¥100'); // ["100", index: 4, input: "总价 ¥100", groups: undefined]

/(?<!\¥)\d+/.exec('总价 $101'); // ["101", index: 4, input: "总价 $101", groups: undefined]

后行断言的实现是先匹配 x 再去匹配 y,也就是先右后左的执行顺序,与其他正则操作相反。

1
2
3
/^(\d+)(\d+)$/.exec('1053') // ["1053", "105", "3", index: 0, input: "1053", groups: undefined]

/(?<=(\d+)(\d+))$/.exec('1053'); // ["", "1", "053", index: 4, input: "1053", groups: undefined]

第一种情况中,没有使用后行断言,执行顺序是先左后右,第一个组匹配是贪婪模式,第二个组匹配只能捕获一个字符;第二种情况存在后行断言,先右后左执行,因此第二个组匹配是贪婪模式,第一个组匹配只能捕获一个字符。

同理,反斜杠引用在后行断言中的顺序也是相反的,需要放在元组前面。

1
2
3
4
5
/h(o)d\1r/.exec('hodor'); // ['hodor', 'o']

/h(?<=(o)d\1)r/.exec('hodor'); // null

/(?<=\1d(o))r/.exec('hodor') // ["r", "o"]

另外,不管是先行断言还是后行断言,括号中的部分都不计入返回结果。

兼容性:目前仅有 Chrome 62+ 支持

RegExp Unicode Property Escapes

Unicode 字符有一些属性,比如π是希腊文字,在 Unicode 中对应的属性是Script=Greek;为了支持根据 Uincode 属性特征匹配字符的场景,ES9 增加了两种语法:

  • \p{UnicodePropertyName=UnicodePropertyValue}

    匹配 Unicode 属性名等于指定属性值的字符

  • \p{LoneUnicodePropertyNameOrValue}

    匹配 Unicode 属性值为 true 的字符

\P{…}\p{…} 的反向匹配,即匹配不满足条件的字符。\p\P 只对 Unicode 字符有效,在使用时需要添加 \u 修饰符。

1
2
3
4
5
6
7
8
9
// 匹配所有数字,包括罗马数字
const re = /^\p{Number}+$/u;
re.test('²³¹¼½¾') // true
re.test('㉛㉜㉝') // true
re.test('ⅠⅡⅢⅣⅤⅥⅦⅧⅨⅩⅪⅫ') // true

// 匹配十进制字符
const re = /^\p{Decimal_Number}+$/u;
re.test()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 匹配所有空格
\p{White_Space}

// 匹配各种文字的所有字母,等同于 Unicode 版的 \w
[\p{Alphabetic}\p{Mark}\p{Decimal_Number}\p{Connector_Punctuation}\p{Join_Control}]

// 匹配各种文字的所有非字母的字符,等同于 Unicode 版的 \W
[^\p{Alphabetic}\p{Mark}\p{Decimal_Number}\p{Connector_Punctuation}\p{Join_Control}]

// 匹配 Emoji
const regexEmoji = /\p{Emoji_Modifier_Base}\p{Emoji_Modifier}?|\p{Emoji_Presentation}|\p{Emoji}\uFE0F/gu;
'👩aaa'.replace(regexEmoji, 'A');

// 匹配所有的箭头字符
const regexArrows = /^\p{Block=Arrows}+$/u;
regexArrows.test('←↑→↓↔↕↖↗↘↙⇏⇐⇑⇒⇓⇔⇕⇖⇗⇘⇙⇧⇩') // true

兼容性:目前仅有 Chrome 64+、Safari 11.1+ 支持

Rest/Spread Properties

对象的解构赋值用于从一个对象取值,相当于将目标对象自身的所有可遍历的(enumerable)、但尚未被读取的属性,分配到指定的对象上面。普通的解构赋值可以获取对象继承的属性,但扩展运算符的解构赋值,不能复制继承的属性。

1
2
// rest properties
let { x, y, ...z } = { x: 1, y: 2, a: 3, b: 4 };
1
2
3
4
5
6
7
8
9
const o = Object.create({ x: 1, y: 2 });
o.z = 3;

let { x, ..n } = o;
let { y, z } = n;

x; // 1
y; // undefined
z; // 3

对象的扩展运算符(...)用于取出参数对象自由的所有可遍历属性,拷贝到当前对象之中;如果扩展运算符后面不是对象,则会自动将其转为对象;效果等同于使用Object.assign()

1
2
3
4
5
6
7
8
// spread properties
let n = { x, y, ...z };

{ ...1 } <=> { ...Object(1) } => {}
{ ...true } <=> { ...Object(true) } => {}
{ ...undefined } <=> { ...Object(undefined) } => {}
{ ...null } <=> { ...Object(null) } => {}
{ ...'string' } <=> { ...{ 0: 's', 1: 't', 2: 'r', 3: 'i', 4: 'i', 5: 'n', 6: 'g' }} => {0: "s", 1: "t", 2: "r", 3: "i", 4: "n", 5: "g"}
1
2
3
4
5
6
const animal = Object.create({ type: '' });
animal.name = '';

const bird = { ...animal };
bird.name; // ''
bird.type; // undefined

Promise.prototype.finally

在用 Promise 进行异步处理时,有时候业务需要在异步完成(Resolve 或 Reject)后处理,一般情况下同样的业务逻辑需要在 then()catch() 中都写一遍。

为了避免这种情况,ES9 新增了 finally() ,在 Promise 完成时(无论是 Fullfilled 还是 rejected)都会执行 finally()该方法不接收任何参数,返回一个新的 Promise

1
2
3
4
5
6
const loading = true;

fetch(url)
.then()
.catch()
.finally(_ => loading = false);

finally() 只要是碰到都会执行,并非是最后才执行。

1
2
3
4
Promise.resolve()
.finally(_ => console.log('First'))
.then(n => console.log('then'))
.finally(v => console.log('Second'));

1
2
3
4
5
6
7
8
9
Promise.resolve('Resolve')
.finally(() => Promise.reject('Error'))
.then(v => console.info(v))
.catch(err => console.error(err)) // Error

Promise.reject('Error')
.finally(() => Promise.resolve('Resolve'))
.then(v => console.info(v))
.catch(err => console.error(err)) // Error

Asynchronous Iteration

ES6 中 Symbol 内置了一个 Symbol.iterator 属性,用于生成遍历器对象,但只能进行同步遍历,无法对异步数据源进行遍历。为了支持异步遍历,ES9 中 Symbol 新增了 Symbol.asyncIterator 属性,用于生成异步遍历器对象。

异步遍历器对象的 next() 方法,返回 Promise 对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const asyncIterable = createAsyncIterable(['a', 'b']);
const asyncIterator = asyncIterable[Symbol.asyncIterator]();
asyncIterator.next()
.then(iterResult1 => {
console.log(iterResult1); // { value: 'a', done: false }
return asyncIterator.next();
})
.then(iterResult2 => {
console.log(iterResult2); // { value: 'b', done: false }
return asyncIterator.next();
})
.then(iterResult3 => {
console.log(iterResult3); // { value: undefined, done: true }
});
1
2
3
4
5
6
7
async function Demo() {
const asyncIterable = createAsyncIterable(['a', 'b']);
const asyncIterator = asyncIterable[Symbol.asyncIterator]();
console.log(await asyncIterator.next()); // { value: 'a', done: false }
console.log(await asyncIterator.next()); // { value: 'b', done: false }
console.log(await asyncIterator.next()); // { value: undefined, done: true }
}

ES6 中可以通过 for...of 来遍历具有 Symbol.iterator 属性的对象,在 ES9 中支持通过 for-await-of 遍历 Symbol.asyncIterator 属性的对象。

1
2
3
4
5
async function Demo() {
for await (const x of createAsyncIterable(['a', 'b'])) {
console.log(x);
}
}

ECMAScript 2019

PDF 版规范:ECMA-262 10th edition

Optional catch binding

try-catch 语句中 catch 后必须携带对应的异常参数,虽然很多情况下这个异常参数并未使用。

1
2
3
4
5
try {

} catch(err) {

}

ES10 开始允许省略 catch 绑定的异常

1
2
3
4
5
try {

} catch {

}

JSON superset

根据规范中对 String literals 的定义,字符串可以包含除单引号、反斜杠、行终止符外的任何字符

其中,行终止符包括以下四种:

Code PointUnicode Name
U+000ALine Feed (LF)
U+000DCarriage Return (CR)
U+2028Line separator
U+2029Paragraph separator

JSON 和 JavaScript 在 U+2028U+2029 关于两个字符的处理存在差异

1
2
3
4
5
// ES3-ES9
var sourceCode = '"\u2028"';
eval(sourceCode); // SyntaxError

JSON.parse(sourceCode); // ok

ES10 调整了相关的语义,移除了这方面的差异,从而保证了合法的 JSON 格式能够直接被用作 JavaScript 表达式使用。

Symbol.prototype.description

ES10 中为 Symbol 对象新增了只读属性 description,该属性返回 Symbol 描述的字符串。

1
2
3
4
5
6
7
8
9
Symbol().description; // undefined

Symbol('demo').description; // "demo"

Symbol.for('demo').description; // "demo"

Symbol.asyncIterator.description; // "Symbol.asyncIterator"

Symbol.hasInstance.description; // "Symbol.hasInstance"

toSting() 的返回不同,toString() 包含 Symbol()

1
2
3
4
5
6
7
8
9
Symbol().toString(); // "Symbol()"

Symbol('demo').toString(); // "Symbol(demo)"

Symbol.for('demo').toString(); // "Symbol(demo)"

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

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

Function.prototype.toString revision

ES10 之前,函数的 toString() 在返回函数对应的字符串时会移除注释、空白符等;ES10 对其进行了调整,会按函数的格式原样返回。

更多调整,可以参考 ES proposal: Function.prototype.toString revision

Object.fromEntries

ES8 中引入了 Object.entries(),该方法返回对的遍历器对象,以 [key, value] 形式遍历对象的可枚举属性。ES10 引入与 Object.entries() 操作正好相反的方法 Object.fromEntries(),将 [key, value] 形式的数组转换为对象。

1
2
3
4
const nums = [['one', 1], ['two', 2], ['three', 3]];
const numObj = Object.fromEntries(nums);

console.log(numObj); // => { one: 1, two: 2, three: 3 }
1
2
3
4
5
6
7
const map = new Map();
map.set('one', 1);
map.set('two', 2);
map.set('thres', 3);

const numObj = Object.fromEntries(map);
console.log(numObj); // => { one: 1, two: 2, three: 3 }
1
2
3
4
5
6
7
// 替代方案
const fromEntries = (arr) => {
return Array.from(arr).reduce((o, [key, value]) => Object.assign(o, {[key]: value }), {});
};

const nums = [['one', 1], ['two', 2], ['three', 3]];
console.log(fromEntries(nums));

Well-formed JSON.stringify

JSON.stringify()Surrogates 字符按 UTF-8 编码方式进行输出,由于 UTF-8 和 UTF-16 码位范围存在差异,这导致返回的结果无法在 UTF-18 编码的应用中正常使用。

ES10 修正了该行为,将 Surrogate code points 以转义序列(\uxxxx)的形式输出,从而避免上述问题。

1
JSON.stringify('\uDC00'); // ""\udc00""

String.prototype.{trimStart,trimEnd}

移除字符串空白符,可以通过 String.prototype.trim() ,该方法会将字符串前后的空白符都移除。另外,Chrome 和 Firefox 也可以通过 String.prototype.trimLeft()String.prototype.trimRight() 移除开头或结尾处的空白符,但这个两个方法都是非标准方法。

为了统一标准,ES10 新增了 trimStart()trimEnd(),效果与 trimLeft()trimRight() 相同。同时为了向后兼容,trimLeft()trimRight() 将保留为trimStart()trimEnd() 的别名。

1
2
3
4
5
6
7
8
const demo = "   demo   ";

// es2019
str.trimStart(); // "string "
str.trimEnd(); // " string"

str.trimLeft(); // "string "
str.trimRight(); // " string"

Array.prototype.{flat,flatMap}

ES10 新增了两个数组方法 flat()flatMap()

flat() 用于将数组扁平化,返回扁平化后的新数组。默认嵌套深度 depth 为 1

1
Array.prototype.flat( [ depth ] );
1
2
3
4
5
6
7
const nums = [1, 2, [3, [4, 5, [6]]];

nums.flat(); // [1, 2, 3, [4, 5, [6]]]

nums.flat(2); // [1, 2, 3, 4, 5, [6]]

nums.flat(Infinity); // [1, 2, 3, 4, 5, 6]

flatMap() 对原数组的每个成员执行一个函数(相当于执行 Array.prototype.map() ),然后对返回值组成的数组执行 flat() 方法。

1
Array.prototype.flatMap( mapperFunction [ , thisArg ] )
1
2
3
4
[1, 2, 3].flatMap(num => [num, num * 2]); [1, 2, 2, 4, 3, 6]

// 类似 filter()
[1, 2, -3, -4, 5].flatMap(num => num > 0 ? [num] : []); // [1, 3, 5]