Set & Map
Set
Es6提供的新数据结构Set,类似于数组,但是成员值均唯一,不重复。
set本身就是一个构造函数,用来生成Set数据结构。其中Set函数可以接受一个数组,也可以接受具有iterable的其他数据结构作为参数来实现初始化。
const set = new Set([1, 2, 3, 4, 4]); // 1,2,3,4
set的属性和方法
Set的属性包含size,用于获取当前set的长度;
set的方法包含操作方法和遍历方法两大类
其中,操作方法主要如下:
set2.add(NaN).add(NaN) // size:1
-
delete(value)
:删除某个值,返回一个布尔值,表示删除是否成功。
-
has(value)
:返回一个布尔值,表示该值是否为Set
的成员。
-
clear()
:清除所有成员,没有返回值。
set的遍历方法如下:
let set = new Set(['red', 'green', 'blue']);
for (let item of set.keys()) {
console.log(item);
}
console.log('--------使用values()----------')
for (let item of set.values()) {
console.log(item);
}
console.log('------------------')
for (let item of set.entries()) {
console.log(item);
}
console.log('--------直接使用of----------')
console.log(Set.prototype[Symbol.iterator] === Set.prototype.values)
for (let x of set) {
console.log(x);
}
set的应用
- 由于扩展运算符(
...
)内部使用for...of
循环,所以也可以用于 Set 结构,这里就出现了我们常用的数组去重方法:[...new Set(array)]
同时也可以用于字符串去重:
[...new Set('ababbc')].join('')
let a = new Set([1, 2, 3]);
let b = new Set([4, 3, 2]);
// 并集
let union = new Set([...a, ...b]);
console.log(union)
// 交集
let intersect = new Set([...a].filter(x => b.has(x)));
console.log(intersect)
// (a 相对于 b 的)差集
let difference = new Set([...a].filter(x => !b.has(x)));
console.log(difference)
// 方法一
let set1 = new Set([1, 2, 3]);
set1 = new Set([...set1].map(val => val * 2));
console.log(set1);
// 方法二
let set2 = new Set([1, 2, 3]);
set2 = new Set(Array.from(set2, val => val * 2));
console.log(set2)
WeakSet
weakSet和set类似,主要的区别时:
首先,WeakSet 的成员只能是对象和** Symbol** 值,而不能是其他类型的值;
其次,WeakSet 中的对象都是弱引用,即垃圾回收机制不考虑 WeakSet 对该对象的引用,也就是说,如果其他对象都不再引用该对象,那么垃圾回收机制会自动回收该对象所占用的内存。
这是因为垃圾回收机制根据对象的可达性(reachability)来判断回收,如果对象还能被访问到,垃圾回收机制就不会释放这块内存。结束使用该值之后,有时会忘记取消引用,导致内存无法释放,进而可能会引发内存泄漏。WeakSet 里面的引用,都不计入垃圾回收机制,所以就不存在这个问题。**因此,WeakSet 适合临时存放一组对象,以及存放跟对象绑定的信息。只要这些对象在外部消失,它在 WeakSet 里面的引用就会自动消失。**由于这个特点,WeakSet 的成员是不适合引用的,因为它会随时消失。
另外,由于 WeakSet 内部有多少个成员,取决于垃圾回收机制有没有运行,运行前后很可能成员个数是不一样的,而垃圾回收机制何时运行是不可预测的,因此 ES6 规定 WeakSet 不可遍历。
基本使用
WeakSet 是一个构造函数,可以使用new
命令,创建 WeakSet 数据结构。作为构造函数,WeakSet 可以接受一个数组或类似数组的对象作为参数。该数组的所有成员,都会自动成为 WeakSet 实例对象的成员。
const a = [[1,2],[2,3]]
const ws = new WeakSet(a);
ws.add(1) // 报错
const b = [1,2]
const ws1 = new WeakSet(b); // error
console.log(ws1)
WeakSet 结构有以下三个方法。
-
add(value)
:向 WeakSet 实例添加一个新成员,返回 WeakSet 结构本身。
-
delete(value)
:清除 WeakSet 实例的指定成员,清除成功返回true
,如果在 WeakSet 中找不到该成员或该成员不是对象,返回false
。
-
has(value)
:返回一个布尔值,表示某个值是否在 WeakSet 实例之中。
weakSet的应用
const trackedObjects = new WeakSet();
class MyClass {
constructor() {
trackedObjects.add(this);
}
isTracked() {
return trackedObjects.has(this);
}
}
const obj1 = new MyClass();
console.log(obj1.isTracked()); // true
const obj2 = new MyClass();
trackedObjects.delete(obj2);
console.log(obj2.isTracked()); // false
const foos = new WeakSet()
class Foo {
constructor() {
foos.add(this)
}
method () {
if (!foos.has(this)) {
throw new TypeError('Foo.prototype.method 只能在Foo的实例上调用!');
}
}
}
let f = new Foo()
f.method() // 通过
Foo.prototype.method() // 报错
Map
js的对象(Object) 本质上时键值对的集合,但是传统上只能使用字符串当作键值,这在使用上产生了很多的限制。
<body>
<div id="box">123</div>
<script>
const data = {};
const element = document.getElementById('box');
console.log(element);
data[element] = 'metadata';
console.log("data",data)
console.log(data['[object HTMLDivElement]']) // "metadata"
</script>
</body>

