参考
- ES6 入门教程:Iterator 和 for...of 循环
- ES6 入门教程:Generator 函数的语法
- ES6 入门教程:Generator 函数的异步应用
- ES6 入门教程:async 函数
Iterator 接口(遍历器接口)
Array和String都用于表示集合,而后者专门用于表示字符集合。对于Array,我们可以通过Array.prototype.forEach和Array.prototype.map来对其进行遍历;但对于String,JavaScript 并没有提供其专属的遍历方法。
// 遍历Array
const arr = ["a", "b", "c"];
arr.forEach((val) => {});
arr.map((val) => {});
// 遍历String(曲线救国)
const str = "abc";
Array.prototype.forEach.call(str, (val) => {});
Array.prototype.map.call(str, (val) => {});
// 当然,Array和String都可以通过for循环来进行遍历
for (let i = 0; i < arr.length; i++) {}
for (let i = 0; i < str.length; i++) {}
在 ES6 中粉墨登场的Set和Map,也用于表示集合。试想,如果能为这类表示集合的数据结构提供一种统一的遍历方法,那该有多棒。于是,Iterator应运而生了。Iterator为不同的数据结构提供了统一的访问机制,以使数据可通过for...of被遍历。在 ES6 中,Array、String、Set和Map都原生具备Iterator接口。
// Array原生具备Iterator接口
// 意味着可以通过for...of来遍历它
const arr = ["h", "e", "l", "l", "o"];
for (let val of arr) {
console.log(val);
}
// 这等价于下面的forEach
arr.forEach((val) => {
console.log(val);
});
// String亦原生具备Iterator接口
const str = "hello";
for (let val of str) {
console.log(val);
}
// Set和Map同理
Object原生不具备Iterator接口,然而,我们常常有对其进行遍历的需要。这时,我们可以手动为其部署Iterator接口,以使其能通过for...of被遍历。在部署Iterator接口之前,我们先来看看在for...of遍历过程中,实际发生了什么。
// 一段for...of遍历
const arr = ["h", "e", "l", "l", "o"];
for (let val of arr) {
console.log(val);
}
// 实际执行流程:
// 1.调用arr的[Symbol.iterator]方法,该方法返回arr的iterator
// 2.不断调用iterator的next方法,该方法返回一个对象
// 3.1 返回的对象的value值会被传给val
// 3.2 当返回的对象的done值为false时,执行才会终止
// 通过以上,我们可以知晓以下几点:
// 1.iterator接口被部署在数据的[Symbol.iterator]属性上
// 2.iterator是一个对象,其有一个next方法
// 3.该next方法会返回一个包含value和done属性的对象
// 可能有人会问,arr的[Symbol.iterator]方法是哪来的
// 答案是,arr的[Symbol.iterator]方法是在JS内部被实现的
// 所以我们才说,Array原生具备Iterator接口
据此,我们试着为Object部署Iterator接口。
// 1.首先,iterator是一个对象
// 其次,其有一个next方法
const iterator = {
next() {},
};
// 2.next方法返回的是一个对象
// 该对象有value和done这两个属性
const iterator = {
next() {
return {
value: "someValue",
done: "hasDone",
};
},
};
// 3.value和done的值从哪来?
// 我们声明一个value,并改写next方法
let value = 1;
const iterator = {
next() {
return value <= 5
? { value: value++, done: false }
: { value: undefined, done: true };
};
};
// 4.把iterator部署到对象上
const obj = {
[Symbol.iterator]() {
return iterator;
},
};
// 5.尝试使用for...of遍历该对象
// 发现依次输出了1、2、3、4、5,遍历成功
// 这说明iterator被成功部署到obj上了!
for (let val of obj) {
console.log(val);
}
上面例子中的value是全局变量,在obj外部。如果想要遍历obj内部的元素,也很简单。
// 假设我们想要以"element_+序号"的顺序来遍历obj
const obj = {
length: 3,
element_1: "A",
element_2: "B",
element_3: "C",
[Symbol.iterator]() {
let index = 1;
return {
// 使用箭头函数,以使this指向obj
next: () => {
return index <= this.length
? {
value: this[`element_${index++}`],
done: false,
}
: {
value: undefined,
done: true,
};
},
};
},
};
// 依次输出A、B、C
for (let val of obj) {
console.log(val);
}
Generator 函数(生成器函数)
Generator函数是一个用于生成Iterator的函数。比如上一节中的obj[Symbol.iterator],以及下面例子中的generator,都是Generator函数。
// 声明generator
function generator() {
let index = 0;
return {
next() {
return index < 3
? { value: index++, done: false }
: { value: undefined, done: true };
},
};
}
// 调用generator,返回iterator
const iterator = generator();
// 调用iterator
iterator.next(); // {value:0, done:false}
iterator.next(); // {value:1, done:false}
iterator.next(); // {value:2, done:false}
iterator.next(); // {value:undefined, done:true}
Generator函数有另一种表达形式——星号函数。
// 声明generator
function* generator() {
// 执行generator(),到这暂停
console.log("before 0");
yield 0;
// 第1次执行iterator.next(),到这暂停
console.log("after 0");
console.log("before 1");
yield 1;
// 第2次执行iterator.next(),到这暂停
console.log("after 1");
console.log("before 2");
yield 2;
// 第3次执行iterator.next(),到这暂停
console.log("after 2");
// 函数末尾有一句隐式的 yield undefined;
// 第4次执行iterator.next(),到这暂停
}
// 调用generator,返回iterator
const iterator = generator(); // 并不会立即输出'before 0'
// 调用iterator
iterator.next(); // 'before 0' {value:0, done:false}
iterator.next(); // 'after 0' 'before 1' {value:1, done:false}
iterator.next(); // 'after 1' 'before 2' {value:2, done:false}
iterator.next(); // 'after 2' {value:undefined, done:true}
不难看出,每次调用iterator.next,都会开始/继续执行generator中的语句,直到碰到yield语句才会暂停执行。同时,会返回一个value值为yield身后的值、表示当前状态的对象。直到generator中的代码全部执行完毕。
此外,iterator.next支持传参,参数会传给上一个yield表达式,作为其返回值;不传参时,其返回值默认为undefined。(要注意的是,yield身后的值为当前状态对象的value值,而非yield表达式的返回值)
function* generator() {
// 第1次执行iterator.next()
// 到"const x = yield 1"右边这部分,即到"yield 1"这里暂停
// 并返回状态对象:{value:1, done:false}
// 第2次执行iterator.next(‘hello’)
// 会把"hello"传给"yield 1",作为"yield 1"的返回值
// 并执行左边剩下的这部分,即执行"const x = 'hello'"
const x = yield 1;
// 第2次执行中,当"yield x"这部分执行完时暂停
// 同理,会返回状态对象:{value:'hello', done:false}
yield x;
}
const iterator = generator();
iterator.next(); // {value:1, done:false}
iterator.next("hello"); // {value:'hello', done:false}
async/await 函数
假设
ajax()为一异步操作。
最开始,我们通过回调函数来进行异步操作。
ajax(params, function (err, dataA) {
ajax(dataA, function (err, dataB) {
ajax(dataB, function (err, dataC) {
console.log(dataC);
});
});
});
后来有了Promise,其将函数的嵌套调用,改为函数的链式调用,解决了回调地狱问题。但是,利用Promise书写的异步操作,代码冗余,看起来仍然不够简洁、直观。
ajax(params)
.then(function (dataA) {
return ajax(dataA);
})
.then(function (dataB) {
return ajax(dataB);
})
.then(function (dataC) {
console.log(dataC);
})
.catch(function (err) {});
联想到星号函数中yield独有的暂停执行功能,我们猜想能否将异步操作书写成下面这样的形式,即以同步形式的代码来完成异步的操作,以符合我们书写和阅读代码时的直觉。
function* service(params) {
const dataA = yield ajax(params);
const dataB = yield ajax(dataA);
const dataC = yield ajax(dataB);
console.log(dataC);
}
然而,由于其中yield表达式的返回值始终为undefined,所以dataA、dataB、dataC的值也均为undefined,故实际上这段代码是无效的。所幸我们可以对Generator函数进行一些巧妙地包装,使如上代码起到预想的效果。
// 1.声明函数spawn,参数为generator
// 调用spawn后,会创建并启动iterator
function spawn(genFunc) {
const iterator = genFunc();
iterator.next(undefined);
}
// 2.在spawn内部写一个中间触发函数step
// 以实现iterator的自动执行和值的传递
function spawn(genFunc) {
const iterator = genFunc();
// 中间触发函数step
function step(nextFunc) {
// 调用nextFunc,即调用iterator.next并返回状态对象
let next = nextFunc();
// 如果状态对象的done为true,则终止执行
if (next.done) return;
// 否则继续调用step,即调用iterator.next
// 且将状态对象的value作为参数传递
step(function () {
return iterator.next(next.value);
});
}
// 第1次调用step
step(function () {
return iterator.next(undefined);
});
}
// 如此一来,我们调用spawn,并传入generator
// 则会自动执行generator内的代码
// 并将每个yield身后的值传递给iterator.next
function* generator(params) {
const dataA = yield ajax(params);
const dataB = yield ajax(dataA);
const dataC = yield ajax(dataB);
console.log(dataC);
}
// 如果ajax是同步操作,那么dataC的值是有效的
// 但如果ajax是异步操作,那么传递的值会是undefined
// 目前为止,这段代码还是没起到作用
// 3.使用Promise包装step
// 以使实现异步操作下的值也能被传递
function step(nextFunc) {
let next = nextFunc();
if (next.done) return;
// 当前操作resolve后,才会执行then并调用step
Promise.resolve(next.value).then(function (val) {
step(function () {
return iterator.next(val);
});
});
}
// 到这里,spawn终于起作用了
// 4.为step加一些错误检测,以完善代码
function step(nextFunc) {
let next;
try {
next = nextFunc();
} catch (err) {
throw err;
}
if (next.done) return;
Promise.resolve(next.value).then(
function (value) {
step(function () {
return iterator.next(value);
});
},
function (err) {
step(function () {
// iterator.throw用法见参考链接
return iterator.throw(err);
});
}
);
}
// 5.可以将整个spawn函数也用Promise包装起来
// 以实现多个"星号函数"的链式调用
function spawn(genFunc) {
return new Promise(function (resolve, reject) {
const iterator = genFunc();
function step(nextFunc) {
let next;
try {
next = nextFunc();
} catch (err) {
reject(err);
}
}
// 如果next.done为true,则resolve
if (next.done) resolve(next.value);
Promise.resolve(next.value).then(
function (value) {
step(function () {
return iterator.next(value);
});
},
function (err) {
step(function () {
return iterator.throw(err);
});
}
);
step(function () {
return iterator.next(undefined);
});
});
}
// 6.为spawn增加一个参数,以实现传参功能
function spawn(genFunc, params) {
// 中间省略...
// 第1次调用iterator.next时,传参params
step(function () {
return iterator.next(params);
});
}
让我们来试用一下刚完成的spwan。
// 原星号函数
function* generator(params) {
var dataA = yield ajax(params);
var dataB = yield ajax(dataA);
var dataC = yield ajax(dataB);
console.log(dataC);
}
// 通过自执行函数启动generator
const params = {};
(function (params) {
return spawn(generator, params);
})(params);
这样一来,写异步操作变得像写同步操作一样简单。而且实际上,我们不需要自己手写spwan函数,因为 ES6 内部替我们做了包装,使用async和await关键字即可实现和spwan一样的功能。比如上面一段代码,用async/await函数写起来是这样的。
async function operate(params) {
var dataA = await ajax(params);
var dataB = await ajax(dataA);
var dataC = await ajax(dataB);
console.log(dataC);
}
operate();
它看起来是不是很眼熟?没错,其形式和星号函数是相同的,只不过*变成了async,yield变成了await。实际上async/await函数内部替我们做了转换。
// 转换前
async function operate(params) {
// codes...
}
// 转换后
function operate(params) {
return spawn(function* (params) {
// codes...(await变成了*)
});
}