深入学习ES6
1. Symbol
1.1 概述
ES6 引入了一种新的原始数据类型 symbol
,表示独一无二的值。它是JavaScript语言的第七种数据类型,前六种是:undefined
、null
、Boolean
、Number
、String
、Object
。
直接调用Symbol
函数即可生成一个Symbol,注意:Symbol
函数前不能使用new
命令,否则会报错。
Symbol
函数可以接受一个字符串作为参数,表示对Symbol的描述,主要是为了在控制台显示,或者转为字符串时比较容易区分。
// 1.有参数,参数不同
let s1 = Symbol('foo');
let s2 = Symbol('bar');
console.log(s1) // Symbol(foo)
console.log(s2) // Symbol(bar)
console.log(s1===s2) // false
// 2.有参数,参数相同
let s1 = Symbol('foo');
let s2 = Symbol('foo');
console.log(s1) // Symbol(foo)
console.log(s2) // Symbol(foo)
console.log(s1===s2) // false
// 3.无参数
let s1 = Symbol();
let s2 = Symbol();
console.log(s1===s2) // false
注意:Symbol
函数的参数只是表示对当前Symbol值的描述,因此相同参数的Symbol函数的返回值是不相等的。
1.2 Symbol作为属性
Symbol值可以作为标识符,用作对象的属性名,由于每个Symbol值都是不相等的,这就意味着能保证不会出现同名的属性,能够避免某个键被不小心改写或覆盖的情况。
**注意:**在对象内部,使用Symbol值定义属性时,Symbol值必须放在方括号之中。
let mySymbol = Symbol();
// 1.第一种写法
let a = {};
a[mySymbol] = 'Hello!'
// 2.第二种写法
let a = {
[mySymbol]: 'Hello!'
};
// 3.第三种写法
let a = {};
object.defineProperty(a, mySmbol, {value: 'Hello!'});
// 以上写法都得到同样结果
console.log(a[mySymbol]) // 'Hello!'
Symbol作为属性名,该属性不会出现在for...in
、for...of
循环中,也不会被Object.keys()
、Object.getOwnPropertyNames()
、JSON.stringify()
返回;但是Object.getOwnPropertySmybols()
方法可以获取指定对象的所有Symbol属性名。
1.3 Symbol.for
有时,我们希望重新使用同一个Symbol值,Symbol.for
方法可以做到这一点。它接受一个字符串作为参数,然后搜索有没有以该参数作为名称的Symbol值,如果有,就返回这个Symbol值,否则就新建并返回一个以该字符串为名称的Symbol值。
let s1 = Symbol.for('foo');
let s2 = Symbol.for('foo');
console.log(s1 === s2) // true
let s1 = Symbol('foo');
let s2 = Symbol.for('foo');
console.log(s1 === s2) // false
Symbol.for()
与Symbol()
这两种写法,都会生成新的Symbol;**它们的区别是:**前者会被登记在全局环境中供搜索,后者不会。Symbol.for()
不会每次调用就返回一个新的Symbol类型的值,而是会先检查给定的key
是否已经存在,如果不存在才会新建一个值;比如,如果调用Symbol.for('cat')
30次,每次都会返回同一个Symbol值,但是调用Symbol('cat')
30次,会返回30个不同的Symbol值。
1.4 内置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
属性,指向该对象的默认生成遍历器的方法。
1.5 实例
1.5.1 消除魔术字符串
const shapeOption = {
triangle: Symbol(),
rectangle: Symbol()
}
function getArea(shape, options) {
let atea = 0;
switch(shape) {
case shapeOption.triangle: // 魔术字符串
area = .5 * options.width * options.height;
break;
/* ...more code ...*/
}
return area;
}
getArea(shapeOption.triangle, {width: 100, height: 100}) // 魔术字符串
上面代码中,字符串Triangle
就是个魔术字符串;它多次出现,与代码形成“强耦合”,不利于将来的修改和维护。
常用的消除魔术字符串方法,就是把它变成一个变量。
1.5.2 实现私有属性
- 第一种方式:用一个字符串或者下划线的方式
var Person = (function() {
let _name = '_name'
// 或者 let name = 'aaa'
function Person(name) {
this[_name] = name;
}
Person.prototype.getName = function() {
return this[_name];
};
return Person;
}());
let a = new Person('zs')
console.log(a) // {_name: 'zs'}
console.log(Object.keys(a)) // ["_name"]
缺点很显然,不是真正的私有,依然可以遍历
- 第二种方式:闭包
var Person = (function() {
function Person(name) {
this.getName = function() {
return name;
};
}
return Person;
}());
let a = new Person('zs')
console.log(a) // 遍历不到
console.log(Object.keys(a)) // 遍历不到
console.log(a.getName()) // zs
实现了私有,但是仍然存在缺点:实例无法共享方法,浪费内存空间
第三种方式:使用Symbol
var Person = (function() {
var nameSymbol = Symbol('name');
function Person(name) {
this[nameSymbol] = name;
}
Person.prototype.getName = function() {
return this[nameSymbol];
};
return Person;
}());
let a = new Person('zs')
console.log(a) // {Symbol(name): 'zs'}
console.log(Object.keys(a)) // 遍历不到
console.log(a.getName()) // zs
// 直接修改原值也只是在对象中新增属性值
缺陷:仍然会被Object.getOwnPropertySymbols
获取到属性,进而修改该属性对应的值
2. Set和WeakSet
2.1 Set
ES6提供了新的数据结构Set
,它类似于数组,但是成员的值都是唯一的,没有重复的值。需要记录不同成员又不希望重复记录的情况下可以用到Set
Set实例的属性:
Set.prototype.size
:返回Set
实例的成员总数。
let a = new Set()
// let a = new Set([1, 2, 3]) 也可传入数组
console.log(a.size) // 0
a.add(1)
console.log(a.size) // 1
a.add(1)
console.log(a.size) // 1
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()
:使用回调函数遍历每个成员
let a = new Set()
a.add(1).add(2)
// 1.keys()
for(let key of a.keys()){
console.log(key) // 1 2
}
// 2.values()
for(let value of a.values()){
console.log(value) // 1 2
}
// 3.entries()
for(let entry of a.entries()){
console.log(entry) // [1, 1] [2, 2]
}
// 4.forEach()
a.forEach((key, value) => {
console.log(key, value) // 1 1 2 2
})
注意:Set实例中key和value是一样的,所以keys()
和values()
这两个方法的结果是一样的
Set中查找某个值是否已经存在的时间复杂度是O(1),而数组的indexOf方法时间复杂度是O(n),又由于Set中值不会重复,所以可以使用Set做数组去重:
// 使用indexOf缺点:时间复杂度O(n^2)性能低下,NaN要做特殊处理
function deduplicatel(arr) {
let temp = []
for(let i=0; i<arr.length; i++) {
if(temp.indexOf(arr[i]) === -1) {
temp.push(arr[i])
}
}
return temp
}
// 使用对象解决性能问题,时间复杂度O(n),但是数组里不能有对象,也无法区分字符串和数字
function deduplicatel2(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 deduplicatel3(arr) {
let temp = [...(new Set(arr))]
return temp
}
2.2 WeakSet
WeakSet结构与Set类似,也是不重复的值得集合;但是它与Set有两个区别:
- WeakSet的成员只能是对象,而不能是其他类型的值。
- WeakSet中的对象都是弱引用。
如果一个对象没有任何引用,name此对象会尽快被垃圾回收,释放掉它占用的内存。
即垃圾回收机制不考虑WeakSet对该对象的引用,也就是说,如果其他对象都不再引用该对象,那么垃圾回收机制会自动回收该对象所占用的内存,不考虑该对象还存在于WeakSet之中。
WeakSet结构有以下三个方法:
WeakSet.prototype.add(value)
:向WeakSet实例中添加一个成员。WeakSet.prototype.detele(value)
:清除WeakSet实例的指定成员。WeakSet.prototype.has(value)
:返回一个布尔值,表示某个值是否在WeakSet实例之中。
WeakSet不能遍历,是因为成员都是弱引用,随时可能消失。
// Set示例
let div = document.querySelector('div')
let set = new Set()
set.add(div)
// ...some code
document.body.removeChild(div)
div = null // dom对象仍在内存中,因为Set中仍然引用此对象
// WeakSet示例
let div = document.querySelector('div')
let weakset = new WeakSet()
weakset.add(div)
// ...some code
document.body.removeChild(div)
div = null // dom对象已经没有引用,将被垃圾回收机制回收
3. Map和WaekMap
3.1 Map
JavaScript的对象(Object),本质上是键值对的集合(Hash结构),但是传统上只能用字符串当做键,这给它的使用带来了很大的限制。
为了解决这个问题,ES6提供了Map数据结构,它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键,是一种更完善的Hash结构实现。
Map实例的属性:
Map.prototype.size
:返回Map
实例的成员总数。
// 初始化Map实例
const map1 = new Map()
const map2 = new Map([
['name', '张三'],
['age', 18]
])
// Size
let a = new Map()
console.log(a.size) // 0
a.set('name', 'zs')
console.log(a.size) // 1
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()
:使用回调函数遍历每个成员
let a = new Map([
['name', 'zs'],
['age', 18]
])
// 1.keys()
for(let key of a.keys()) {
console.log(key) // name age
}
// 2.values()
for(let value of a.values()) {
console.log(value) // zs 18
}
// 3.entries()
for(let entry of a.entries()) {
console.log(entry) // ['name', 'zs'] ['age', 18]
}
// 4.forEach()
a.forEach((key, value) => {
console.log(key, value) // zs name 18 age
})
3.1.1 实例1:扩展对象
当我们有一系列对象,想记录每个对象一种属性;假设有100只鸡,需要记录每只鸡的重量,有两种思路:
- 1.想办法用笔写到鸡身上
- 2.记录到一个本子上
function getWeight(chicken) {
return Math.random()
}
class Chicken {}
// 100只鸡
let chickenList = []
for(let i=0; i<100; i++) {
chickenList.push(new Chicken())
}
// 方法1:记录到鸡身上
chickenList.forEach((chicken, index) => {
chicken.weight = getWeight(chicken)
})
// 方法2:记录到本子上
let notebook = []
chickenList.forEach((chicken, index) => {
notebook[index] = getWeight(chicken)
})
第1种思路存在以下问题:
- 1.破坏了鸡的卖相,有时候这是很严重的问题,比如你想把一只5斤的鸡当成6斤卖出去,结果鸡身上直接写“我只有5斤”(修改了原有对象,可能导致意外的行为)
- 2.可能碰到一些战斗机,一个字都写不上去(对象冻结了或者有不可覆盖的属性)
- 3.可能写到一些本来就写了字的地方,导致根本看不清(与对象原有属性冲突)
第2种思路存在以下问题:
- 1.本子无法和鸡精准地一一对应,只能靠一些索引或者标记(id)(例如给每只鸡起一个名字)去(不可靠)地记录对应关系(无法精准地对比到底是哪一个对象)
此时就可以使用Map
扩展对象
let notebook = new Map()
chickenList.forEach((chicken, index) => {
notebook.set(chicken, getWeight(chicken))
})
3.1.2 实例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 Preson
}())
var a = new Person('zs')
console.log(a) // 获取不到值
console.log(a.getName()) // zs
3.2 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) // {foo: 1} 不会被垃圾回收
3.2.1 实例:完善私有属性的实现
前面基于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.getNmae = function() {
return wm.get(this)
}
return Person
}())
let zs = new Person('zs')
let ls = new Person('ls')
zs = null
4. Proxy
在ES6之前Object.defineProperty
可以拦截对象属性的读取和修改操作,Proxy可以理解成比这个API更强大的,在目标对象之前架设一层“拦截”;外界对该Proxy对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。Proxy这个词的愿意是代理,用在这里表示由它来“代理”某些操作,可以译为“代理器”。
注意:只有对生成的Proxy实例操作才能起到拦截作用。
// 生成Proxy实例 target:需要代理的对象,handler:拦截函数的集合
var proxy = new Proxy(target, handler)
如果handler是空对象则代表任何操作都不会拦截
let obj = {}
let proxy = new Proxy(obj, {})
proxy.a = 1
console.log(obj.a) // 1
对属性的读取进行拦截:
var proxy = new Proxy({}, {
get(target, key, receiver) {
return 18
}
})
console.log(proxy.a) // 18
console.log(proxy.b) // 18
console.log(proxy.c) // 18
下面是Proxy支持的拦截操作,共13种:
- get(target, propKey, receiver):拦截对象属性的读取,比如
Proxy.foo
和Proxy['foo']
。 - set(target, propKey, value, receiver):拦截对象属性的设置,比如
Proxy.foo = v
或Proxy['foo'] = v
,返回一个布尔值。 - has(target, propKey):拦截
Proxy 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给了开发者拦截语言默认行为的权限,可以不改变原有对象或函数的情况下,轻松运用在很多场景。例如:统计函数调用次数、实现响应式数据观测(vue3.0)、实现不可变数据(Immutable)等等。
5. 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(err) {
// 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(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个静态方法:
- get(target, name, receiver)
- set(target, name, value, receiver)
- has(target, name)
- deleteProperty(target, name)
- ownKeys(target)
- getOwnPropertyDescriptor(target, name)
- defineProperty(target, name, desc)
- getPrototypeOf(target)
- setPrototypeOf(target, proto)
- apply(target, object, args)
- construct(target, args)
- isExtensible(target)
- preventExtensions(target)
上面这些方法的作用,与Proxy
对象handler
的方法是一一对应的。
6. Iterator
Itertor
(遍历器、迭代器)是一个对象,Iterator
对象需要包含一个next
方法,该方法返回一个对象,此对象有两个属性,一个value
表示当前结果,一个done
表示是否可以继续迭代。
let it = makeIterator()
function makeIterator() { // 返回一个遍历器对象
let nextIndex = 0
return {
next() {
return nextIndex < 3 ?
{value: nextIndex++, done: false} :
{value: undefined, done: true}
}
}
}
console.log(it.next()) // {value: 0, done: false}
console.log(it.next()) // {value: 1, done: false}
console.log(it.next()) // {value: 2, done: false}
console.log(it.next()) // {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
}
// 实例
function makeIterator() { // 返回一个遍历器对象
let nextIndex = 0
return {
next() {
return nextIndex < 3 ?
{value: nextIndex++, done: false} :
{value: undefined, done: true}
}
}
}
let obj = {[Symbol.iterator]: makeIterator}
let a = obj[Symbol.iterator] // makeIterator函数
let it = a()
console.log(it.next()) // {value: 0, done: false}
ES6中以下场合会默认调用Iterator
接口(即Symbol.iterator
方法)
for...of
循环- 数组解构
- 扩展运算符
yield*
- 其他隐式调用的地方,例如
new Set(['a', 'b'])
,Promise.all()
等
function makeIterator() { // 返回一个遍历器对象
let nextIndex = 0
return {
next() {
return nextIndex < 5 ?
{value: nextIndex++, done: false} :
{value: undefined, done: true}
}
}
}
let obj = {[Symbol.iterator]: makeIterator}
// 1.for...of循环
for(let value of obj) {
console.log(value) // 依次打印 0 1 2 3 4
}
// 2.数组解构
let arr = []
arr[Symbol.iterator] = makeIterator
let [x, y] = arr // 遍历器运行了2遍
console.log(x, y) // 0 1
// 3.扩展运算符
let arr = []
arr[Symbol.iterator] = makeIterator
let a = [...arr] // 遍历器运行了6遍
console.log(a) // [0, 1, 2, 3, 4]
// 4.yield*
function* f() {
yield* arr
}
for(let value of f()) {
console.log(value) // 依次打印 0 1 2 3 4
}
// 5.隐式调用
let arr = []
arr[Symbol.iterator] = makeIterator
let a = new Set(arr) // 遍历器运行了6遍
console.log(a) // {0, 1, 2, 3, 4}
ES6中以下数据结构默认为可遍历对象,即默认部署了Symbol.iterator
属性
- Array
- Map
- Set
- String
- 函数的argument对象
- NodeList对象
7. Generator函数
7.1 基本概念
Generator(生成器)函数是ES6提供的一种异步编程解决方案,并且Generator函数的行为与传统函数完全不同
7.1.1 生成Generator函数
function* f() {}
形式上,Generator函数是一个普通函数,但是有两个特征:
function
关键字与函数名之间有一个*
- 函数体内部可以使用
yield
关键字,定义不同的内部状态(yield
在英语里的意思就是“产出”)
7.1.2 执行Generator函数
执行Generator函数,函数本身不会执行,而是会返回一个遍历器对象,同时该对象也是可遍历的,因为在其原型链上也具有Symbol.iterator
方法,而且该方法返回的对象就是该遍历器对象自身
function* f() {
console.log(1)
}
let a = f()
console.log(a[Symbol.iterator]() === a) // true
因此,Generator函数返回的对象也可以被遍历,相当于每次调用此对象next()
的value
来作为遍历结果
只有执行了该遍历器对象的next()
方法,Generator函数才会执行
function* f() {
console.log(1)
}
let a = f()
console.log(a.next()) // 依次打印 1 {value: undefined, done: true}
7.1.3 yield和yield*
Generator函数中可以使用yield
关键字来定义函数返回的遍历器对象每次next()
后的value
// yield后的表达式表示next返回的value
function* f() {
yield 1
}
let a = f()
console.log(a.next()) // {value: 1, done: false}
并且a每次执行next()
,都会在下一个yield
出暂停,直到后面没有yield
关键字,则执行剩余代码,并且返回done: true
// yield暂停函数
function* f() {
console.log('step1')
yield 1
console.log('step2')
yield 2
console.log('step3')
}
let a = f()
console.log(a.next()) // 依次打印 step1 {value: 1, done: false}
console.log(a.next()) // 依次打印 step2 {value: 2, done: false}
console.log(a.next()) // 依次打印 step3 {value: undefined, done: true}
yield
本身没有返回值,yield
的返回值是下一次next()
函数传入的值
所以next()
方法的作用有两个:
- 1.执行本次
yield
到下一个yield
之间的代码 - 2.将形参的值传给本次
yield
的返回值
next()
和yield
实现了函数内外控制权的转移
function* f() {
console.log('start')
let result = yield 1
console.log('result:', result)
}
let a = f()
console.log(a.next()) // 依次打印 start {value: 1, done: false}
console.log(a.next()) // 依次打印 result: undefined {value: undefined, done: true}
yield*
等同于遍历某个对象,并且yield
每个结果
function* f() {
yield 'start'
yield* [1, 2, 3]
// 等同于
// for(let value of [1, 2, 3]) {
// yield value
// }
yield 'end'
}
let a = f()
console.log(a.next()) // {value: 'start', done: false}
console.log(a.next()) // {value: 1, done: false}
console.log(a.next()) // {value: 2, done: false}
console.log(a.next()) // {value: 3, done: false}
console.log(a.next()) // {value: 'end', done: false}
console.log(a.next()) // {value: undefined, done: true}
7.2 Generator函数配合自动执行器
7.2.1 直接循环存在的问题
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
后面本身就是个一步操作,就会有问题
const {readFile} = require('fs')
const path = require('path')
const file1 = path.join(__dirname, './text/1.txt')
const file2 = path.join(__dirname, './text/2.txt')
// 模拟异步操作
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')
}
// 完成1
// 完成2
// 读取到了数据1:111
// 读取到了数据2:222
7.2.2 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(fileNmae, callback)
}
}
// 两个方法结果一样
// Thunk(file)((err, data)=> {
// console.log(String(data))
// })
let readFileThunk = Thunk(file1)
readFileThunk((err, data)=>{
console.log(String(data))
})
有一个
thunkify
库可以方便地将api
变成Thunk
函数
7.2.3 自动执行器
写一个自动执行器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(fileNmae, callback)
}
}
// 模拟异步操作
function* f() {
let data1 = yield Thunk(file1)
console.log('完成1,读取到了数据1:'+data1)
let data2 = yield Thunk(file2)
console.log('完成2,读取到了数据2:'+data2)
}
// 自动执行器
function run(f) {
let it = f()
function nextStep(err, data) {
var result = it.next(data) // {value: function() {...}, done: false}
if(result.done) return
result.value(nextStep) // 执行readFile,并且把nextStep作为回调传入
}
nextStep()
}
run(f)
// 完成1,读取到了数据1:111
// 完成2,读取到了数据2:222
如此一来,基于自动执行器,只要异步操作是Thunk函数或者返回Promise的情况下,写异步逻辑在形式上就如同写同步逻辑一样,非常简洁。
7.2.4 co模块
co模块是一个封装得更好的自动执行器,它支持yield
的类型,不光包含Thunk函数,还有Promise对象、数组、对象,甚至Generator函数
const co = require('co')
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 readFileP = promisify(readFile)
let Thunk = function(fileName) {
return function(callback) {
return readFile(fileName, callback)
}
}
// 1.Thunk
function* f() {
let data1 = yield Thunk(file1)
console.log('完成1,读取到了数据1:'+data1)
let data2 = yield Thunk(file2)
console.log('完成2,读取到了数据2:'+data2)
}
// 完成1,读取到了数据1:111
// 完成2,读取到了数据2:222
// 2.Promise
function* f() {
let data1 = yield readFileP(file1)
console.log('完成1,读取到了数据1:'+data1)
let data2 = yield readFileP(file2)
console.log('完成2,读取到了数据2:'+data2)
}
// 完成1,读取到了数据1:111
// 完成2,读取到了数据2:222
// 3.数组(并发)
function* f() {
let data = yield [readFileP(file1), readFileP(file2)]
console.log('完成,读取到了数据:'+data)
}
// 完成,读取到了数据:111, 222
// 4.对象(并发)
function* f() {
let data = yield {data1: readFileP(file1), data2: readFileP(file2)}
console.log('完成,读取到了数据:'+JSON.stringify(data))
}
// 完成,读取到了数据:{'data1': 'xxx', 'data2': 'xxx'}
// 5.Generator函数
function* f() {
function* f1() {
return yield {data1: readFileP(file1), data2: readFileP(file2)}
}
let data = yield f1
// 或者 let data = yield f1()
console.log('完成,读取到了数据:'+JSON.stringify(data))
}
// 完成,读取到了数据:{'data1': 'xxx', 'data2': 'xxx'}
co(f)
8. async函数
8.1 基本概念
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)
async function f() {
let data1 = await readFileP(file1)
console.log('完成1,读取到了数据1:'+data1)
let data2 = await readFileP(file2)
console.log('完成2,读取到了数据2:'+data2)
}
f()
// 完成1,读取到了数据1:111
// 完成2,读取到了数据2:222
比较之后就会发现,async
函数的版本就是将Generator函数的*
替换成async
,将yield
替换成await
8.2 定义async函数
使用async关键字定义一个async函数:
async function f() {
let data1 = await readFileP(file1)
console.log('完成1,读取到了数据1:'+data1)
}
8.3 执行async函数
执行async
函数相当于执行了一个自动运行的Generator函数,async
函数如果返回的结果不是Promise,则会将运行结果包装成一个Promise返回:
async function f() {
console.log(1) // 1 含一个Promise对象
}
f().then(()=> {
console.log(2) // 依次打印 1 2
})
async function f() {
console.log(1) // 1
return 'done' // Promise.resolve('done')
}
f().then(value=> {
console.log(value) // done
})
8.4 await关键字
与yield
类似,async
函数中可以使用await
关键字,await
关键字后面一般会写一个Promise实例,async
函数执行过程中,每次遇到await
关键字,都会将控制权转回外部环境
- 1.如果
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')
// end
// a
如果await
后面不是Promise实例
async function f() {
let data = await 'a'
console.log(data)
}
f()
console.log('end')
// end
// a
8.5 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(err) {
console.log('发生错误:', err)
}
}
f() // 发生错误:123
8.6 async函数处理并发异步任务
如果,async
函数中的每个await
都是等到前面await
resolve后才会执行,若想并发执行,可以使用Promise.all
两个异步操作若无相互依赖关系时可并发处理,节约执行时间
// 并发处理异步 若不并发处理,用时大概5s甚至更久
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() // 123 123 用时:3001
8.7 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('完成2,数据是:'+data2)
return readFileP(file3)
}).then(data3 => {
console.log('完成3,数据是:'+data3)
})
}
f1()
// 完成1,数据是:111
// 完成2,数据是:222
// 完成3,数据是:333
async function f2() {
let data1 = await readFileP(file1)
console.log('完成1,数据是:'+data1)
let data2 = await readFileP(file2)
console.log('完成2,数据是:'+data2)
let data3 = await readFileP(file3)
console.log('完成3,数据是:'+data3)
}
f2()
// 完成1,数据是:111
// 完成2,数据是:222
// 完成3,数据是:333