由于对象只接受字符串作为键名,所以element
被自动转为字符串[object HTMLDivElement]
。为了解决这个问题,ES6提供了Map数据结构,与对象的主要区别在于“键”的范围不在局限于字符串,各种类型的值都可以作为键。通俗点说,对象提供了了“字符串-值”的对应,Map提供了“值-值”的对应。还是上面的例子,我们使用Map来实现,效果如下:

map也可以接受数组作为参数,数组的成员是一个个表示键值对的数组,如果对同一个键多次赋值,后面的值将覆盖前面的值,这里我们要注意,只有对同一个对象的引用,才算是同一个键值。如果读取一个未知的键,则返回undefined
。
const map = new Map([
['name', '张三'],
['title', 'Author']
]);
console.log('-----------------------')
const map1 = new Map();
map1.set(1,'aaa').set(1,'bbb');
console.log(map1.get(1)); // "bbb" 会覆盖前面的值
console.log(map1.get(2)); // undefined
console.log('-----------------------')
const map2 = new Map();
const k1 = ['a'];
map2.set(['a'], 555);
map2.set(k1, 666);
console.log(map2.get(['a']) ) // undefined
console.log(map2.get(k1) ) // 666
console.log(map2.size)
console.log('-----------------------')
const map3 = new Map();
map3.set(NaN, 123);
console.log(map3.get(NaN)) // 123
map3.set(NaN, 456);
console.log(map3.get(NaN)) // 456
由上可知,Map 的键实际上是跟内存地址绑定的,只要内存地址不一样,就视为两个键。这就解决了同名属性碰撞(clash)的问题,我们扩展别人的库的时候,如果使用对象作为键名,就不用担心自己的属性与原作者的属性同名。
如果 Map 的键是一个简单类型的值(数字、字符串、布尔值),则只要两个值严格相等,Map 将其视为一个键,比如0
和-0
就是一个键,布尔值true
和字符串true
则是两个不同的键。另外,undefined
和null
也是两个不同的键。虽然**NaN
**不严格相等于自身,但 Map 将其视为同一个键。
map的属性和方法
属性: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.prototype.keys()
:返回键名的遍历器。
-
Map.prototype.values()
:返回键值的遍历器。
-
Map.prototype.entries()
:返回所有成员的遍历器。
-
Map.prototype.forEach()
:遍历 Map 的所有成员。
需要特别注意的是,Map 的遍历顺序就是插入顺序。
与set类似,直接使用for...of,等同于使用map.entries(),表示 Map 结构的默认遍历器接口(Symbol.iterator
属性),就是entries
方法。
map[Symbol.iterator] === map.entries // true
map的应用
-
实现图结构
class Graph {
constructor() {
this.adjacencyList = new Map();
}
addVertex(vertex) {
this.adjacencyList.set(vertex, []);
}
addEdge(vertex1, vertex2) {
this.adjacencyList.get(vertex1).push(vertex2);
this.adjacencyList.get(vertex2).push(vertex1);
}
}
const graph = new Graph();
graph.addVertex("A");
graph.addVertex("B");
graph.addVertex("C");
graph.addEdge("A", "B");
graph.addEdge("A", "C");
console.log(graph.adjacencyList);
-
记录元数据
const metadata = new Map();
const user1 = { id: 1, name: "John" };
const user2 = { id: 2, name: "Jane" };
metadata.set(user1, { role: "admin" });
metadata.set(user2, { role: "user" });
console.log(metadata.get(user1)); // { role: 'admin' }
console.log(metadata.get(user2)); // { role: 'user' }
WeakMap
WeakMap
结构与Map
结构类似,也是用于生成键值对的集合。区别有两点。首先,WeakMap
只接受对象(null
除外)和 Symbol 值作为键名,不接受其他类型的值作为键名。其次,WeakMap的键名所指向的对象,不计入垃圾回收机制。
一般情况下,我们想在一个对象上存放一些数据,会形成对这个对象的引用,一旦不再需要对象就必须手动删除引用,否则垃圾回收机制不会使用占用的内存,造成内存泄漏。WeakMap 就是为了解决这个问题而诞生的,它的键名所引用的对象都是弱引用,即垃圾回收机制不将该引用考虑在内。也就是说,其他位置对该对象的引用一旦消除,该对象占用的内存就会被垃圾回收机制释放。WeakMap 保存的这个键值对,也会自动消失。
注意,WeakMap 弱引用的只是键名,而不是键值。键值依然是正常引用。
下面我们可以看一下垃圾回收在weakmap上的效果:
然后我们创建一个weakmap和一个数组类型的key。这时的数组被引用了两次,一次时变量key的引用,一次时weakmap的引用,但是weakmap时弱引用。
> let wm = new WeakMap()
> let key = new Array(5 * 1024 * 1024)
> key
> wm.set(key,1)
这个是时候我们再看一下内存的使用情况,会发现内存被占用了很多
> global.gc()
> process.memoryUsage()

