这篇主要涉及了Symbol、Set和Map、Proxy,和很重要的异步的新特性,尤其是最后的aysnc函数,提供了很好的异步解决方案~
引入Symbol,保证每个属性的名字都是独一无二的,这样就从根本上防止属性名的冲突。这就是说,对象的属性名现在可以有两种类型,一种是原来就有的字符串,另一种就是新增的 Symbol 类型。
此外,还能用于消除魔术字符串。
ES6 引入了一种新的原始数据类型Symbol,表示独一无二的值。它是 JavaScript 语言的第七种数据类型,前六种是:undefined、null、布尔值(Boolean)、字符串(String)、数值(Number)、对象(Object)。
Symbol 值通过Symbol函数生成。
1 | let s = Symbol(); |
Symbol是一种类似于字符串的数据类型。Symbol函数可以接受一个字符串作为参数,表示对 Symbol 实例的描述,主要是为了在控制台显示,或者转为字符串时,比较容易区分。
1 | let s1 = Symbol('foo'); |
如果 Symbol 的参数是一个对象,就会调用该对象的toString方法,将其转为字符串,然后才生成一个 Symbol 值。
1 | const obj = { |
Symbol函数的参数只是表示对当前 Symbol 值的描述,因此相同参数的Symbol函数的返回值是不相等的。
1 | // 没有参数的情况 |
Symbol 值不能与其他类型的值进行运算,会报错。
Symbol 值可以显式转为字符串。Symbol 值也可以转为布尔值,但是不能转为数值。
1 | // 创建时的参数就是添加描述 |
提供了一个实例属性description,直接返回 Symbol 的描述。
1 | const sym = Symbol('foo'); |
例子:
1 | let mySymbol = Symbol(); |
点运算符后面总是字符串。
使用 Symbol 值定义属性时,Symbol 值必须放在方括号之中。如果s不放在方括号中,该属性的键名就是字符串s,而不是s所代表的那个 Symbol 值。
Symbol 值作为属性名时,该属性还是公开属性,不是私有属性。
Symbol 类型还可以用于定义一组常量,保证这组常量的值都是不相等的。
1 | const log = {}; |
魔术字符串指的是,在代码之中多次出现、与代码形成强耦合的某一个具体的字符串或者数值。
1 | function getArea(shape, options) { |
字符串Triangle就是一个魔术字符串。它多次出现,与代码形成“强耦合”,不利于将来的修改和维护。
常用的消除魔术字符串的方法,就是把它写成一个变量。
1 | const shapeType = { |
如果仔细分析,可以发现shapeType.triangle等于哪个值并不重要,只要确保不会跟其他shapeType属性的值冲突即可。因此,这里就很适合改用 Symbol 值。
1 | const shapeType = { |
Object.getOwnPropertySymbols——返回一个数组,成员是当前对象的所有用作属性名的Symbol值。(其他方式会处理成字符串,得不到想要的symbol)
Reflect.ownKeys——返回所有类型的键名,包括常规键名和 Symbol 键名。(由于以 Symbol 值作为名称的属性,不会被常规方法遍历得到。我们可以利用这个特性,为对象定义一些非私有的、但又希望只用于内部的方法。)
我们希望重新使用同一个 Symbol 值。
Symbol.for()与Symbol()这两种写法,都会生成新的 Symbol。它们的区别是,前者会被登记在全局环境中供搜索,后者不会。Symbol.for()不会每次调用就返回一个新的 Symbol 类型的值,而是会先检查给定的key是否已经存在,如果不存在才会新建一个值。
Singleton 模式指的是调用一个类,任何时候返回的都是同一个实例。
除了定义自己使用的 Symbol 值以外,ES6 还提供了 11 个内置的 Symbol 值,指向语言内部使用的方法。
对象的Symbol.hasInstance属性,指向一个内部方法。当其他对象使用instanceof运算符,判断是否为该对象的实例时,会调用这个方法。比如,foo instanceof Foo在语言内部,实际调用的是FooSymbol.hasInstance。
对象的Symbol.isConcatSpreadable属性等于一个布尔值,表示该对象用于Array.prototype.concat()时,是否可以展开。
对象的Symbol.species属性,指向一个构造函数。创建衍生对象时,会使用该属性。
对象的Symbol.match属性,指向一个函数。当执行str.match(myObject)时,如果该属性存在,会调用它,返回该方法的返回值。
对象的Symbol.replace属性,指向一个方法,当该对象被String.prototype.replace方法调用时,会返回该方法的返回值。
对象的Symbol.split属性,指向一个方法,当该对象被String.prototype.split方法调用时,会返回该方法的返回值。
对象的Symbol.iterator属性,指向该对象的默认遍历器方法。
对象的Symbol.toPrimitive属性,指向一个方法。该对象被转为原始类型的值时,会调用这个方法,返回该对象对应的原始类型值。
对象的Symbol.toStringTag属性,指向一个方法。在该对象上面调用Object.prototype.toString方法时,如果这个属性存在,它的返回值会出现在toString方法返回的字符串之中,表示对象的类型。也就是说,这个属性可以用来定制[object Object]或[object Array]中object后面的那个字符串。
对象的Symbol.unscopables属性,指向一个对象。该对象指定了使用with关键字时,哪些属性会被with环境排除。
Set感觉是对数组的补充,它保证了数组中没有重复的值。Map是对对象的补充,它从对象的键值对应变为值值对应,键本身只能是字符串。
ES6 提供了新的数据结构 Set。它类似于数组,但是成员的值都是唯一的,没有重复的值。
1 | // new |
Set函数可以接受一个数组(或者具有 iterable 接口的其他数据结构)作为参数,用来初始化。
1 | // 去除数组的重复成员 |
向 Set 加入值的时候,不会发生类型转换。
Set 内部判断两个值是否不同,使用的算法叫做“Same-value-zero equality”,它类似于精确相等运算符(===),主要的区别是NaN等于自身,而精确相等运算符认为NaN不等于自身。
在 Set 内部,两个NaN是相等,两个对象总是不相等的。
Set 结构的实例有以下属性。
Set 实例的方法分为两大类:操作方法(用于操作数据)和遍历方法(用于遍历成员)。下面先介绍四个操作方法。
Set的遍历顺序就是插入顺序。这个特性有时非常有用,比如使用 Set 保存一个回调函数列表,调用时就能保证按照添加顺序调用。
Set 结构的实例有四个遍历方法,可以用于遍历成员。
扩展运算符(…)内部使用for…of循环,所以也可以用于 Set 结构。扩展运算符和 Set 结构相结合,就可以去除数组的重复成员。而且,数组的map和filter方法也可以间接用于 Set 了。
1 | let a = new Set([1, 2, 3]); |
1 | // 同步改变原来的 Set 结构 |
主要优点是不会引起内存泄漏。
WeakSet 结构与 Set 类似,也是不重复的值的集合。但是,它与 Set 有两个区别。
WeakSet 的成员只能是对象。
WeakSet 中的对象都是弱引用,即垃圾回收机制不考虑 WeakSet 对该对象的引用,也就是说,如果其他对象都不再引用该对象,那么垃圾回收机制会自动回收该对象所占用的内存,不考虑该对象还存在于 WeakSet 之中。
WeakSet 的成员是不适合引用的,因为它会随时消失
WeakSet 不可遍历
1 | // 作为构造函数,WeakSet 可以接受一个数组或类似数组的对象作为参数。(实际上,任何具有 Iterable 接口的对象,都可以作为 WeakSet 的参数。)该数组的所有成员,都会自动成为 WeakSet 实例对象的成员。 |
WeakSet 结构有以下三个方法。
WeakSet 的一个用处,是储存 DOM 节点,而不用担心这些节点从文档移除时,会引发内存泄漏。
另一个例子,保证了Foo的实例方法,只能在Foo的实例上调用。这里使用 WeakSet 的好处是,foos对实例的引用,不会被计入内存回收机制,所以删除实例的时候,不用考虑foos,也不会出现内存泄漏。
ES6 提供了 Map 数据结构。它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。也就是说,Object 结构提供了“字符串—值”的对应,Map 结构提供了“值—值”的对应,是一种更完善的 Hash 结构实现。如果你需要“键值对”的数据结构,Map 比 Object 更合适。
作为构造函数,Map 可以接受一个数组作为参数。该数组的成员是一个个表示键值对的数组。
1 | const map = new Map([ |
不仅仅是数组,任何具有 Iterator 接口、且每个成员都是一个双元素的数组的数据结构(详见《Iterator》一章)都可以当作Map构造函数的参数。这就是说,Set和Map都可以用来生成新的 Map。
如果对同一个键多次赋值,后面的值将覆盖前面的值。
如果读取一个未知的键,则返回undefined。
只有对同一个对象的引用,Map 结构才将其视为同一个键。Map 的键实际上是跟内存地址绑定的,只要内存地址不一样,就视为两个键。
NaN不严格相等于自身,但 Map 将其视为同一个键。
1 | const map = new Map(); |
size 属性:size属性返回 Map 结构的成员总数。
set(key, value):set方法设置键名key对应的键值为value,然后返回整个 Map 结构。如果key已经有值,则键值会被更新,否则就新生成该键。
get(key):get方法读取key对应的键值,如果找不到key,返回undefined。
has(key):has方法返回一个布尔值,表示某个键是否在当前 Map 对象之中。
delete(key):delete方法删除某个键,返回true。如果删除失败,返回false。
clear():clear方法清除所有成员,没有返回值。
Map 结构原生提供三个遍历器生成函数和一个遍历方法。
Map 的遍历顺序就是插入顺序。可以使用扩展运算符(…)转化为数组。
1 | const map0 = new Map() |
1 | function strMapToObj(strMap) { |
1 | function objToStrMap(obj) { |
Map 转为 JSON——Map 转为 JSON 要区分两种情况。
JSON 转为 Map——也分为两种情况。
WeakMap与Map的区别有两点。
WeakMap只接受对象作为键名(null除外),不接受其他类型的值作为键名。
WeakMap的键名所指向的对象,不计入垃圾回收机制。
一个典型应用场景是,在网页的 DOM 元素上添加数据,就可以使用WeakMap结构。当该 DOM 元素被清除,其所对应的WeakMap记录就会自动被移除。
WeakMap 弱引用的只是键名,而不是键值。键值依然是正常引用。
WeakMap只有四个方法可用:get()、set()、has()、delete()。
Proxy 用于修改某些操作的默认行为,等同于在语言层面做出修改,所以属于一种“元编程”(meta programming),即对编程语言进行编程。
Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。
1 | // 对一个空对象架设了一层拦截,重定义了属性的读取(get)和设置(set)行为 |
上面代码说明,Proxy 实际上重载(overload)了点运算符,即用自己的定义覆盖了语言的原始定义。
要使得Proxy起作用,必须针对Proxy实例(上例是proxy对象)进行操作,而不是针对目标对象(上例是空对象)进行操作。
一个技巧是将 Proxy 对象,设置到object.proxy属性,从而可以在object对象上调用。
1 | var object = { proxy: new Proxy(target, handler) }; |
Proxy 实例也可以作为其他对象的原型对象。
同一个拦截器函数,可以设置拦截多个操作。
Proxy.revocable方法返回一个可取消的 Proxy 实例。
Proxy.revocable的一个使用场景是,目标对象不允许直接访问,必须通过代理访问,一旦访问结束,就收回代理权,不允许再次访问。
在 Proxy 代理的情况下,目标对象内部的this关键字会指向 Proxy 代理。这时,this绑定原始对象,就可以解决这个问题。
1 | const service = createWebService('http://example.com/data'); |
Reflect对象的设计目的有这样几个。
将Object对象的一些明显属于语言内部的方法(比如Object.defineProperty),放到Reflect对象上。也就是说,从Reflect对象上可以拿到语言内部的方法。
修改某些Object方法的返回结果,让其变得更合理。
1 | // 老写法 |
1 | // 老写法 |
1 | Proxy(target, { |
保证原生行为能够正常执行,再部署额外的功能。
Reflect对象一共有 13 个静态方法。
观察者模式(Observer mode)指的是函数自动观察数据对象,一旦对象有变化,函数就会自动执行。
1 |
|
使用 Proxy 写一个观察者模式的最简单实现,即实现observable和observe这两个函数。思路是observable函数返回一个原始对象的 Proxy 代理,拦截赋值操作,触发充当观察者的各个函数。
1 | const queuedObservers = new Set(); |
上面代码中,先定义了一个Set集合,所有观察者函数都放进这个集合。然后,observable函数返回原始对象的代理,拦截赋值操作。拦截函数set之中,会自动执行所有观察者。
Promise 是异步编程的一种解决方案。
所谓Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理。
Promise对象有以下两个特点。
(1)对象的状态不受外界影响。Promise对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。
(2)一旦状态改变,就不会再变,任何时候都可以得到这个结果。Promise对象的状态改变,只有两种可能:从pending变为fulfilled和从pending变为rejected。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果,这时就称为 resolved(已定型)。如果改变已经发生了,你再对Promise对象添加回调函数,也会立即得到这个结果。这与事件(Event)完全不同,事件的特点是,如果你错过了它,再去监听,是得不到结果的。
有了Promise对象,就可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。此外,Promise对象提供统一的接口,使得控制异步操作更加容易。
Promise也有一些缺点。
(1)无法取消Promise,一旦新建它就会立即执行,无法中途取消。
(2)如果不设置回调函数,Promise内部抛出的错误,不会反应到外部。
(3)当处于pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。
如果某些事件不断地反复发生,一般来说,使用 Stream 模式是比部署Promise更好的选择。
Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolve和reject。它们是两个函数,由 JavaScript 引擎提供,不用自己部署。
1 | const promise = new Promise(function(resolve, reject) { |
Promise实例生成以后,可以用then方法分别指定resolved状态和rejected状态的回调函数。
1 | promise.then(function(value) { |
如果调用resolve函数和reject函数时带有参数,那么它们的参数会被传递给回调函数。
调用resolve或reject并不会终结 Promise 的参数函数的执行。立即 resolved 的 Promise 是在本轮事件循环的末尾执行,总是晚于本轮循环的同步任务。所以,最好在它们前面加上return语句,这样就不会有意外。
1 | new Promise((resolve, reject) => { |
then方法的第一个参数是resolved状态的回调函数,第二个参数(可选)是rejected状态的回调函数。
then方法返回的是一个新的Promise实例(注意,不是原来那个Promise实例)。因此可以采用链式写法,即then方法后面再调用另一个then方法。
采用链式的then,可以指定一组按照次序调用的回调函数。这时,前一个回调函数,有可能返回的还是一个Promise对象(即有异步操作),这时后一个回调函数,就会等待该Promise对象的状态发生变化,才会被调用。
1 | getJSON("/post/1.json").then(function(post) { |
Promise.prototype.catch方法是.then(null, rejection)或.then(undefined, rejection)的别名,用于指定发生错误时的回调函数。
Promise 对象的错误具有“冒泡”性质,会一直向后传递,直到被捕获为止。也就是说,错误总是会被下一个catch语句捕获。
一般来说,不要在then方法里面定义 Reject 状态的回调函数(即then的第二个参数),总是使用catch方法。
1 | // bad |
跟传统的try/catch代码块不同的是,如果没有使用catch方法指定错误处理的回调函数,Promise 对象抛出的错误不会传递到外层代码,即不会有任何反应。
一般总是建议,Promise 对象后面要跟catch方法,这样可以处理 Promise 内部发生的错误。catch方法返回的还是一个 Promise 对象,因此后面还可以接着调用then方法。
finally方法用于指定不管 Promise 对象最后状态如何,都会执行的操作。该方法是 ES2018 引入标准的。
1 | promise |
finally本质上是then方法的特例。
Promise.all方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。Promise.all方法的参数可以不是数组,但必须具有 Iterator 接口,且返回的每个成员都是 Promise 实例。
p的状态由p1、p2、p3决定,分成两种情况。
(1)只有p1、p2、p3的状态都变成fulfilled,p的状态才会变成fulfilled,此时p1、p2、p3的返回值组成一个数组,传递给p的回调函数。
(2)只要p1、p2、p3之中有一个被rejected,p的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数。
1 | // 生成一个Promise对象的数组 |
如果作为参数的 Promise 实例,自己定义了catch方法,那么它一旦被rejected,并不会触发Promise.all()的catch方法。
Promise.race方法同样是将多个 Promise 实例,包装成一个新的 Promise 实例。
1 | const p = Promise.race([p1, p2, p3]); |
只要p1、p2、p3之中有一个实例率先改变状态,p的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给p的回调函数。
有时需要将现有对象转为 Promise 对象,Promise.resolve方法就起到这个作用。
1 | Promise.resolve('foo') |
有以下几种情况:
(1)参数是一个 Promise 实例——Promise.resolve将不做任何修改、原封不动地返回这个实例。
(2)参数是一个thenable对象
thenable对象指的是具有then方法的对象,比如下面这个对象。
1 | let thenable = { |
Promise.resolve方法会将这个对象转为 Promise 对象,然后就立即执行thenable对象的then方法。
1 | let p1 = Promise.resolve(thenable); |
(3)参数不是具有then方法的对象,或根本就不是对象
如果参数是一个原始值,或者是一个不具有then方法的对象,则Promise.resolve方法返回一个新的 Promise 对象,状态为resolved。
1 | const p = Promise.resolve('Hello'); |
(4)不带有任何参数
Promise.resolve()方法允许调用时不带参数,直接返回一个resolved状态的 Promise 对象。
所以,如果希望得到一个 Promise 对象,比较方便的方法就是直接调用Promise.resolve()方法。
1 | const p = Promise.resolve(); |
1 | setTimeout(function () { |
setTimeout(fn, 0)在下一轮“事件循环”开始时执行,Promise.resolve()在本轮“事件循环”结束时执行,console.log(‘one’)则是立即执行,因此最先输出。
Promise.reject(reason)方法也会返回一个新的 Promise 实例,该实例的状态为rejected。
Promise.reject()方法的参数,会原封不动地作为reject的理由,变成后续方法的参数。这一点与Promise.resolve方法不一致。
我们可以将图片的加载写成一个Promise,一旦加载完成,Promise的状态就发生变化。
1 | const preloadImage = function (path) { |
使用 Generator 函数管理流程,遇到异步操作的时候,通常返回一个Promise对象。
让同步函数同步执行,异步函数异步执行,并且让它们具有统一的 API。
Promise.try为所有操作提供了统一的处理机制,所以如果想用then方法管理流程,最好都用Promise.try包装一下。
1 | // 可以更好地管理异常,统一用promise.catch()捕获所有同步和异步的错误。 |
遍历器(Iterator)就是这样一种机制。它是一种接口,为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署 Iterator 接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)。
Iterator 的作用有三个:
ES6 规定,默认的 Iterator 接口部署在数据结构的Symbol.iterator属性,或者说,一个数据结构只要具有Symbol.iterator属性,就可以认为是“可遍历的”(iterable)。Symbol.iterator属性本身是一个函数,就是当前数据结构默认的遍历器生成函数。执行这个函数,就会返回一个遍历器。至于属性名Symbol.iterator,它是一个表达式,返回Symbol对象的iterator属性,这是一个预定义好的、类型为 Symbol 的特殊值,所以要放在方括号内。
ES6 的有些数据结构原生具备 Iterator 接口(比如数组),即不用任何处理,就可以被for…of循环遍历。
原生具备 Iterator 接口的数据结构如下。
一个对象如果要具备可被for…of循环调用的 Iterator 接口,就必须在Symbol.iterator的属性上部署遍历器生成方法(原型链上的对象具有该方法也可)。
1 | class RangeIterator { |
对于类似数组的对象(存在数值键名和length属性),部署 Iterator 接口,有一个简便方法,就是Symbol.iterator方法直接引用数组的 Iterator 接口。
1 | let iterable = { |
Symbol.iterator方法对应的是遍历器生成函数(即会返回一个遍历器对象)
(1)解构赋值
(2)扩展运算符
(3)yield*
(4)由于数组的遍历会调用遍历器接口,所以任何接受数组作为参数的场合,其实都调用了遍历器接口。下面是一些例子。
可以覆盖原生的Symbol.iterator方法,达到修改遍历器行为的目的。
Symbol.iterator方法的最简单实现,还是使用下一章要介绍的 Generator 函数。
1 | let myIterable = { |
遍历器对象除了具有next方法,还可以具有return方法和throw方法。next方法是必须部署的,return方法和throw方法是否部署是可选的。
return方法的使用场合是,如果for…of循环提前退出(通常是因为出错,或者有break语句),就会调用return方法。如果一个对象在完成遍历前,需要清理或释放资源,就可以部署return方法。
throw方法主要是配合 Generator 函数使用,一般的遍历器对象用不到这个方法。
for…of循环相比其他几种做法,有一些显著的优点。
语法上,可以理解成,Generator 函数是一个状态机,封装了多个内部状态。
执行 Generator 函数会返回一个遍历器对象,也就是说,Generator 函数除了状态机,还是一个遍历器对象生成函数。返回的遍历器对象,可以依次遍历 Generator 函数内部的每一个状态。
形式上,Generator 函数是一个普通函数,但是有两个特征。一是,function关键字与函数名之间有一个星号;二是,函数体内部使用yield表达式,定义不同的内部状态(yield在英语里的意思就是“产出”)。
1 | function* helloWorldGenerator() { |
Generator 函数是分段执行的,yield表达式是暂停执行的标记,而next方法可以恢复执行。
ES6 没有规定,function关键字与函数名之间的星号,写在哪个位置。这导致下面的写法都能通过。
1 | function * foo(x, y) { ··· } |
yield表达式与return语句既有相似之处,也有区别。
相似之处在于,都能返回紧跟在语句后面的那个表达式的值。
区别在于每次遇到yield,函数暂停执行,下一次再从该位置继续向后执行,而return语句不具备位置记忆的功能。一个函数里面,只能执行一次(或者说一个)return语句,但是可以执行多次(或者说多个)yield表达式。正常函数只能返回一个值,因为只能执行一次return;Generator 函数可以返回一系列的值,因为可以有任意多个yield。
yield表达式只能用在 Generator 函数里面。
yield表达式如果用在另一个表达式之中,必须放在圆括号里面。
1 | function* demo() { |
yield表达式用作函数参数或放在赋值表达式的右边,可以不加括号。
1 | function* demo() { |
由于 Generator 函数就是遍历器生成函数,因此可以把 Generator 赋值给对象的Symbol.iterator属性,从而使得该对象具有 Iterator 接口。(for…of章节提过)
yield表达式本身没有返回值,或者说总是返回undefined。next方法可以带一个参数,该参数就会被当作上一个yield表达式的返回值。
1 | function* foo(x) { |
for…of循环可以自动遍历 Generator 函数运行时生成的Iterator对象,且此时不再需要调用next方法。
除了for…of循环以外,扩展运算符(…)、解构赋值和Array.from方法内部调用的,都是遍历器接口。这意味着,它们都可以将 Generator 函数返回的 Iterator 对象,作为参数。
Generator 函数返回的遍历器对象,都有一个throw方法,可以在函数体外抛出错误,然后在 Generator 函数体内捕获。
如果 Generator 函数内部和外部,都没有部署try…catch代码块,那么程序将报错,直接中断执行。
throw方法抛出的错误要被内部捕获,前提是必须至少执行过一次next方法。
Generator 函数返回的遍历器对象,还有一个return方法,可以返回给定的值,并且终结遍历 Generator 函数。
next()、throw()、return()这三个方法本质上是同一件事,可以放在一起理解。它们的作用都是让 Generator 函数恢复执行,并且使用不同的语句替换yield表达式。
next()是将yield表达式替换成一个值。
throw()是将yield表达式替换成一个throw语句。
return()是将yield表达式替换成一个return语句。
ES6 提供了yield*表达式,用来在一个 Generator 函数里面执行另一个 Generator 函数。
1 | function* bar() { |
yield*后面的 Generator 函数(没有return语句时),等同于在 Generator 函数内部,部署一个for…of循环。
yield*命令可以很方便地取出嵌套数组的所有成员。
如果一个对象的属性是 Generator 函数,可以简写成下面的形式。
1 | let obj = { |
Generator 函数返回的总是遍历器对象,而不是this对象。
Generator 函数也不能跟new命令一起用,会报错。
让 Generator 函数返回一个正常的对象实例,既可以用next方法,又可以获得正常的this。(略)
Generator 是实现状态机的最佳结构。
1 | var clock = function* () { |
协程(coroutine)是一种程序运行的方式,可以理解成“协作的线程”或“协作的函数”。协程既可以用单线程实现,也可以用多线程实现。前者是一种特殊的子例程,后者是一种特殊的线程。
Generator 可以暂停函数执行,返回任意表达式的值。这种特点使得 Generator 有多种应用场景。
Generator 函数的暂停执行的效果,意味着可以把异步操作写在yield表达式里面,等到调用next方法时再往后执行。这实际上等同于不需要写回调函数了。
1 | function* main() { |
1 | let steps = [step1Func, step2Func, step3Func]; |
ES6 诞生以前,异步编程的方法,大概有下面四种。
所谓”异步”,简单说就是一个任务不是连续完成的,可以理解成该任务被人为分成两段,先执行第一段,然后转而执行其他任务,等做好了准备,再回过头执行第二段。
JavaScript 语言对异步编程的实现,就是回调函数。所谓回调函数,就是把任务的第二段单独写在一个函数里面,等到重新执行这个任务的时候,就直接调用这个函数。
Promise 对象就是为了解决这个问题而提出的。它不是新的语法功能,而是一种新的写法,允许将回调函数的嵌套,改成链式调用。
Promise 的写法只是回调函数的改进,使用then方法以后,异步任务的两段执行看得更清楚了,除此以外,并无新意。
Promise 的最大问题是代码冗余,原来的任务被 Promise 包装了一下,不管什么操作,一眼看去都是一堆then,原来的语义变得很不清楚。
“协程”(coroutine),意思是多个线程互相协作,完成异步任务。
协程有点像函数,又有点像线程。它的运行流程大致如下。
第一步,协程A开始执行。
第二步,协程A执行到一半,进入暂停,执行权转移到协程B。
第三步,(一段时间后)协程B交还执行权。
第四步,协程A恢复执行。
上面流程的协程A,就是异步任务,因为它分成两段(或多段)执行。
Generator 函数是协程在 ES6 的实现,最大特点就是可以交出函数的执行权(即暂停执行)。
它还有两个特性,使它可以作为异步编程的完整解决方案:函数体内外的数据交换和错误处理机制。
next返回值的 value 属性,是 Generator 函数向外输出数据;next方法还可以接受参数,向 Generator 函数体内输入数据。
Generator 函数体外,使用指针对象的throw方法抛出的错误,可以被函数体内的try…catch代码块捕获。
这意味着,出错的代码与处理错误的代码,实现了时间和空间上的分离,这对于异步编程无疑是很重要的。
虽然 Generator 函数将异步操作表示得很简洁,但是流程管理却不方便(即何时执行第一阶段、何时执行第二阶段)。
Thunk 函数是自动执行 Generator 函数的一种方法。
async 函数是什么?一句话,它就是 Generator 函数的语法糖。async函数就是将 Generator 函数的星号(*)替换成async,将yield替换成await。
前文有一个 Generator 函数,依次读取两个文件。
1 | const fs = require('fs'); |
上面代码的函数gen可以写成async函数,就是下面这样。
1 | const asyncReadFile = async function () { |
async函数对 Generator 函数的改进,体现在以下四点。
(1)内置执行器。
Generator 函数的执行必须靠执行器,所以才有了co模块,而async函数自带执行器。也就是说,async函数的执行,与普通函数一模一样,只要一行。
(2)更好的语义。
async和await,比起星号和yield,语义更清楚了。async表示函数里有异步操作,await表示紧跟在后面的表达式需要等待结果。
(3)更广的适用性。
co模块约定,yield命令后面只能是 Thunk 函数或 Promise 对象,而async函数的await命令后面,可以是 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时会自动转成立即 resolved 的 Promise 对象)。
(4)返回值是 Promise。
async函数的返回值是 Promise 对象,这比 Generator 函数的返回值是 Iterator 对象方便多了。你可以用then方法指定下一步的操作。
进一步说,async函数完全可以看作多个异步操作,包装成的一个 Promise 对象,而await命令就是内部then命令的语法糖。
async 函数有多种使用形式。
1 | // 函数声明 |
async函数返回一个 Promise 对象。
async函数内部return语句返回的值,会成为then方法回调函数的参数。
async函数内部抛出错误,会导致返回的 Promise 对象变为reject状态。抛出的错误对象会被catch方法回调函数接收到。
1 | async function f() { |
只有async函数内部的异步操作执行完,才会执行then方法指定的回调函数。除非遇到return语句或者抛出错误。
正常情况下,await命令后面是一个 Promise 对象,返回该对象的结果。如果不是 Promise 对象,就直接返回对应的值。
1 | async function f() { |
另一种情况是,await命令后面是一个thenable对象(即定义then方法的对象),那么await会将其等同于 Promise 对象。
1 | // 借助await命令就可以让程序停顿指定的时间。 |
任何一个await语句后面的 Promise 对象变为reject状态,那么整个async函数都会中断执行。
1 | async function f() { |
有时,我们希望即使前一个异步操作失败,也不要中断后面的异步操作。await后面的 Promise 对象再跟一个catch方法,处理前面可能出现的错误。
1 | async function f() { |
如果await后面的异步操作出错,那么等同于async函数返回的 Promise 对象被reject。
1 | async function f() { |
防止出错的方法,也是将其放在try…catch代码块之中。
1 | async function main() { |
1 | async function main() { |
1 | // 继发关系。这样比较耗时,因为只有getFoo完成以后,才会执行getBar,完全可以让它们同时触发 |
1 | // 同时触发 |
如果将forEach方法的参数改成async函数,也有问题。
1 | // 并发执行 |
1 | // 继发执行 |
如果确实希望多个请求并发执行,可以使用Promise.all方法。当三个请求都会resolved时,下面两种写法效果相同。
1 | async function dbFuc(db) { |
1 | const a = () => { |
上面代码中,函数a内部运行了一个异步任务b()。当b()运行的时候,函数a()不会中断,而是继续执行。等到b()运行结束,可能a()早就运行结束了,b()所在的上下文环境已经消失了。如果b()或c()报错,错误堆栈将不包括a()。
现在将这个例子改成async函数。
1 | const a = async () => { |
上面代码中,b()运行的时候,a()是暂停执行,上下文环境都保存着。一旦b()或c()报错,错误堆栈将包括a()。
async 函数的实现原理,就是将 Generator 函数和自动执行器,包装在一个函数里。
1 | async function chainAnimationsAsync(elem, animations) { |
可以看到 Async 函数的实现最简洁,最符合语义,几乎没有语义不相关的代码。它将 Generator 写法中的自动执行器,改在语言层面提供,不暴露给用户,因此代码量最少。如果使用 Generator 写法,自动执行器需要用户自己提供。
1 | // 继发执行 |
目前,有一个语法提案,允许在模块的顶层独立使用await命令。这个提案的目的,是借用await解决模块异步加载的问题。