前言
迭代器和生成器在js这门语言中扮演着非常重要的角色,涉及js许多语法糖的实现,比如for of
async await
等等,本篇文章将以一个全新的角度去讲解他们
迭代器
背景知识
什么是迭代?
从一个数据集合中按照一定的顺序,不断的取出数据。
类比产品研发的迭代,产品的迭代是一次次的做出来的,不确定做多少个迭代(有可能做到中途项目废弃等等因素),只能知道依次迭代的动作。强调的是一个过程。
迭代和遍历的区别
如上所说迭代强调的是过程,依次取出数据的动作,不保证能取完数据。
而遍历强调的是把整个数据都要依次取完,更强调一个结果
迭代器
在迭代过程的封装,通常在不同语言中都有不同的表现形式,通常为对象
js中的迭代器
js规定,如果一个对象具有next方法,并且该方法返回一个对象,该对象的格式如下:
js
const obj = {
next (){
return{
value:xxx,
done:xxx
}
}
}
那么就称这个对象为迭代器
以数组循环为例,改造成为迭代器模式
js
const array = [1, 2, 3, 4, 5];
//for循环
for (let i = 0; i < array.length; i++) {
console.log(array[i]);
}
function arrayIterator(arr) {
let i = 0;
return {
next() {
console.log(i);
return {
value: arr[i++],
done: i > arr.length,
};
},
};
}
//迭代器便利数组改造
let iterator = arrayIterator(array);
let data = iterator.next();
console.log(data);
while (!data.done) {
console.log(data);
data = iterator.next();
}
console.log("迭代完成");
虽然从上面代码来看迭代器操作数据会让代码变得复杂,但是也有一个好处,就是隔离的调用者去操作数据源数据的问题。
举一个迭代器能实现的需求,循环不能实现的需求
ini
//依次得到斐波拉契数列前面n位的值
//eg 1 1 2 3 5 8...
function FbIterator() {
let prev1 = 1,
prev2 = 1,
n = 1;
return {
next() {
console.log(n);
let value;
if (n <= 2) {
value = 1;
} else {
value = prev1 + prev2;
}
const result = {
value,
done: false,
};
prev2 = prev1;
prev1 = result.value;
n++;
return result;
},
};
}
const fbIterator = FbIterator();
上面的案例,可以得到一个无限迭代的得波拉契数列
上面例子的FbIterator
和arrayIterator
都称为迭代器创建函数
可迭代器协议
es6规定,如果一个对象具有知名符号属性 Symbol.iterator
,并且属性值是一个迭代器创建函数,则该对象是一个可迭代的(iterable)
eg
js
const obj = {
[Symbol.iterator]() {
return {
next() {
return { value: xx, done: xx };
},
};
},
};
那么
- 如何知晓一个对象是否是可迭代的?
看是否有Symbol.iterator
我们常用的数组就是一个可迭代对象,
ini
const arr = [1, 2, 3, 4];
const iterator = arr[Symbol.iterator]();
console.log(iterator.next());
//{value:1,done:false}
很多的伪数组也是一个可迭代对象
- 如何遍历一个可迭代对象?
js
const arr = [1, 2, 3, 4];
const iterator = arr[Symbol.iterator]();
console.log(iterator.next());
let result = iterator.next();
while (!result.done) {
const item = result.value;
console.log(item);
result = iterator.next();
}
for of
用于循环便利可迭代对象,格式如下
js
for (let item of iterable){
//iterable 可迭代对象
// item 每次迭代得到的数据
}
//这个语法糖实现原理其实就是上面遍历可迭代对象的过程
生成器(Generator)
生成器和迭代器有非常多的关联,其设计出来的核心目的是为了使迭代器书写方便。但是随着发展,生成器也有一些特殊的地方
什么是生成器
生成器是一个通过构造函数 Generator
创造的对象
但是这个对象是没发自己创建的。它是js引擎内部持有
生成器的特点
-
生成器是一个迭代器(一定有next方法)
-
又是一个可迭代对象(一定有知名符号
Symbol.iterator
) -
一定可以使用
for of
循环便利
如何创建生成器
生成器的创建,必需使用生成器函数(Generator Function
)
书写生成器函数
javascript
function *method() {} //该函数一定返回一个生成器
只需要在函数名称前面加上 *
上面函数调用会生成什么?
一定会得到一个生成器
生成器函数内部执行逻辑
javascript
function *method() { //该函数一定返回一个生成器
console.log("test") //直接调用,不会执行 因为这里并没有迭代
}
生成器函数内部是为了给生成器的每次迭代提供数据的
每一次调用生成器的next方法,将导致生成器函数运行到下一个yield
关键字位置
yield
关键字,只能在生成器函数内部使用,表示产生一个迭代数据。
arduino
function *method() { //该函数一定返回一个生成器
console.log("第一次运行") //会执行
yield 1;
console.log("第二次运行") //会执行
yield 2;
console.log("第三次运行") //会执行
}
const generator = method();
consolo.log(generator.next())
//"第一次运行"
//{value:1,done:false}
consolo.log(generator.next())
//"第二次运行"
//{value:2,done:true}
consolo.log(generator.next())
//"第三次运行"
//{value:undefined,done:true}
上述例子,可以看到,生成器主要的核心就是方便产生迭代器
将上面迭代器遍历数据的代码使用生产器重构
js
const array = [1, 2, 3, 4, 5];
//for循环
for (let i = 0; i < array.length; i++) {
console.log(array[i]);
}
function arrayIterator(arr) {
let i = 0;
return {
next() {
return {
value: arr[i++],
done: i > arr.length,
};
},
};
}
//迭代器便利数组改造
let iterator = arrayIterator(array);
let data = iterator.next();
console.log(data);
while (!data.done) {
console.log(data);
data = iterator.next();
}
//使用生成器改造
function* arrayGenerator(arr) {
for (let item of arr) {
yield item;
}
}
const generator = arrayGenerator(array);
console.log("迭代完成");
生成器需要注意的细节
- 生成器函数可以有返回值,返回值出现在第一次done为ture时的value属性中
javascript
function *method() { //该函数一定返回一个生成器
console.log("第一次运行") //会执行
yield 1;
console.log("第二次运行") //会执行
yield 2;
console.log("第三次运行") //会执行
return 10 // 运行为done之后返回10,并且放在value中
}
const generator = method();
- 调用生成器的next方法时,可以传递参数,传递的参数可以交给
yeild
的表达式
ini
function* method() {
//该函数一定返回一个生成器
let num = yield 1;
console.log(num); //这个num就是next参数传进来的参数
num = yield 2 + num;
}
const generator = method();
- 生成器的其他Api
return:调用该方法,可以提前结束生成器函数,从而提前结束整个迭代过程
javascript
function* method() {
yield 1;
yield 2;
yield 3;
}
const generator = method();
generator.next()
generator.return()//直接结束迭代
throw方法:调用该方法,可以在生成器中产生一个错误
生产器的运用
背景
在es6刚出来的时候,是没有async await关键字的使用promise用起来很麻烦,所以这一段真空期时间,有些人都想出来一个办法,使用生成器解决这个问题。
ini
function* task() {
const d = yield 1;
const a = yield "abc";
const resp = yield fetch("....");
const result = yield resp.json();
}
function run(generatorFunc) {
const generator = generatorFunc();
let result = generator.next();
handleResult();
function handleResult() {
if (result.done) {
return;
}
if (typeof result.value.then === "function") {
result.value.then(
(data) => {
result = generator.next(data);
handleResult();
},
(error) => {
result = generator.throw(error);
handleResult();
}
);
} else {
result = generator.next(result.value);
handleResult();
}
}
}
run(task);