此时我们清空变量key对数组的引用,单不需要手动清除weakmap的实例对键名的引用,并手动执行垃圾回收机制,查看内存的使用情况,会发现内存占用回到了原本的6M,可以看出weakmap的键名没有阻止gc对内存的回收。
> key = null
> global.gc()
> process.memoryUsage()
基本使用
WeakMap 与 Map 在 API 上的区别主要是两个。
一是没有遍历操作(即没有keys()
、values()
和entries()
方法),也没有size
属性。因为没有办法列出所有键名,某个键名是否存在完全不可预测,跟垃圾回收机制是否运行相关。这一刻可以取到键名,下一刻垃圾回收机制突然运行了,这个键名就没了,为了防止出现不确定性,就统一规定不能取到键名。
二是无法清空,即不支持clear
方法。因此,WeakMap
只有四个方法可用:get()
、set()
、has()
、delete()
。
weakmap的应用
场景一:WeakMap 应用的典型场合就是 DOM 节点作为键名
let myWeakmap = new WeakMap();
myWeakmap.set(
document.getElementById('logo'),
{timesClicked: 0})
;
document.getElementById('logo').addEventListener('click', function() {
let logoData = myWeakmap.get(document.getElementById('logo'));
logoData.timesClicked++;
console.log(myWeakmap.get(document.getElementById('logo')))
}, false);
document.getElementById('logo')
是一个 DOM 节点,每当发生click
事件,就更新一下状态。我们将这个状态作为键值放在 WeakMap 里,对应的键名就是这个节点对象。一旦这个 DOM 节点删除,该状态就会自动消失,不存在内存泄漏风险。
场景二:部署私有属性
// 创建一个 WeakMap 用于存储私有属性
const privateData = new WeakMap();
class MyClass {
constructor(name) {
// 将私有属性存储在 WeakMap 中
privateData.set(this, {name});
}
// 公共方法可以访问私有属性
getName() {
return privateData.get(this).name;
}
// 修改私有属性的方法
setName(newName) {
privateData.get(this).name = newName;
}
}
const person = new MyClass("Lily");
console.log(person.getName());
person.setName("Tom");
console.log(person.getName());
// 私有属性无法从实例直接访问
console.log(person.name);
Promise & Generator & Async
Promise
Promise是异步编程的一种解决方案,简单点说,promise是一个容器里面保存着未来才结束的事件的结果。其主要特点有两个:
(1)promise有三种状态,pending
(进行中)、fulfilled
(已成功)和rejected
(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。
(2)一旦状态发生了变化就不会在改变了。Promise
对象的状态改变,只有两种可能:从pending
变为fulfilled
和从pending
变为rejected
。
有了Promise
对象,就可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。此外,Promise
对象提供统一的接口,使得控制异步操作更加容易。
基本使用
Promise
对象是一个构造函数,用来生成Promise
实例。构造函数接受一个函数作为参数,函数的两个参数分别为resolve
和reject
。
注意,调用resolve或reject并不会中断promise的操作,一般来说最好在他们前面加上return语句。
Promise 实例具有then
方法,也就是说,then
方法是定义在原型对象Promise.prototype
上的。它的作用是为 Promise 实例添加状态改变时的回调函数。前面说过,then
方法的第一个参数是resolved
状态的回调函数,第二个参数是rejected
状态的回调函数,它们都是可选的。then
方法返回的是一个新的Promise
实例(注意,不是原来那个Promise
实例)。因此可以采用链式写法,即then
方法后面再调用另一个then
方法。
finally()
方法用于指定不管 Promise 对象最后状态如何,都会执行的操作。finally
方法里面的操作,应该是与状态无关的,不依赖于 Promise 的执行结果。**finally
本质上是then
**方法的特例。
function firstTask() {
return new Promise((resolve) => {
console.log("First task");
resolve();
});
}
function secondTask() {
return new Promise((resolve) => {
console.log("Second task");
resolve();
});
}
function thirdTask() {
return new Promise((resolve) => {
console.log("Third task");
resolve();
});
}
firstTask()
.then(() => {
return secondTask();
})
.then(() => {
return thirdTask();
})
.catch((error) => {
console.error("An error occurred:", error);
});
其他API:
-
Promise.all(): 用于将多个 Promise 实例,包装成一个新的 Promise 实例。
-
const p = Promise.all([p1, p2, p3])
p
的状态由p1
、p2
、p3
决定,只有p1
、p2
、p3
的状态都变成fulfilled
,p
的状态才会变成fulfilled
,此时p1
、p2
、p3
的返回值组成一个数组,传递给p
的回调函数。
只要p1
、p2
、p3
之中有一个被rejected
,p
的状态就变成rejected
,此时第一个被reject
的实例的返回值,会传递给p
的回调函数。
注意,如果作为参数的 Promise 实例,自己定义了catch
方法,那么它一旦被rejected
,并不会触发Promise.all()
的catch
方法。
-
example
const p1 = new Promise((resolve, reject) => {
resolve('1');
})
.then(result => result)
.catch(e => e);
const p2 = new Promise((resolve, reject) => {
resolve('2');
})
.then(result => result)
.catch(e => e);
const p3 = new Promise((resolve, reject) => {
throw new Error('报错了');
// resolve('3');
})
.then(result => result)
// .catch(e => e);
Promise.all([p1, p2, p3])
.then(result => console.log('res',result))
.catch(e => console.log('error',e));
-
Promise.race():同样是将多个 Promise 实例,包装成一个新的 Promise 实例。
-
const p = Promise.race([p1, p2, p3]);
只要p1
、p2
、p3
之中有一个实例率先改变状态,p
的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给p
的回调函数。
-
Promise.allSettled():有时候,我们希望等到一组异步操作都结束了,不管每一个操作是成功还是失败,再进行下一步操作**。****Promise.all()
方法只适合所有异步操作都成功的情况,如果有一个操作失败,就无法满足要求。为了解决这个问题,ES2020 引入了Promise.allSettled()
****方法,**用来确定一组异步操作是否都结束了(不管成功或失败)。
const promise1 = Promise.resolve("Promise 1");
const promise2 = Promise.reject(new Error("Promise 2 rejected"));
const promise3 = new Promise((resolve) => setTimeout(() => resolve("Promise 3"), 1000));
// 使用 Promise.allSettled()
Promise.allSettled([promise1, promise2, promise3])
.then((results) => {
results.forEach((result, index) => {
if (result.status === "fulfilled") {
console.log(`Promise ${index + 1} is fulfilled with value: ${result.value}`);
} else if (result.status === "rejected") {
console.log(`Promise ${index + 1} is rejected with reason: ${result.reason}`);
}
});
})
.catch((error) => {
console.error("An error occurred:", error);
});
// Output:
// Promise 1 is fulfilled with value: Promise 1
// Promise 2 is rejected with reason: Error: Promise 2 rejected
// Promise 3 is fulfilled with value: Promise 3
// 使用 Promise.all()
Promise.all([promise1, promise2, promise3])
.then((values) => {
values.forEach((value, index) => {
console.log(`Promise ${index + 1} is fulfilled with value: ${value}`);
});
})
.catch((error) => {
console.error("An error occurred:", error);
});
// Output:
// An error occurred: Error: Promise 2 rejected
Promise.all()
会在遇到第一个 rejected Promise 时立即拒绝整个组,而 Promise.allSettled()
会等待所有 Promise 结束,不管是完成还是拒绝。
-
Promise.any():只要参数实例有一个变成**fulfilled
状态,包装实例就会变成fulfilled
状态;如果所有参数实例都变成rejected
状态,包装实例就会变成rejected
**状态。Promise.any()
跟Promise.race()
方法很像,只有一点不同,就是Promise.any()
不会因为某个 Promise 变成rejected
状态而结束,必须等到所有参数 Promise 变成rejected
状态才会结束。
-
Promise.resolve():将现有对象转为 Promise 对象
Promise.resolve('foo')
// 等价于
new Promise(resolve => resolve('foo'))
-
如果参数是promise对象,Promise.resolve()
将原封返回这个实例。
-
如果参数是一个具有then方法的对象,Promise.resolve()
方法会将这个对象转为 Promise 对象,然后就立即执行thenable
对象的then()
方法。
-
如果参数是一个原始值,或者是一个不具有then()
方法的对象,则Promise.resolve()
方法返回一个新的 Promise 对象,状态为resolved
。
-
Promise.reject()
-
Promise.reject(reason)
方法也会返回一个新的 Promise 实例,该实例的状态为rejected
。
- 等价关系
const p = Promise.reject('出错了');
// 等同于
const p = new Promise((resolve, reject) => reject('出错了'))
应用
-
加载图片:将图片的加载写成一个Promise
,一旦加载完成,Promise
的状态就发生变化。
const { Image } = require("canvas");
const fs = require("fs");
function loadImage(path) {
return new Promise((resolve, reject) => {
const image = new Image();
image.onload = () => resolve(image);
image.onerror = (error) => reject(error);
image.src = path;
});
}
const imagePath = "./O1CN01jJUykX1zwNMUeekbT_!!1990086778.jpg";
loadImage(imagePath)
.then((image) => {
console.log("Image loaded:", image);
})
.catch((error) => {
console.error("An error occurred:", error);
});
-
ajax封装
Generator
Generator是ES6提供的一种异步编程的解决方案,语法上可以把他理解成一个状态机,封装了内部多个状态。由于执行generator函数会返回迭代器对象,我们还可以把generator函数理解成遍历器对象的生成函数。
在形式上,generator函数是一个普通函数,主要有两个特征,一是,function关键字和函数名之间有一个*;二是函数内部使用yield表达式定义不同状态。
在调用上,主要特点是调用之后,函数并不执行,返回的也不是函数运行的结果,而是一个执行内部状态的指针对象(遍历器对象),内次调用next方法,会让内部指针从上一次停下来的地方开始执行,知道遇到下一个yield语句或者return语句。可以理解成yield是暂停执行的标识,next是恢复执行的方法。
其中,yield表达式只有在next方法调用,内部执行指向该语句时才会执行,类似于“惰性求值”的语法功能。即下面例子中的123+456不会立即求值。
function* gen() {
yield 123 + 456;
}
每次调用遍历器对象的next
方法,就会返回一个有着value
和done
两个属性的对象。value
属性表示当前的内部状态的值,是yield
表达式后面那个表达式的值;done
属性是一个布尔值,表示是否遍历结束。
前面我们提到过,Generator 函数就是遍历器生成函数,那么我们可以把generator赋值给对象的Symbol.iterator属性,从而使得对象具备Iterator接口,从而可以被...
运算符遍历。
基本使用
Generator 函数可以暂停执行和恢复执行,这是它能封装异步任务的根本原因。除此之外,它还有两个特性,使它可以作为异步编程的完整解决方案:函数体内外的数据交换和错误处理机制。
数据交换
function* gen(x){
var y = yield x + 2;
return y;
}
var g = gen(1);
g.next() // { value: 3, done: false }
g.next(2) // { value: 2, done: true }
第一个next
方法的value
属性,返回表达式x + 2
的值3
。第二个next
方法带有参数2
,这个参数可以传入 Generator 函数,作为上个阶段异步任务的返回结果,被函数体内的变量**y
**接收。
错误处理:使用指针对象的throw
方法抛出的错误,可以被函数体内的try...catch
代码块捕获
function* gen(x){
try {
var y = yield x + 2;
} catch (e){
console.log(e);
}
return y;
}
var g = gen(1);
g.next();
g.throw('出错了');
// 出错了
异步任务封装
function* gen(){
var url = 'https://api.github.com/users/github';
var result = yield fetch(url);
console.log('res',result.bio);
}
var g = gen();
var result = g.next();
result.value.then(function(data){
return data.json();
}).then(function(data){
g.next(data);
});
基于Thunk函数的自动执行
Thunk 函数是自动执行 Generator 函数的一种方法,可以用于 Generator 函数的自动流程管理。下面我们以文件读取为例来了解一下使用。
首先什么是Thunk函数,编译器的“传名调用”实现,往往是将参数放到一个临时函数之中,再将这个临时函数传入函数体。这个临时函数就叫做 Thunk 函数。举例说明:
function f(m) {
return m * 2;
}
f(x + 5);
// 等同于
var thunk = function () {
return x + 5;
};
function f(thunk) {
return thunk() * 2;
}
js语言是传值调用,它的 Thunk 函数含义有所不同。在 JavaScript 语言中,Thunk 函数替换的不是表达式,而是多参数函数,将其替换成一个只接受回调函数作为参数的单参数函数。举例说明
// 正常版本的readFile(多参数版本)
fs.readFile(fileName, callback);
// Thunk版本的readFile(单参数版本)
var Thunk = function (fileName) {
return function (callback) {
return fs.readFile(fileName, callback);
};
};
var readFileThunk = Thunk(fileName);
readFileThunk(callback);
Thunk 函数真正的威力,在于可以自动执行 Generator 函数。下面就是一个基于 Thunk 函数的 Generator 执行器。
function run(gen){
var g = gen();
// 层层添加回调函数
function next(data){
var result = g.next(data);
if (result.done) return result.value;
result.value.then(function(data){
next(data);
});
}
next();
}
run(gen);
co模块
co 也可以自动执行 Generator 函数,前面说过,Generator 就是一个异步操作的容器。它的自动执行需要一种机制,当异步操作有了结果,能够自动交回执行权。
两种方法可以做到这一点。
(1)回调函数。将异步操作包装成 Thunk 函数,在回调函数里面交回执行权。
(2)Promise 对象。将异步操作包装成 Promise 对象,用then
方法交回执行权。
co 模块其实就是将两种自动执行器(Thunk 函数和 Promise 对象),包装成一个模块。使用 co 的前提条件是,Generator 函数的yield
命令后面,只能是 Thunk 函数或 Promise 对象。
-
使用
const fs = require('fs');
const readFile = function (fileName) {
return new Promise(function (resolve, reject) {
fs.readFile(fileName, function(error, data) {
if (error) return reject(error);
resolve(data);
});
});
};
var gen = function* () {
var f1 = yield readFile('../Async/file1.txt');
var f2 = yield readFile('../Async/file2.txt');
console.log(f1.toString());
console.log(f2.toString());
};
var co = require('co');
co(gen).then(function () {
console.log('Generator 函数执行完成');
});
Async
async 函数是什么?一句话,它就是 Generator 函数的语法糖。我们可以使用generator和async分别实现两个文件读取的异步操作。一比较就会发现,async
函数就是将 Generator 函数的星号(*
)替换成async
,将yield
替换成await
。
但async对Generator的主要改进在以下几方面:
-
Generator 函数的执行必须靠执行器,所以才有了
co
模块,而async
函数自带执行器。不同于Genertor需要co模块或next方法才能执行
-
async
表示函数里有异步操作,await
表示紧跟在后面的表达式需要等待结果。语义上比*和yeild更明确。
- yeild语句后面只能时Thunk函数或Promise对象,但是await后面可以是Promise对象或原始类型的值。
- async返回的是Promise对象,比generator返回Iterator对象方便,可以直接使用then指定下一个操作。
基本用法
async
async
函数返回一个 Promise 对象,可以使用then
方法添加回调函数。其中return语句返回的值会成为then方法回调函数的参数。
async函数返回的 Promise 对象,必须等到内部所有await
命令后面的 Promise 对象执行完,才会发生状态改变,除非遇到return
语句或者抛出错误。也就是说,只有**async
函数内部的异步操作执行完,才会执行then
**方法指定的回调函数。
async function getTitle(url) {
let response = await fetch(url);
let html = await response.text();
return html.match(/<title>([\s\S]+)</title>/i)[1];
}
getTitle('https://tc39.github.io/ecma262/').then(console.log)
上面代码中,函数getTitle
内部有三个操作:抓取网页、取出文本、匹配页面标题。只有这三个操作全部完成,才会执行then
方法里面的console.log
。
await命令
await
命令后面是一个 Promise 对象,返回该对象的结果。如果不是 Promise 对象,就直接返回对应的值。另一种情况是,await
命令后面是一个thenable
对象(即定义了then
方法的对象),那么await
会将其等同于 Promise 对象,这里我们可以看一个休眠的例子。
class Sleep {
constructor(timeout) {
this.timeout = timeout;
}
// then (resolve, reject) {
// const startTime = Date.now();
// setTimeout(
// () => resolve(Date.now() - startTime),
// this.timeout
// );
// }
}
(async () => {
const sleepTime = await new Sleep(1000);
console.log(sleepTime);
})();
如果没有then方法直接返回一个sleep对象,定义了then方法会将它看作一个promise处理。
使用注意点
// reject
async function f1() {
await Promise.reject('出错了');
return await Promise.resolve('hello world');
}
f1().then(v => console.log(v)).catch(e => console.log(e))
//error
async function f4() {
await new Promise(function (resolve, reject) {
throw new Error('出错了');
});
return await Promise.resolve('hello world');
}
f4().then(v => console.log(v)).catch(e => console.log(e))
优化处理
// reject
async function f2() {
try {
await Promise.reject('出错了');
} catch(e) {
console.log(e)
}
return await Promise.resolve('hello world');
}
f2().then(v => console.log(v)).catch(e => console.log(e))
// error
async function f5() {
try {
await new Promise(function (resolve, reject) {
throw new Error('出错了');
});
} catch(e) {
// console.log(e)
}
return await Promise.resolve('hello world');
}
f5().then(v => console.log(v)).catch(e => console.log(e))
-
多个await
命令后面的异步操作,如果不存在继发关系,最好让它们同时触发。通常我们可以使用promise.all
-
async函数可以保留运行堆栈,当使用 async
函数时,它会在执行异步操作时暂停函数的执行,并在操作完成后恢复执行。这种暂停和恢复不会导致堆栈信息丢失,因为 async
函数会确保在恢复执行时保留堆栈信息。
async function fetchData() {
try {
const response = await fetch("https://api.example.com/data");
const data = await response.json();
return data;
} catch (error) {
console.error("Error:", error);
// Error stack contains the correct context of the error
console.error("Error stack:", error.stack);
}
}
fetchData();
async 函数可以保留运行堆栈,使得在处理异步操作时,可以轻松获取错误发生的上下文,进行调试。这使得错误处理更加简洁和直观,提高了代码的可读性和可维护性。
基本原理
async 函数的实现原理,就是将 Generator 函数和自动执行器,包装在一个函数里。
async function fn(args) {
// ...
}
// 等同于
function fn(args) {
return spawn(function* () {
// ...
});
}
所有的async
函数都可以写成上面的第二种形式,其中的spawn
函数就是自动执行器。
下面给出spawn
函数的实现,基本就是前文自动执行器的翻版。
function spawn(genF) {
return new Promise(function(resolve, reject) {
const gen = genF();
** function step(nextF) {**
let next;
try {
next = nextF();
} catch(e) {
return reject(e);
}
**if(next.done) {
return resolve(next.value);
}**
Promise.resolve(next.value).then(function(v) {
** step(function() { return gen.next(v); });**
}, function(e) {
step(function() { return gen.throw(e); });
});
}
** step(function() { return gen.next(undefined); });**
});
}
总结:
三种方案都是为解决传统的回调函数而提出的,所以它们相对于回调函数的优势不言而喻。而async/await
又是Generator
函数的语法糖。
- Promise的内部错误使用
try catch
捕获不到,只能只用then
的第二个回调或catch
来捕获,而async/await
的错误可以用try catch
捕获
-
Promise
一旦新建就会立即执行,不会阻塞后面的代码,而async
函数中await后面是Promise对象会阻塞后面的代码。
-
async
函数会隐式地返回一个promise
,该promise
的reosolve
值就是函数return的值。
- 使用
async
函数可以让代码更加简洁,不需要像Promise
一样需要调用then
方法来获取返回值,不需要写匿名函数处理Promise
的resolve值,也不需要定义多余的data变量,还避免了嵌套代码。