自ES6推出以来,yield就一直是其仅次于Promise的第二大难得一个东西,主要难于理解,所以出这篇文章也算是帮助大家理解这一概念。
顺便介绍一些符号和迭代器相关的知识
迭代器
在讲生成器之前,我们先来讲一下迭代器。
迭代器也是ES6推出来的一个新的东西,其可以让开发这控制迭代过程,甚至可以将对象转变为可迭代对象。写为Symbol.iterator,这是ES6推出的知名符号的一种(后面会针对符号讲解一篇文章)
迭代器,顾名思义,就是用来迭代循环的,常见的比如数组、set、map等,我们可以打开控制台查看原型就可以看到:
这里我们可以私自改写这个原型方法让其变成我们控制的迭代过程,举个例子:
Array.prototype[Symbol.iterator] = function () {
let i = 0;
return {
next() {
let data = {
value: arr[i],
done: arr[i] == undefined,
};
console.log(`正在迭代第${i}个元素`);
i++;
return data;
}
};
}
var arr = [1, 2, 3, 4, 5];
for (const iterator of arr) {
}
执行结果如下:
所以从例子中不难看出,迭代器是一个需要返回一个对象中带有next方法且next方法返回值done和value组成的函数。再比如无限迭代的斐波拉契数列
function createFeiboIterator() {
let prev1 = 1;
let prev2 = 1;
let n = 1;
return {
next() {
if (n <= 2) {
value = 1;
}
else {
value = prev1 + prev2;
}
const result = {
value: value,
done: false,
};
prev2 = prev1;
prev1 = result.value;
n++;
return result;
}
}
}
var fib = createFeiboIterator();
console.log(fib.next());
console.log(fib.next());
console.log(fib.next());
console.log(fib.next());
console.log(fib.next());
console.log(fib.next());
console.log(fib.next());
console.log(fib.next());
console.log(fib.next());
console.log(fib.next());
console.log(fib.next());
console.log(fib.next());
console.log(fib.next());
console.log(fib.next());
console.log(fib.next());
console.log(fib.next());
console.log(fib.next());
甚至于你还可以对对象进行自定义迭代器,例如:
const obj = {
a: 1,
b: 2,
[Symbol.iterator]() {
let entry = Object.entries(this);
let i = 0;
return {
next() {
let data = {
done: entry[i] === undefined,
value: entry[i],
};
i++;
return data;
}
}
}
};
for (const item of obj) {
console.log(item);
}
执行结果如下:
迭代器缺点
迭代器虽然可以让我们很好的控制迭代过程,但是有一个不小的缺点,太麻烦了,正常人谁会去写控制迭代的代码呢,所以ES6为了减低麻烦,方便开发人员利用好迭代器出来了一个生成器,当然也在迭代器基础上增添了许多其他功能。
生成器
生成器的语法我就不赘述了,记住生成器函数执行时并不是马上执行,需要调用next方法才能执行即可,所以说生成器函数是一个状态机函数。
生成器最开始是用于生成迭代器的,所以一个生成器函数执行一定返回的是个迭代器。
1. 生成器基本使用:改写斐波拉契数列
function* createFeiboIt() {
let prev1 = 1;
let prev2 = 1;
let n = 1;
while (true) {
let value = 1;
if (n > 2) {
value = prev1 + prev2;
}
yield value;
prev2 = prev1;
prev1 = value;
n++;
}
}
var fib = createFeiboIt();
console.log(fib.next());
console.log(fib.next());
console.log(fib.next());
console.log(fib.next());
console.log(fib.next());
console.log(fib.next());
console.log(fib.next());
console.log(fib.next());
console.log(fib.next());
console.log(fib.next());
console.log(fib.next());
console.log(fib.next());
console.log(fib.next());
console.log(fib.next());
console.log(fib.next());
console.log(fib.next());
console.log(fib.next());
使用生成器来编写无限的斐波拉契数列可以很好的进行代码编写,没有那么多复杂的过程。
2. 传递参数
生成器函数传递参数主要通过三个实例方法next、throw、return
next在生成器中被用来继续执行生成器函数知道遇到下一个yield停止,如果函数执行完了,返回值done就为true了
关于next传递参数的问题,我这里引用阮一峰老师的ES6 入门教程,next的参数是设置上一个yield的结果,yield本身并没有结果,举个例子:
function *test() {
let info = yield 1;
console.log(info);
info = yield 2 + info;
console.log(info);
}
const gen = test();
console.log(gen.next());
console.log(gen.next(2));
console.log(gen.next(5));
解释一下:
gen的第一个next不能传递参数,就算传递参数也没用,因为next传参是设置上一个yield的结果
第二个next传递参数2,是设置第一个yield的结果,则info=2,函数执行到第二个yield时,函数暂停,yield向外抛出一个4
第三个next传递参数5,是设置第二个yield的结果,则info为5,函数执行完毕返回值为undefined
throw和return就比较好理解了,throw用来抛出一个错误到生成器,return用于提前终止生成器函数
跟Promise的结合使用
在async和await没有发布之前,yield常备用来和Promise一起用,达到和async相同的效果,比如说模拟fetch异步请求数据
const p = new Promise((resolve, reject) => {
setTimeout(() => resolve({
json() {
return { name: "john" };
}
}), 5000);
});
function* task() {
const d = yield p;
console.log(d);
const e = yield d.json();
console.log(e);
}
function runTask(genFunc) {
const gen = genFunc();
let result = gen.next();
const handleResult = () => {
if (result.done) {
return;
}
if (typeof result.value.then === "function") {
result.value.then(data => {
result = gen.next(data);
handleResult();
})
.catch(err => {
result = gen.throw(err);
handleResult();
});
} else {
result = gen.next(result.value);
handleResult();
}
}
handleResult();
}
runTask(task);
以上就是我对于生成器的理解,非常nice,你学费了吗?