深入学习ES6
什么是ES6?什么是ES2015?它们是什么关系呢?
ES2015是ECMAScript 2015的简称,ECMA又是欧洲计算机制造者协会的简称,ECMAScript代表 JavaScript 这门语言所遵循的规范。2011 年,ECMAScript 5.1 版发布后,就开始制定 6.0 版了,直到2015年ES6.0才正式出炉。因此,ES6 这个词的原意,就是指 JavaScript 语言在ES5.1后的下一个版本ES6.0。
标准委员会决定,标准在每年的 6 月份正式发布一次,作为当年的正式版本。这样一来,就不需要以前的版本号了,只要用年份标记就可以了。(ES2015、ES2016…)
所以,ES6 既是一个历史名词,也是一个泛指,含义是 5.1 版以后的 JavaScript 的下一代标准,涵盖了 ES2015、ES2016、ES2017 等等,而 ES2015 则是正式名称,特指该年发布的正式版本的语言标准。
ES6代表着 JS 这门语言最新的标准,是非常有必要学习的。目前以Chrome为首的现代浏览器已经基本实现了ES6的大多数新特性,即使在不支持ES6的浏览器上,开发者也可以通过转译工具如Babel
将ES6代码转译为ES5代码来实现兼容。
本课程的学习目标
掌握ES6中进阶的内容包括: Symbol
、 Set和WeakSet
、 Map和WeakMap
、 Proxy
、 Reflect
、 Iterator
、 Generator函数
、 async函数
的概念、用法和用途
Symbol
概述
ES6 引入了一种新的原始数据类型Symbol
,表示独一无二的值。它是 JavaScript 语言的第七种数据类型,前六种是:undefined
、null
、Boolean
、String
、Number
、Object
直接调用Symbol
函数即可生成一个Symbol,注意Symbol
函数前不能使用new
命令,否则会报错。
Symbol
函数可以接受一个字符串作为参数,表示对 Symbol 的描述,主要是为了在控制台显示,或者转为字符串时,比较容易区分。
let s1 = Symbol('foo');
let s2 = Symbol('bar');
s1 // Symbol(foo)
s2 // Symbol(bar)
注意,Symbol
函数的参数只是表示对当前 Symbol 值的描述,因此相同参数的Symbol
函数的返回值是不相等的。
// 没有参数的情况
let s1 = Symbol();
let s2 = Symbol();
s1 === s2 // false
// 有参数的情况
let s1 = Symbol('foo');
let s2 = Symbol('foo');
s1 === s2 // false
Symbol作为属性
Symbol 值可以作为标识符,用于对象的属性名,由于每一个 Symbol 值都是不相等的,这意味着就能保证不会出现同名的属性,能防止某一个键被不小心改写或覆盖的情况。
注意,在对象的内部,使用 Symbol 值定义属性时,Symbol 值必须放在方括号之中。
let mySymbol = Symbol();
// 第一种写法
let a = {};
a[mySymbol] = 'Hello!';
// 第二种写法
let a = {
[mySymbol]: 'Hello!'
};
// 第三种写法
let a = {};
Object.defineProperty(a, mySymbol, { value: 'Hello!' });
// 以上写法都得到同样结果
a[mySymbol] // "Hello!"
Symbol 作为属性名,该属性不会出现在for...in
、for...of
循环中,也不会被Object.keys()
、Object.getOwnPropertyNames()
、JSON.stringify()
返回。但是,有一个Object.getOwnPropertySymbols
方法,可以获取指定对象的所有 Symbol 属性名。
Symbol.for
有时,我们希望重新使用同一个 Symbol 值,Symbol.for
方法可以做到这一点。它接受一个字符串作为参数,然后搜索有没有以该参数作为名称的 Symbol 值。如果有,就返回这个 Symbol 值,否则就新建并返回一个以该字符串为名称的 Symbol 值。
let s1 = Symbol.for('foo');
let s2 = Symbol.for('foo');
s1 === s2 // true
Symbol.for()
与Symbol()
这两种写法,都会生成新的 Symbol。它们的区别是,前者会被登记在全局环境中供搜索,后者不会。Symbol.for()
不会每次调用就返回一个新的 Symbol 类型的值,而是会先检查给定的key
是否已经存在,如果不存在才会新建一个值。比如,如果你调用Symbol.for("cat")
30 次,每次都会返回同一个 Symbol 值,但是调用Symbol("cat")
30 次,会返回 30 个不同的 Symbol 值。
内置Symbol
除了定义自己使用的 Symbol 值以外,ES6 还提供了 11 个内置的 Symbol 值,指向语言内部使用的方法。
Symbol.hasInstance
Symbol.isConcatSpreadable
Symbol.species
Symbol.match
Symbol.replace
Symbol.search
Symbol.split
Symbol.toPrimitive
Symbol.toStringTag
Symbol.unscopables
-
Symbol.iterator
,对象的Symbol.iterator
属性,指向该对象的默认生成遍历器
的方法。
实例:
消除魔术字符串
function getArea(shape, options) {
let area = 0;
switch (shape) {
case 'Triangle': // 魔术字符串
area = .5 * options.width * options.height;
break;
/* ... more code ... */
}
return area;
}
getArea('Triangle', { width: 100, height: 100 }); // 魔术字符串
上面代码中,字符串Triangle
就是一个魔术字符串。它多次出现,与代码形成“强耦合”,不利于将来的修改和维护。
常用的消除魔术字符串的方法,就是把它写成一个变量。
const shapeType = {
triangle: 'Triangle'
};
function getArea(shape, options) {
let area = 0;
switch (shape) {
case shapeType.triangle:
area = .5 * options.width * options.height;
break;
}
return area;
}
getArea(shapeType.triangle, { width: 100, height: 100 });
但是如果没有办法保证有人写了一句
const shapeType = {
triangle: 'Triangle',
rectangle:'Triangle'
};
接下来一旦出现bug会很难调试
如果仔细分析,可以发现shapeType.triangle
等于哪个值并不重要,只要确保不会跟其他shapeType
属性的值冲突即可。因此,这里就很适合改用 Symbol 值。
const shapeType = {
triangle: Symbol(),
rectangle:Symbol()
};
实现私有属性
第一种方式:用一个字符串或者下划线的方式
var Person = (function() {
let _name = `_name`
// 或者 let name = 'shfjkshfkjsjkf'
function Person(name) {
this[_name] = name;
}
Person.prototype.getName = function() {
return this[_name];
};
return Person;
}());
缺点很显然,不是真正的私有,依然可以遍历
第二种方式:闭包
var Person = (function() {
function Person(name) {
this.getName = function() {
return name;
};
}
return Person;
}());
真的私有了,但是仍然存在缺点:实例无法共享方法,浪费内存空间
第三种方式:使用Symbol
var Person = (function() {
var nameSymbol = Symbol('name');
function Person(name) {
this[nameSymbol] = name;
}
Person.prototype.getName = function() {
return this[nameSymbol];
};
return Person;
}());
缺陷:仍然会被Object.getOwnPropertySymbols
获取到属性,进而修改该属性对应的值
Set和WeakSet
Set
ES6 提供了新的数据结构 Set
。它类似于数组,但是成员的值都是唯一的,没有重复的值。需要记录不同成员的又不希望重复记录的情况下可以用到Set
如何生成Set:
let set1 = new Set()
let set2 = new Set([1,2,3])
Set 实例的属性:
-
Set.prototype.size
:返回Set
实例的成员总数。
Set 实例的方法分为两大类:操作方法(用于操作数据)和遍历方法(用于遍历成员)。
四个操作方法:
-
Set.prototype.add(value)
:添加某个值,返回 Set 结构本身。 -
Set.prototype.delete(value)
:删除某个值,返回一个布尔值,表示删除是否成功。 -
Set.prototype.has(value)
:返回一个布尔值,表示该值是否为Set
的成员。 -
Set.prototype.clear()
:清除所有成员,没有返回值。
由于Set中值不会重复,可以用来Set来做数组去重
四个遍历方法:
-
Set.prototype.keys()
:返回键名遍历器 -
Set.prototype.values()
:返回键值遍历器 -
Set.prototype.entries()
:返回键值对遍历器 -
Set.prototype.forEach()
:使用回调函数遍历每个成员
注意:Set实例中key和value是一样的,所以keys()
和values()
这两个方法的结果是一样的
实例
Set中查找某个值是否已经存在的时间复杂度是O(1),而数组的indexOf
方法时间复杂度是O(n),又由于Set中值不会重复,所以可以使用Set做数组去重:
//使用indexOf 缺点:时间复杂度O(n^2)性能低下, NaN要做特殊处理
function deduplicate1(arr) {
let temp =[]
for (let i = 0; i < arr.length; i++) {
if(temp.indexOf(arr[i]) === -1){
temp.push(arr[i])
}
}
return temp
}
//使用对象解决性能问题 但是数组里不能有对象,null,undefined,Boolean值,也无法区分字符串和数字
function deduplicate2(arr) {
let temp = {}
for (let i = 0; i < arr.length; i++) {
if(!temp[arr[i]]){
temp[arr[i]] = true
}
}
return Object.keys(temp)
}
//使用Set来去重
function deduplicate3(arr) {
let temp = [...(new Set(arr))]
return temp
}
WeakSet
WeakSet
结构与 Set
类似,也是不重复的值的集合。但是,它与 Set
有两个区别。
-
WeakSet
的成员只能是对象,而不能是其他类型的值。 -
WeakSet
中的对象都是弱引用
如果一个对象没有任何引用,那么此对象会尽快被垃圾回收,释放掉它占用的内存。
即垃圾回收机制不考虑 WeakSet
对该对象的引用,也就是说,如果其他对象都不再引用该对象,那么垃圾回收机制会自动回收该对象所占用的内存,不考虑该对象还存在于 WeakSet
之中。
WeakSet
结构有以下三个方法。
-
WeakSet.prototype.add(value)
:向 WeakSet 实例添加一个新成员。 -
WeakSet.prototype.delete(value)
:清除 WeakSet 实例的指定成员。 -
WeakSet.prototype.has(value)
:返回一个布尔值,表示某个值是否在WeakSet
实例之中。
WeakSet
不能遍历,是因为成员都是弱引用,随时可能消失。
示例:
let div = document.querySelector('div')
let set = new Set()
set.add(div)
//...some code
document.body.removeChild(div)
div = null //dom对象仍在内存中,因为Set中仍然引用此对象
let div = document.querySelector('div')
let weakset = new WeakSet()
weakset.add(div)
//...some code
document.body.removeChild(div)
div = null //dom对象的已经没有引用,将被垃圾回收机制回收
Map和WeakMap
Map
JavaScript 的对象(Object),本质上是键值对的集合(Hash 结构),但是传统上只能用字符串当作键。这给它的使用带来了很大的限制。
为了解决这个问题,ES6 提供了 Map 数据结构。它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键,是一种更完善的 Hash 结构实现。
生成Map实例:
const map1 = new Map();
const map2 = new Map([
['name', '张三'],
['title', 'Author']
]);
Map 实例的属性:
-
Map.prototype.size
:返回Map
实例的成员总数。
Map实例的方法分为两大类:操作方法(用于操作数据)和遍历方法(用于遍历成员)。
四个操作方法:
-
Map.prototype.set(key,value)
:设置键名key
对应的键值为value
,然后返回整个 Map 结构。如果key
已经有值,则键值会被更新,否则就新生成该键。 -
Map.prototype.get(key)
:读取key
对应的键值,如果找不到key
,返回undefined
。 -
Map.prototype.has(key)
:返回一个布尔值,表示某个键是否在当前 Map 对象之中。 -
Map.prototype.delete(key)
:删除某个键,返回true
。如果删除失败,返回false
。 -
Map.prototype.clear()
:清除所有成员,没有返回值。
四个遍历方法:
-
Map.prototype.keys()
:返回键名遍历器 -
Map.prototype.values()
:返回键值遍历器 -
Map.prototype.entries()
:返回键值对遍历器 -
Map.prototype.forEach()
:使用回调函数遍历每个成员
实例1:扩展对象
当我们有一系列对象,想记录每个对象一种属性。假设有100只鸡,需要记录每只鸡的重量,有两种思路:
- 想办法用笔写到鸡身上
- 记录到一个本本上
class Chicken {
}
// 100只鸡
let chickenList = []
for (let i = 0; i < 100; i++) {
chickenList.push(new Chicken())
}
// 方法1:记录到鸡身上
chickenList.forEach(function(chicken, index){
chicken.weight = getWeight(chicken);
});
// 方法2:记录到本本上
let notebook = [];
chickenList.forEach(function(chicken, index){
notebook[index] = getWeight(chicken);
});
第1种思路存在以下问题:
- 破坏了鸡的卖相,有时候这是很严重的事情,比如你想把一只5斤的鸡当成6斤卖出去,结果鸡身上直接写“我只有5斤”(修改了原有对象,可能导致意外的行为)
- 可能碰到一些战斗鸡,一个字都写不上去(对象冻结了或者有不可覆盖的属性)
- 可能写到一些本来就写了字的地方,导致根本看不清(与对象原有属性冲突)
再看第2种方法,存在以下问题:
- 本本无法和鸡精准地一一对应,只能靠一些索引或者标记(例如给每只鸡起一个名字)去(不可靠)地记录对应关系(无法精准地对比到是哪一个对象)
这时候就可以使用Map
扩展对象
// 记录到另一个本本上
let notebook = new Map();
chickenList.forEach(function(chicken, index){
notebook.set(chicken, getWeight(chicken));
});
实例2:完善私有属性的实现
回顾之前的Symbol
实现的私有属性的版本里,仍然存在着可以被特殊api遍历的缺陷。
基于Map
的解决思路:
用一个闭包内的Map
来扩展每个生成的对象
var Person = (function() {
var map = new Map();
function Person(name) {
map.set(this,name);
}
Person.prototype.getName = function() {
return map.get(this);
};
return Person;
}());
WeakMap
与之前介绍的WeakSet
类似,WeakMap
与 Map
有两个区别。
-
WeakMap
的键只能是对象,而不能是其他类型的值。 -
WeakMap
中对键的引用是弱引用
同样地,WeakMap
不能遍历,是因为成员都是弱引用,随时可能消失。
WeakMap
只有四个方法可用:get()
、set()
、has()
、delete()
。
注意:WeakMap
弱引用的只是键名,而不是键值。键值依然是正常引用。
const wm = new WeakMap();
let key = {};
let obj = {foo: 1};
wm.set(key, obj);
obj = null;
wm.get(key)
实例:完善私有属性的实现
前面基于Map
的实现还存在一个问题:
当Person
实例的外部引用消除时,闭包中的Map
仍然有Person
实例作为键的引用,Person
实例不会被垃圾回收,必须等到所有的Person
实例的外部引用消除,Map
所在的闭包也会消除,最后Person
实例才会被垃圾回收
为了解决这个问题,使用WeakMap
进一步完善:
var Person = (function() {
var wm = new WeakMap();
function Person(name) {
wm.set(this,name);
}
Person.prototype.getName = function() {
return wm.get(this);
};
return Person;
}());
Proxy
在ES6之前Object.defineProperty
可以拦截对象属性的读取和修改操作,Proxy 可以理解成比这个API更强大的,在目标对象之前架设一层的“拦截”。外界对该Proxy对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。Proxy 这个词的原意是代理,用在这里表示由它来“代理”某些操作,可以译为“代理器”。
注意:只有对生成的 Proxy 实例操作才能起到拦截的作用
生成Proxy实例:
var proxy = new Proxy(target, handler);
- target :需要代理的对象
- handler :拦截函数的集合
如果handler是空对象则代表任何操作都不会拦截
let obj = {}
/*handler为空对象*/
let proxy = new Proxy(obj, {});
proxy.a = 1
//obj.a //1
对属性的读取进行拦截:
var proxy = new Proxy({}, {
get: function(target, property) {
return 35;
}
});
proxy.time // 35
proxy.name // 35
proxy.title // 35
下面是 Proxy 支持的拦截操作一览,一共 13 种。
- get(target, propKey, receiver):拦截对象属性的读取,比如
proxy.foo
和proxy['foo']
。 - set(target, propKey, value, receiver):拦截对象属性的设置,比如
proxy.foo = v
或proxy['foo'] = v
,返回一个布尔值。 - has(target, propKey):拦截
propKey in proxy
的操作,返回一个布尔值。 - deleteProperty(target, propKey):拦截
delete proxy[propKey]
的操作,返回一个布尔值。 - ownKeys(target):拦截
Object.getOwnPropertyNames(proxy)
、Object.getOwnPropertySymbols(proxy)
、Object.keys(proxy)
、for...in
循环,返回一个数组。该方法返回目标对象所有自身的属性的属性名,而Object.keys()
的返回结果仅包括目标对象自身的可遍历属性。 - getOwnPropertyDescriptor(target, propKey):拦截
Object.getOwnPropertyDescriptor(proxy, propKey)
,返回属性的描述对象。 - defineProperty(target, propKey, propDesc):拦截
Object.defineProperty(proxy, propKey, propDesc)
、Object.defineProperties(proxy, propDescs)
,返回一个布尔值。 - getPrototypeOf(target):拦截
Object.getPrototypeOf(proxy)
,返回一个对象。 - setPrototypeOf(target, proto):拦截
Object.setPrototypeOf(proxy, proto)
,返回一个布尔值。如果目标对象是函数,那么还有两种额外操作可以拦截。 - apply(target, object, args):拦截 Proxy 实例作为函数调用的操作,比如
proxy(...args)
、proxy.call(object, ...args)
、proxy.apply(...)
。 - construct(target, args):拦截 Proxy 实例作为构造函数调用的操作,比如
new proxy(...args)
。 - isExtensible(target):拦截
Object.isExtensible(proxy)
,返回一个布尔值。 - preventExtensions(target):拦截
Object.preventExtensions(proxy)
,返回一个布尔值。
Proxy给了开发者拦截语言默认行为的权限,可以不改变原有对象或函数的情况下,轻松运用在很多场景。例如:统计函数调用次数,实现响应式数据观测(Vue 3.0),实现不可变数据(Immutable)等等
Reflect
Reflect
是 ES6 为了操作对象而提供的新 API。ES6把原先版本中很多语言层面的API,比如Object.defineProperty
delete
in
等集中在了Reflect
的静态方法上,引入Reflect
的目的有这样几个。
(1) 将Object
对象的一些明显属于语言内部的方法(比如Object.defineProperty
),放到Reflect
对象上。现阶段,某些方法同时在Object
和Reflect
对象上部署,未来的新方法将只部署在Reflect
对象上。也就是说,从Reflect
对象上可以拿到语言内部的方法。
(2) 修改某些Object
方法的返回结果,让其变得更合理。
// 老写法
try {
Object.defineProperty(target, property, attributes);
// success
} catch (e) {
// failure
}
// 新写法
if (Reflect.defineProperty(target, property, attributes)) {
// success
} else {
// failure
}
(3)将命令式操作转变为函数调用,避免更多的保留字占用。比如name in obj
和delete obj[name]
,对应Reflect.has(obj, name)
和Reflect.deleteProperty(obj, name)
// 老写法
'assign' in Object // true
// 新写法
Reflect.has(Object, 'assign') // true
(4)Reflect
对象的方法与Proxy
对象的方法一一对应,想要调用默认行为,直接在Reflect
上调用同名方法,简单可靠,省去人工写默认行为的代码。
let proxy = new Proxy({}, {
set: function(target, name, value, receiver) {
var success = Reflect.set(target, name, value, receiver);
if (success) {
console.log('property ' + name + ' on ' + target + ' set to ' + value);
}
return success;
}
});
Reflect
对象一共有 13 个静态方法。
- Reflect.apply(target, thisArg, args)
- Reflect.construct(target, args)
- Reflect.get(target, name, receiver)
- Reflect.set(target, name, value, receiver)
- Reflect.defineProperty(target, name, desc)
- Reflect.deleteProperty(target, name)
- Reflect.has(target, name)
- Reflect.ownKeys(target)
- Reflect.isExtensible(target)
- Reflect.preventExtensions(target)
- Reflect.getOwnPropertyDescriptor(target, name)
- Reflect.getPrototypeOf(target)
- Reflect.setPrototypeOf(target, prototype)
上面这些方法的作用,与Proxy
对象handler
的方法是一一对应的。
Iterator
Iterator
(遍历器、迭代器) 是一个对象,Iterator
对象需要包含一个next
方法,该方法返回一个对象,此对象有两个属性,一个value
表示当前结果,一个done
表示是否可以继续迭代
let it = makeIterator();
function makeIterator() {
let nextIndex = 0;
return {
next: function() {
return nextIndex < 5 ?
{value: nextIndex++, done: false} :
{value: undefined, done: true};
}
};
}
ES6 规定,如果数据结构的Symbol.iterator
属性是一个方法,该方法返回Iterator
对象,就可以认为此数据结构是“可遍历的”(iterable)
interface Iterable {
[Symbol.iterator]() : Iterator,
}
interface Iterator {
next(value?: any) : IterationResult,
}
interface IterationResult {
value: any,
done: boolean,
}
实例:
let obj = {
[Symbol.iterator]:makeIterator
}
ES6中以下场合会默认调用 Iterator
接口(即Symbol.iterator
方法),
-
for...of
循环 - 数组解构
- 扩展运算符
yield*
- 其他隐式调用的地方,例如
new Set(['a','b'])
,Promise.all()
等
ES6中以下数据结构默认为可遍历对象,即默认部署了Symbol.iterator
属性
- Array
- Map
- Set
- String
- 函数的 arguments 对象
- NodeList 对象
Generator函数
基本概念
Generator(生成器) 函数是 ES6 提供的一种异步编程解决方案,并且Generator函数的行为与传统函数完全不同。
定义Generator函数
function* f() {
}
形式上,Generator 函数是一个普通函数,但是有两个特征。一是,function
关键字与函数名之间有一个星号;二是,函数体内部可以使用yield
关键字,定义不同的内部状态(yield
在英语里的意思就是“产出”)。
执行Generator函数
执行 Generator 函数,函数本身不会执行,而是会返回一个遍历器对象
,同时该对象也是可遍历的
,因为在其原型链上也具有Symbol.iterator
方法,并且改方法返回的对象就是该遍历器对象自身
function* f() {
console.log(1)
}
let a = f()
a[Symbol.iterator]() === a // true
因此,Generator函数返回的对象也可以被遍历,相当于每次调用此对象next()
的value
来作为遍历结果
只有执行了该遍历器对象的next()
方法,Generator函数才会执行:
function* f() {
console.log(1)
}
let a = f()
a.next() // 打印1 返回{value:undefined,done:true}
yield 和 yield*
Generator函数中可以使用yield
关键字来定义函数返回的遍历器对象每次next()
后的value
function* f() {
yield 1
}
let a = f()
a.next() // 返回 {value: 1, done: false}
并且a每次执行next()
,都会在下一个yield
处暂停,直到后面没有yield
关键字,则执行剩余代码,并且返回done:true
:
function* f() {
console.log('step1')
yield 1
console.log('step2');
yield 2
console.log('step3');
}
let a = f()
a.next() // 打印step1 返回 {value: 1, done: false}
a.next() // 打印step2 返回 {value: 2, done: false}
a.next() // 打印step3 返回 {value: undefined, done: true}
yield
本身没有返回值,yield
的返回值是下一次next()
函数传入的值。
所以next()
方法的作用有两个
- 执行本次
yield
到下一个yield
之间的代码 - 将形参的值传给本次
yield
的返回值
next()
和yield
实现了函数内外控制权的转移。
function* f() {
console.log('start');
let result = yield 1
console.log('result:',result);
}
let a = f()
yield*
等同于遍历某个对象,并且yield
每个结果
function* f() {
yield 'start'
yield* [1, 2, 3]
/*等同于*/
// for(let value of [1,2,3]){
// yield value
// }
yield 'end'
}
let a = f()
a.next() // 返回 {value: 'start', done: false}
a.next() // 返回 {value: 1, done: false}
a.next() // 返回 {value: 2, done: false}
a.next() // 返回 {value: 3, done: false}
a.next() // 返回 {value: 'end', done: false}
a.next() // 返回 {value: undefined, done: true}
Generator函数配合自动执行器
直接循环存在的问题
Generator函数是一种新的异步编程解决方案,但是每次手动调用next()
很麻烦,如果我们写一个循环来执行next()
呢?
function* f() {
yield 1
console.log('完成1');
yield 2
console.log('完成2');
}
let it = f()
let done = false
while (!done){
done = it.next().done
}
看似是没有问题,但是如果yield
后面本身就是一个异步操作,就会有问题
function* f() {
yield readFile(file1)
console.log('耶,完成了1');
yield readFile(file2)
console.log('耶,完成了2');
}
let it = f()
let done = false
while (!done){
done = it.next().done
}
//耶,完成了1
//耶,完成了2
如果我们的需求是让异步操作执行完毕后再执行yield
后面的代码,那么上述执行顺序就不符合需求。验证:
function* f() {
yield readFile(file1,function (err,data) {
console.log('读取到数据1:' + data)
})
console.log('耶,完成了1');
yield readFile(file2,function (err,data) {
console.log('读取到数据2:' + data)
})
console.log('耶,完成了2');
}
let it = f()
let done = false
while (!done){
done = it.next().done
}
//耶,完成了1
//耶,完成了2
//读取到数据1:111
//读取到数据2:222
Thunk函数
在 JavaScript 语言中,Thunk 函数是指将多参数函数,将其替换成一个只接受回调函数作为参数的单参数函数。
// Thunk版本的readFile(单参数版本)
const {readFile} = require('fs')
const path = require('path')
const file1 = path.join(__dirname,'./text/1.txt')
const file2 = path.join(__dirname,'./text/2.txt')
let Thunk = function (fileName) {
return function (callback) {
return readFile(fileName, callback);
};
};
let readFileThunk = Thunk(file1);
readFileThunk(function(err,data){
console.log(String(data));
});
有一个
thunkify
库可以方便的将api
变成Thunk
函数
自动执行器
写一个自动执行器run
函数,每次将it.next()
的逻辑封装到nextStep()
中,并且将nextStep
作为回调函数传给Thunk化后的读取文件函数。
// Thunk版本的readFile(单参数版本)
const {readFile} = require('fs')
const path = require('path')
const file1 = path.join(__dirname, './text/1.txt')
const file2 = path.join(__dirname, './text/2.txt')
let Thunk = function (fileName) {
return function (callback) { //result.value
return readFile(fileName, callback);
};
};
function* f() {
let data1 = yield Thunk(file1)
console.log('耶,完成了1,数据是' + data1);
let data2 = yield Thunk(file2)
console.log('耶,完成了2,数据是' + data2);
}
function run(f) {
let it = f();
function nextStep(err, data) {
var result = it.next(data);
if (result.done) return;
result.value(nextStep); //执行readFile,并且把nextStep作为回调传入
}
nextStep();
}
run(f)
如此一来,基于自动执行器,只要异步操作是Thunk函数或者返回Promise的情况下,写异步逻辑在形式上就如同写同步逻辑一样,非常简洁。
co模块
co模块是对一个封装的更好的自动执行器,它支持yield
的类型,不光包含thunk函数,还有Promise对象,数组,对象,甚至Generator函数
const { promisify } = require("util");
const path = require('path')
const file1 = path.join(__dirname, './text/1.txt')
const file2 = path.join(__dirname, './text/2.txt')
const readFileP = promisify(readFile)
let Thunk = function (fileName) {
return function (callback) { //result.value
return readFile(fileName, callback);
};
};
/*Thunk*/
function* f() {
let data1 = yield Thunk(file1)
console.log('耶,完成了1,数据是' + data1);
let data2 = yield Thunk(file2)
console.log('耶,完成了2,数据是' + data2);
}
/*Promise*/
function* f() {
let data1 = yield readFileP(file1)
console.log('耶,完成了1,数据是' + data1);
let data2 = yield readFileP(file2)
console.log('耶,完成了2,数据是' + data2);
}
/*数组(并发)*/
function* f() {
let data = yield [readFileP(file1),readFileP(file2)]
console.log('耶,完成了,数据是' + data);
}
/*对象(并发)*/
function* f() {
let data = yield {data1:readFileP(file1),data2:readFileP(file2)}
console.log('耶,完成了,数据是' + JSON.stringify(data));
}
/*Generator函数*/
function* f() {
function* f1() {
return yield {data1:readFileP(file1),data2:readFileP(file2)}
}
let data = yield f1()
console.log('耶,完成了,数据是' + JSON.stringify(data));
}
co(f)
经过一个co模块执行后的Generator函数会返回一个Promise对象:
co(f).then(()=>{
console.log('co执行完毕');
})
async函数
基本概念
async
函数是什么?一句话,它就是 Generator 函数的语法糖。
将上一章的代码改成 async 函数的版本:
const { promisify } = require("util");
const path = require('path')
const file1 = path.join(__dirname, './text/1.txt')
const file2 = path.join(__dirname, './text/2.txt')
const readFileP = promisify(readFile)
function* f() {
let data1 = yield readFileP(file1)
console.log('耶,完成了1,数据是' + data1);
let data2 = yield readFileP(file2)
console.log('耶,完成了2,数据是' + data2);
}
//async函数的版本
async function f() {
let data1 = await readFileP(file1)
console.log('耶,完成了1,数据是' + data1);
let data2 = await readFileP(file2)
console.log('耶,完成了2,数据是' + data2);
}
比较后就会发现,async
函数的版本就是将 Generator 函数的星号(*
)替换成async
,将yield
替换成await
。
定义async函数
使用async关键字定义一个async函数:
async function f() {
let data1 = await readFileP(file1)
console.log('耶,完成了1,数据是' + data1);
let data2 = await readFileP(file2)
console.log('耶,完成了2,数据是' + data2);
}
执行async函数
执行async
函数则相当于执行了一个自动运行的Generator函数,async
函数如果返回的结果不是Promise,则会运行结果包装成一个Promise返回:
async function f() {
console.log(1);
}
f().then(()=>{
console.log(2);
})
async function f() {
console.log(1);
return 'done'
}
f().then(value => {
console.log(value);
})
await关键字
与yield
类似,async
函数中可以使用await
关键字,await
关键字后面一般会写一个Promise实例,async
函数执行的过程中,每次遇到await
关键字,会将控制权转回外部环境。
- 如果
await
后面是Promise实例,则会等到该 Promise实例被resolve后,才会把本次await
到下次await
之间的代码推到MircoTask(微任务)
中等待执行,并且await
的返回值是该Promise实例resolve的值 - 如果
await
后面不是Promise实例,则会立即将本次await
到下次await
之间的代码推到MircoTask(微任务)
中等待执行,并且await
的返回值是等于await
后面表达式的值:
async function f() {
let data = await new Promise((resolve, reject) => {
setTimeout(() => {
resolve('a')
}, 2000)
})
console.log(data);
}
//f()
//console.log('end')
如果await
后面不是Promise 实例
async function f() {
let data = await 'a'
console.log(data);
}
f()
console.log('end');
//end
//a
async函数的错误处理
如果Promise被reject或抛出错误,await之后的代码不会执行,因此,需要使用try..catch
对await
进行错误捕捉:
async function f() {
try {
let data = await new Promise((resolve, reject) => {
setTimeout(() => {
reject('123')
}, 2000)
})
//后续代码无法执行
console.log('done');
}catch (e) {
console.log('发生错误:',e);
}
}
f()
async函数处理并发异步任务
如果,async
函数中的每个await
都是等到前面await
resolve后才会执行,如果想并发执行,可以使用Promise.all
:
/*并发处理异步*/
async function f() {
let time1 = new Date()
let [data1,data2] = await Promise.all([
new Promise((resolve, reject) => {
setTimeout(() => {
resolve('123')
}, 2000)
}),
new Promise((resolve, reject) => {
setTimeout(() => {
resolve('123')
}, 3000)
})
])
console.log(data1,data2,'用时:'+ (new Date() - time1));
}
f()
async函数与Promise的对比
用async
函数写异步逻辑相比Promise会更加简洁,在处理不同异步结果相互依赖,错误处理,if…else分支等情况时更加简便:
const {readFile} = require('fs')
const { promisify } = require("util");
const path = require('path')
const file1 = path.join(__dirname, './text/1.txt')
const file2 = path.join(__dirname, './text/2.txt')
const file3 = path.join(__dirname, './text/3.txt')
const readFileP = promisify(readFile)
function f1() {
readFileP(file1).then(data1 =>{
console.log('耶,完成了1,数据是' + data1);
return readFileP(file2)
}).then(data2 => {
console.log('耶,完成了1,数据是' + data2);
return readFileP(file3)
}).then(data3 => {
console.log('耶,完成了1,数据是' + data3);
})
}
async function f2() {
let data1 = await readFileP(file1)
console.log('耶,完成了1,数据是' + data1);
let data2 = await readFileP(file2)
console.log('耶,完成了2,数据是' + data1 + data2);
let data3 = await readFileP(file3)
console.log('耶,完成了2,数据是' + data1 + data2 + data3);
}
f()