MarkArch 2019-06-29
导言:ECMAScript的演化不会停止,但是我们完全没必要害怕。除了ES6这个史无前例的版本带来了海量的信息和知识点以外,之后每年一发的版本都仅仅带有少量的增量更新,一年更新的东西花半个小时就能搞懂了,完全没必要畏惧。本文将带您花大约一个小时左右的时间,迅速过一遍ES7,ES8,ES9的新特性。
想追求更好的阅读体验,请移步原文地址
题记:本文提供了一个在线PPT版本,方便您浏览 细解JAVASCRIPT ES7 ES8 ES9 新特性 在线PPT ver
本文的大部分内容译自作者Axel Rauschmayer博士的网站,想了解更多关于作者的信息,可以浏览Exploring JS: JavaScript books for programmers
TC39 (Technical Committee 39)。
TC39 是推进 JavaScript 发展的委员会。其会员都是公司(其中主要是浏览器厂商)。TC39 定期召开会议,会议由会员公司的代表与特邀专家出席。会议纪录都可在网上查看,可以让你对 TC39 如何工作有一个清晰的概念。
很有意思的是,TC39 实行的是协商一致的原则:通过一项决议必须得到每一位会员(公司代表)的赞成。
在2015年发布的 ECMAScript(ES6)新增内容很多,在 ES5 发布近 6 年(2009-11 至 2015-6)之后才将其标准化。两个发布版本之间时间跨度如此之大主要有两大原因:
因此,从 ECMAScript 2016(ES7)开始,版本发布变得更加频繁,每年发布一个新版本,这么一来新增内容也会更小。新版本将会包含每年截止时间之前完成的所有特性。
每个 ECMAScript 特性的建议将会从阶段 0 开始, 然后经过下列几个成熟阶段。其中从一个阶段到下一个阶段必须经过 TC39 的批准。
当前的stage 0列表可以查看这里 --> Stage 0 Proposals
什么是 Proposal?一份新特性的正式建议文档。提案必须指明此建议的潜在问题,例如与其他特性之间的关联,实现难点等。
什么是 Draft?草案是规范的第一个版本。其与最终标准中包含的特性不会有太大差别。
草案之后,原则上只接受增量修改。这个阶段开始实验如何实现,实现形式包括polyfill, 实现引擎(提供草案执行本地支持),或者编译转换(例如babel)
候选阶段,获得具体实现和用户的反馈。此后,只有在实现和使用过程中出现了重大问题才会修改。至少要在一个浏览器中实现,提供polyfill或者babel插件。
已经准备就绪,该特性会出现在下个版本的ECMAScript规范之中。
当前的stage 1-3列表可以查看这里 --> ECMAScript proposals
Proposal | Author | Champion(s) | TC39 meeting notes | Expected Publication Year |
---|---|---|---|---|
Array.prototype.includes | Domenic Denicola | Domenic Denicola Rick Waldron | November 2015 | 2016 |
Exponentiation operator | Rick Waldron | Rick Waldron | January 2016 | 2016 |
Object.values /Object.entries | Jordan Harband | Jordan Harband | March 2016 | 2017 |
String padding | Jordan Harband | Jordan Harband Rick Waldron | May 2016 | 2017 |
Object.getOwnPropertyDescriptors | Jordan Harband Andrea Giammarchi | Jordan Harband Andrea Giammarchi | May 2016 | 2017 |
Trailing commas in function parameter lists and calls | Jeff Morrison | Jeff Morrison | July 2016 | 2017 |
Async functions | Brian Terlson | Brian Terlson | July 2016 | 2017 |
Shared memory and atomics | Lars T Hansen | Lars T Hansen | January 2017 | 2017 |
Lifting template literal restriction | Tim Disney | Tim Disney | March 2017 | 2018 |
s (dotAll ) flag for regular expressions | Mathias Bynens | Brian Terlson Mathias Bynens | November 2017 | 2018 |
RegExp named capture groups | Gorkem Yakin Daniel Ehrenberg | Daniel Ehrenberg Brian Terlson Mathias Bynens | November 2017 | 2018 |
Rest/Spread Properties | Sebastian Markbåge | Sebastian Markbåge | January 2018 | 2018 |
RegExp Lookbehind Assertions | Gorkem Yakin Nozomu Katō Daniel Ehrenberg | Daniel Ehrenberg Mathias Bynens | January 2018 | 2018 |
RegExp Unicode Property Escapes | Mathias Bynens | Brian Terlson Daniel Ehrenberg Mathias Bynens | January 2018 | 2018 |
Promise.prototype.finally | Jordan Harband | Jordan Harband | January 2018 | 2018 |
Asynchronous Iteration | Domenic Denicola | Domenic Denicola | January 2018 | 2018 |
Optional catch binding | Michael Ficarra | Michael Ficarra | May 2018 | 2019 |
JSON superset | Richard Gibson | Mark Miller Mathias Bynens | May 2018 | 2019 |
ES7在ES6的基础上主要添加了两项内容:
includes() 方法用来判断一个数组是否包含一个指定的值,根据情况,如果包含则返回 true,否则返回false。
var array = [1, 2, 3]; console.log(array.includes(2)); // expected output: true var pets = ['cat', 'dog', 'bat']; console.log(pets.includes('cat')); // expected output: true console.log(pets.includes('at')); // expected output: false
Array.prototype.includes()方法接收两个参数:
当第二个参数被传入时,该方法会从索引处开始往后搜索(默认索引值为0)。若搜索值在数组中存在则返回true,否则返回false。 且看下面示例:
['a', 'b', 'c', 'd'].includes('b') // true ['a', 'b', 'c', 'd'].includes('b', 1) // true ['a', 'b', 'c', 'd'].includes('b', 2) // false
乍一看,includes的作用跟数组的indexOf重叠,为什么要特意增加这么一个api呢?主要区别有以下几点:
var ary = [1]; if (ary.indexOf(1) !== -1) { console.log("数组存在1") } if (ary.includes(1)) { console.log("数组存在1") }
var ary1 = [NaN]; console.log(ary1.indexOf(NaN))//-1 console.log(ary1.includes(NaN))//true
var ary1 = new Array(3); console.log(ary1.indexOf(undefined));//-1 console.log(ary1.includes(undefined))//true
加/减法我们通常都是用其中缀形式,直观易懂。在ECMAScript2016中,我们可以使用**
来替代Math.pow。
4 ** 3 // 64
效果等同于
Math.pow(4,3)
值得一提的是,作为中缀运算符,**还支持以下操作
let n = 4; n **= 3; // 64
在2017年1月的TC39会议上,ECMAScript 2017的最后一个功能“Shared memory and atomics”推进到第4阶段。这意味着它的功能集现已完成。
ECMAScript 2017特性一览
主要新功能:
次要新功能:
Async Functions也就是我们常说的Async/Await,相信大家对于这个概念都已经不陌生了。Async/Await是一种用于处理JS异步操作的语法糖,可以帮助我们摆脱回调地狱,编写更加优雅的代码。
通俗的理解,async关键字的作用是告诉编译器对于标定的函数要区别对待。当编译器遇到标定的函数中的await关键字时,要暂时停止运行,带到await标定的函数处理完毕后,再进行相应操作。如果该函数fulfiled了,则返回值是fulfillment value,否则得到的就是reject value。
下面通过拿普通的promise写法来对比,就很好理解了:
async function asyncFunc() { const result = await otherAsyncFunc(); console.log(result); } // Equivalent to: function asyncFunc() { return otherAsyncFunc() .then(result => { console.log(result); }); }
按顺序处理多个异步函数的时候优势更为明显:
async function asyncFunc() { const result1 = await otherAsyncFunc1(); console.log(result1); const result2 = await otherAsyncFunc2(); console.log(result2); } // Equivalent to: function asyncFunc() { return otherAsyncFunc1() .then(result1 => { console.log(result1); return otherAsyncFunc2(); }) .then(result2 => { console.log(result2); }); }
并行处理多个异步函数:
async function asyncFunc() { const [result1, result2] = await Promise.all([ otherAsyncFunc1(), otherAsyncFunc2(), ]); console.log(result1, result2); } // Equivalent to: function asyncFunc() { return Promise.all([ otherAsyncFunc1(), otherAsyncFunc2(), ]) .then([result1, result2] => { console.log(result1, result2); }); }
处理错误:
async function asyncFunc() { try { await otherAsyncFunc(); } catch (err) { console.error(err); } } // Equivalent to: function asyncFunc() { return otherAsyncFunc() .catch(err => { console.error(err); }); }
Async Functions若是要展开去讲,可以占用很大段的篇幅。鉴于本文是一篇介绍性文章,再次不再进行深入。
注,如果之前您没有接触过ArrayBuffer相关知识的话,建议您从内存管理速成教程系列漫画解说入门,强推:
A crash course in memory management
[A cartoon intro to ArrayBuffers and SharedArrayBuffers
](https://hacks.mozilla.org/201...
[Avoiding race conditions in SharedArrayBuffers with Atomics
](https://hacks.mozilla.org/201...
ECMAScript 2017 特性 SharedArrayBuffer 和 atomics”,由Lars T. Hansen设计。它引入了一个新的构造函数 SharedArrayBuffer 和 具有辅助函数的命名空间对象 Atomics。
在我们开始之前,让我们澄清两个相似但截然不同的术语:并行(Parallelism) 和 并发(Concurrency) 。他们存在许多定义,我使用的定义如下
Web workers 将任务并行引入了 JavaScript :这些是相对重量级的进程。每个 workers 都有自己的全局环境。默认情况下,不共享任何内容。 workers 之间的通信(或在 workers 和主线程之间的通信)发展:
共享阵列缓冲区是更高并发抽象的基本构建块。它们允许您在多个 workers 和主线程之间共享 SharedArrayBuffer 对象的字节(该缓冲区是共享的,用于访问字节,将其封装在一个 TypedArray 中)这种共享有两个好处:
你可以更快地在 workers 之间共享数据。
workers 之间的协调变得更简单和更快(与 postMessage() 相比)。
// main.js const worker = new Worker('worker.js'); // 要分享的buffer const sharedBuffer = new SharedArrayBuffer( // (A) 10 * Int32Array.BYTES_PER_ELEMENT); // 10 elements // 使用Worker共用sharedBuffer worker.postMessage({sharedBuffer}); // clone // 仅限本地使用 const sharedArray = new Int32Array(sharedBuffer); // (B)
创建一个共享数组缓冲区(Shared Array Buffers)的方法与创建普通的数组缓冲区(Array Buffer)类似:通过调用构造函数,并以字节的形式指定缓冲区的大小(行A)。你与 workers 共享的是 缓冲区(buffer) 。对于你自己的本地使用,你通常将共享数组缓冲区封装在 TypedArray 中(行B)。
workers的实现如下所列。
// worker.js self.addEventListener('message', function (event) { const {sharedBuffer} = event.data; const sharedArray = new Int32Array(sharedBuffer); // (A) // ··· });
构造函数:
创建一个 length 字节的 buffer(缓冲区)。
静态属性:
默认情况下返回 this。 覆盖以控制 slice() 的返回。
实例属性:
返回 buffer(缓冲区) 的字节长度。
创建一个新的 this.constructor[Symbol.species] 实例,并用字节填充从(包括)开始到(不包括)结束的索引。
举一个例子
// main.js sharedArray[1] = 11; sharedArray[2] = 22;
在单线程中,您可以重新排列这些写入操作,因为在中间没有读到任何内容。 对于多线程,当你期望以特定顺序执行写入操作时,就会遇到麻烦:
// worker.js while (sharedArray[2] !== 22) ; console.log(sharedArray[1]); // 0 or 11
Atomics 方法可以用来与其他 workers 进行同步。例如,以下两个操作可以让你读取和写入数据,并且不会被编译器重新排列:
这个想法是使用常规操作读取和写入大多数数据,而 Atomics 操作(load ,store 和其他操作)可确保读取和写入安全。通常,您将使用自定义同步机制,例如锁,其实现基于Atomics。
这是一个非常简单的例子,它总是有效的:
// main.js console.log('notifying...'); Atomics.store(sharedArray, 0, 123); // worker.js while (Atomics.load(sharedArray, 0) !== 123) ; console.log('notified');
Atomic 函数的主要操作数必须是 Int8Array ,Uint8Array ,Int16Array ,Uint16Array ,Int32Array 或 Uint32Array 的一个实例。它必须包裹一个 SharedArrayBuffer 。
所有函数都以 atomically 方式进行操作。存储操作的顺序是固定的并且不能由编译器或 CPU 重新排序。
加载和存储
读取和返回 ta[index] 上的元素,返回数组指定位置上的值。
在 ta[index] 上写入 value,并且返回 value。
将 ta[index] 上的元素设置为 value ,并且返回索引 index 原先的值。
如果 ta[index] 上的当前元素为 expectedValue , 那么使用 replacementValue 替换。并且返回索引 index 原先(或者未改变)的值。
简单修改 TypeArray 元素
以下每个函数都会在给定索引处更改 TypeArray 元素:它将一个操作符应用于元素和参数,并将结果写回元素。它返回元素的原始值。
执行 ta[index] += value 并返回 ta[index] 的原始值。
执行 ta[index] -= value 并返回 ta[index] 的原始值。
执行 ta[index] &= value 并返回 ta[index] 的原始值。
执行 ta[index] |= value 并返回 ta[index] 的原始值。
执行 ta[index] ^= value 并返回 ta[index] 的原始值。
等待和唤醒
如果 ta[index] 的当前值不是 value ,则返回 'not-equal'。否则继续等待,直到我们通过 Atomics.wake() 唤醒或直到等待超时。 在前一种情况下,返回 'ok'。在后一种情况下,返回'timed-out'。timeout 以毫秒为单位。记住此函数执行的操作:“如果 ta[index] 为 value,那么继续等待” 。
唤醒等待在 ta[index] 上的 count workers。
Object.values() 方法返回一个给定对象自己的所有可枚举属性值的数组,值的顺序与使用for...in循环的顺序相同 ( 区别在于for-in循环枚举原型链中的属性 )。
obj参数是需要待操作的对象。可以是一个对象,或者一个数组(是一个带有数字下标的对象,[10,20,30] -> {0: 10,1: 20,2: 30})。
const obj = { x: 'xxx', y: 1 }; Object.values(obj); // ['xxx', 1] const obj = ['e', 's', '8']; // 相当于 { 0: 'e', 1: 's', 2: '8' }; Object.values(obj); // ['e', 's', '8'] // 当我们使用数字键值时,返回的是数字排序 // 根据键值排序 const obj = { 10: 'xxx', 1: 'yyy', 3: 'zzz' }; Object.values(obj); // ['yyy', 'zzz', 'xxx'] Object.values('es8'); // ['e', 's', '8']
Object.entries 方法返回一个给定对象自身可遍历属性 [key, value] 的数组, 排序规则和 Object.values 一样。这个方法的声明比较琐碎:
const obj = { x: 'xxx', y: 1 }; Object.entries(obj); // [['x', 'xxx'], ['y', 1]] const obj = ['e', 's', '8']; Object.entries(obj); // [['0', 'e'], ['1', 's'], ['2', '8']] const obj = { 10: 'xxx', 1: 'yyy', 3: 'zzz' }; Object.entries(obj); // [['1', 'yyy'], ['3', 'zzz'], ['10': 'xxx']] Object.entries('es8'); // [['0', 'e'], ['1', 's'], ['2', '8']]
为 String 对象增加了 2 个函数:padStart 和 padEnd。
像它们名字那样,这几个函数的主要目的就是填补字符串的首部和尾部,为了使得到的结果字符串的长度能达到给定的长度。你可以通过特定的字符,或者字符串,或者默认的空格填充它。下面是函数的声明:
str.padStart(targetLength [, padString]) str.padEnd(targetLength [, padString])
这些函数的第一个参数是 targetLength(目标长度),这个是结果字符串的长度。第二个参数是可选的 padString(填充字符),一个用于填充到源字符串的字符串。默认值是空格。
'es8'.padStart(2); // 'es8' 'es8'.padStart(5); // ' es8' 'es8'.padStart(6, 'woof'); // 'wooes8' 'es8'.padStart(14, 'wow'); // 'wowwowwowwoes8' 'es8'.padStart(7, '0'); // '0000es8' 'es8'.padEnd(2); // 'es8' 'es8'.padEnd(5); // 'es8 ' 'es8'.padEnd(6, 'woof'); // 'es8woo' 'es8'.padEnd(14, 'wow'); // 'es8wowwowwowwo' 'es8'.padEnd(7, '6'); // 'es86666'
getOwnPropertyDescriptors 方法返回指定对象所有自身属性的描述对象。属性描述对象是直接在对象上定义的,而不是继承于对象的原型。ES2017加入这个函数的主要动机在于方便将一个对象深度拷贝给另一个对象,同时可以将getter/setter拷贝。声明如下:
Object.getOwnPropertyDescriptors(obj)
obj 是待操作对象。返回的描述对象键值有:configurable, enumerable, writable, get, set and value。
const obj = { get es7() { return 777; }, get es8() { return 888; } }; Object.getOwnPropertyDescriptor(obj); // { // es7: { // configurable: true, // enumerable: true, // get: function es7(){}, //the getter function // set: undefined // }, // es8: { // configurable: true, // enumerable: true, // get: function es8(){}, //the getter function // set: undefined // } // }
结尾逗号用代码展示非常明了:
// 参数定义时 function foo( param1, param2, ) {} // 函数调用时 foo( 'abc', 'def', ); // 对象中 let obj = { first: 'Jane', last: 'Doe', }; // 数组中 let arr = [ 'red', 'green', 'blue', ];
这个改动有什么好处呢?
[ 'foo' ]
修改为
[ 'foo', 'bar' ]
导致线条'foo'和线条'bar'被标记为已更改,即使唯一真正的变化是后一条线被添加。
ES9的新特性索引如下:
主要新功能:
新的正则表达式功能:
其他新功能:
ES6引入了同步迭代器,其工作原理如下:
示例:
const iterable = ['a', 'b']; const iterator = iterable[Symbol.iterator](); iterator.next() // { value: 'a', done: false } iterator.next() // { value: 'b', done: false } iterator.next() // { value: undefined, done: true }
先前的迭代方式是同步的,并不适用于异步数据源。例如,在以下代码中,readLinesFromFile()无法通过同步迭代传递其异步数据:
for (const line of readLinesFromFile(fileName)) { console.log(line); }
异步迭代器和常规迭代器的工作方式非常相似,但是异步迭代器涉及promise:
async function example() { // 普通迭代器: const iterator = createNumberIterator(); iterator.next(); // Object {value: 1, done: false} iterator.next(); // Object {value: 2, done: false} iterator.next(); // Object {value: 3, done: false} iterator.next(); // Object {value: undefined, done: true} // 异步迭代器: const asyncIterator = createAsyncNumberIterator(); const p = asyncIterator.next(); // Promise await p;// Object {value: 1, done: false} await asyncIterator.next(); // Object {value: 2, done: false} await asyncIterator.next(); // Object {value: 3, done: false} await asyncIterator.next(); // Object {value: undefined, done: true} }
异步迭代器对象的next()方法返回了一个Promise,解析后的值跟普通的迭代器类似。
用法:iterator.next().then(({ value, done })=> {//{value: ‘some val’, done: false}}
const promises = [ new Promise(resolve => resolve(1)), new Promise(resolve => resolve(2)), new Promise(resolve => resolve(3)), ]; async function test() { for await (const p of promises) { console.log(p); } } test(); //1 ,2 3
这个就是我们通常所说的rest参数和扩展运算符,这项特性在ES6中已经引入,但是ES6中的作用对象仅限于数组:
restParam(1, 2, 3, 4, 5); function restParam(p1, p2, ...p3) { // p1 = 1 // p2 = 2 // p3 = [3, 4, 5] } const values = [99, 100, -1, 48, 16]; console.log( Math.max(...values) ); // 100
在ES9中,为对象提供了像数组一样的rest参数和扩展运算符:
const obj = { a: 1, b: 2, c: 3 } const { a, ...param } = obj; console.log(a) //1 console.log(param) //{b: 2, c: 3} function foo({a, ...param}) { console.log(a); //1 console.log(param) //{b: 2, c: 3} }
//正则表达式命名捕获组 const RE_DATE = /([0-9]{4})-([0-9]{2})-([0-9]{2})/; const matchObj = RE_DATE.exec('1999-12-31'); const year = matchObj[1]; // 1999 const month = matchObj[2]; // 12 const day = matchObj[3]; // 31
通过数字引用捕获组有几个缺点:
ES9中可以通过名称来识别捕获组:(?<year>[0-9]{4})
在这里,我们用名称标记了前一个捕获组year。该名称必须是合法的JavaScript标识符(认为变量名称或属性名称)。匹配后,您可以通过访问捕获的字符串matchObj.groups.year来访问。
让我们重写前面的代码:
const RE_DATE = /(?<year>[0-9]{4})-(?<month>[0-9]{2})-(?<day>[0-9]{2})/; const matchObj = RE_DATE.exec('1999-12-31'); const year = matchObj.groups.year; // 1999 const month = matchObj.groups.month; // 12 const day = matchObj.groups.day; // 31 // 使用解构语法更为简便 const {groups: {day, year}} = RE_DATE.exec('1999-12-31'); console.log(year); // 1999 console.log(day); // 31
可以发现,命名捕获组有以下优点:
该特性允许您使用\p{}
通过提及大括号内的Unicode字符属性来匹配字符,在正则表达式中使用标记 u
(unicode) 设置。
/^\p{White_Space}+$/u.test('\t \n\r') // true /^\p{Script=Greek}+$/u.test('μετά') // true
由于在Unicode里面,中文字符对应的Unicode Script是Han,于是我们就可以用这个reg来匹配中文:
/\p{Script=Han}/u
这样我们就可以不用记忆繁琐又不好记的/[\u4e00-\u9fa5]/
了,况且这个表达式已经有些年头了,说实话,后来又新增的属性为Han的字符并不在这个范围内,因此这个有年头reg并不一定好使。
我随便从网上找了一个Unicode8.0添加的中文字符“