导读
读完本篇文章,你可以学到:迭代器和生成器的概念、作用和基本使用方法。
迭代器
迭代
迭代就是反复执行某一个步骤。
循环是迭代的基础,它可以指定迭代的次数,以及每次迭代要执行的操作。
每次循环都会在下一次迭代开始之前完成,而每次迭代的顺序都是事先定义好的。
for (let i = 0; i <= 5; ++i) {
console.log(i);
}
但是,通过循环来进行迭代有的时候并不理想,会有如下的限制:
- 迭代之前需要事先知道要迭代目标的数据结构。如这里使用的是数组,所有通过
[]
操作符获取数据,但是如果不是数组呢。 - 需要实现知道数据结构的遍历顺序。通过递增索引来访问数据是数组独有的方式,并不适用于其它具有隐式顺序结构的数据结构。
那么有没有办法在不知道对象内部结构的时候,也可以按顺序访问其中的每个元素呢?
迭代器模式
迭代器模式是设计模式的一种,也是上述问题的解决方案。
迭代器模式能够提供一种方案,把迭代的过程从业务逻辑中分离出来,顺序访问一个聚合对象中的各个元素,而又不需要暴露该对象的内部表示。
方案内容:
定义了两个协议,可迭代协议和迭代器协议。只要某个结构实现了可迭代协议(Iterable
接口),就可以被迭代器(Iterator
)消费。
目前大部分语言已经内置了迭代器的实现,包括JavaScript。
迭代器种类
迭代器可以分为内部迭代器和外部迭代器,它们有各自的适用场景。
内部迭代器
JavaScript中的Array.prototype.forEach
就是一个内部迭代器。
const arr = ['a', 'b', 'c'];
arr.forEach(element => console.log(element));
// expected output: "a"
// expected output: "b"
// expected output: "c"
forEach
函数的内部已经定义好了迭代规则,它完全接手整个迭代过程,外部只需要一次初始调用。
内部迭代器在调用的时候很方便,外界不用关心迭代器的内部实现,跟迭代器的交互也仅仅是一次初始调用。
但是,由于内部已经定义好迭代规则,所以外部获取数据只能按这个定好的规则来,如果想修改迭代内容和顺序等,只能在迭代器内部修改迭代规则。
外部迭代器
外部迭代器必须显式地请求迭代下一个元素。
下面是外部迭代器的一个例子:
const Iterator = function (obj) {
let current = 0;
const next = function () {
current += 1;
};
const isDone = function () {
return current >= obj.length;
};
const getCurrItem = function () {
return obj[current];
};
return {
next,
isDone,
getCurrItem,
length: obj.length,
};
};
const iterator = Iterator([1,2,3]);
console.log(iterator.getCurrItem()); // 1
iterator.next();
console.log(iterator.getCurrItem()); // 2
iterator.next();
console.log(iterator.getCurrItem()); // 3
不难看出,外部迭代器虽然调用方式相对复杂,但它的适用面更广,也能满足更多变的需求。
区别
- 内部迭代器:调用方式简单,灵活性差
- 外部迭代器:调用方式复杂,灵活性好
可迭代对象(iterable object)
实现了可迭代协议的对象叫做可迭代对象。
可迭代对象是一种抽象的说法。基本上,可以把可迭代对象理解成数组或者集合类型的对象。他们包含的元素都是有限的,而且都具有无歧义的遍历顺序。
可迭代协议(Iterable protocols)
可迭代协议表示将一个对象变为可迭代对象需要遵循的规则。
实现可迭代协议(Iterable 接口)需要具备以下两种能力:
- 支持迭代的自我识别功能(让js引擎能辨认出来该对象可以被迭代)
- 创建实例
Iterable
接口的对象的能力
在ECMAScript中,这意味着必须暴露一个属性作为默认迭代器,而这个属性必须使用特殊的Symbol.iterator
作为键。这个默认迭代器必须引用一个迭代器工厂函数,调用这个工厂函数必须返回一个新迭代器。
用代码来表示:
// 可迭代对象
const iterableObj = {
// 迭代器工厂函数
[Symbol.iterator]() {
// 返回一个迭代器
return // 迭代器代码...
}
}
如果对象原型链上的父类实现了
Iterable
接口,那这个对象也就实现了这个接口
很多内置类型都实现了Iterable
接口:
- 字符串
- 数组
- 映射(
Map
) - 集合(
Set
) arguments
对象NodeList
等DOM集合类型
实际写代码的过程,很多原生语言特性会自动调用可迭代对象的迭代器工厂函数,从而创建一个迭代器进行数据的迭代过程,这些元素语言特性包括:
for-of
循环- 数组解构(
...
) - 扩展操作符(
...
) - 创建
Map
- 创建
Set
Promise.race()
/Promise.all()
接受由Promise
组成的可迭代对象yield*
操作符,在生成器中使用,后面会讲到
其实这些方法就是一直调用迭代器的next()
方法直到done
的状态为true
为止
迭代器协议
迭代器协议描述了迭代器的数据结构。
迭代器是一种一次性使用的对象,用于迭代与其关联的可迭代对象。
迭代器使用next()
方法在可迭代对象中遍历数据,每次成功调用,都会返回一个IteratorResult
对象。
IteratorResult
对象包含两个值:
value
: 包含可迭代对象的下一个值,done
为true
的时候是undefined
done
: 表明是否迭代完成,true
或false
根据迭代器协议完善一下上面可迭代对象的代码:
// 可迭代对象
const iterableObj = {
// 迭代器工厂函数
[Symbol.iterator]() {
// 返回一个迭代器对象
return {
next() {
// 返回一个IteratorResult对象
return { done: false, value: 'foo' }
}
}
}
}
这里用代码来表示一下各个术语之间的关系:
// 可迭代对象:数组实现了可迭代协议,所示是可迭代对象
let arr = ['a', 'b'];
// 迭代器工厂函数
console.log(arr[Symbol.iterator]); // f values() { [native code] }
// 迭代器
let iter = arr[Symbol.iterator]();
console.log(iter); // ArrayIterator();
// 执行迭代
console.log(iter.next()); // { done: false, value: 'a' }
console.log(iter.next()); // { done: false, value: 'b' }
console.log(iter.next()); // { done: true, value: undefined }
自定义迭代器
有了可迭代协议和迭代器协议,如何编写自定义迭代器就很明确了,只要按照协议内容编写即可,写个计数器:
class Counter {
// Counter实例应该迭代的次数
constructor(limit){
this.limit = limit;
}
[Symbol.iterator](){
let count = 1;
let limit = this.limit;
return {
next() {
if (count <= limit) {
return { done: false, value: count++ };
} else {
return { done: true, value: undefined };
}
}
}
}
}
let counter = new Counter(3); // 实例化一个迭代器对象
// counter实现了Iterable接口,所以可以被用在任何可以接受可迭代对象的地方,如for-of
for (let i of counter) { console.log(i); }
// 1
// 2
// 3
提前终止迭代器
可以通过可选的return()
方法终止迭代器。
return()
方法用于指定在迭代器提前关闭时执行的逻辑。
class Counter {
constructor(limit){
this.limit = limit;
}
[Symbol.iterator](){
let count = 1;
let limit = this.limit;
return {
next() {
if (count <= limit) {
return { done: false, value: count++ };
} else {
return { done: true, value: undefined };
}
},
return () {
console.log('Exiting early');
return { done: true };
}
}
}
}
执行迭代的结构在想让迭代器知道它不想遍历到迭代对象耗尽时,就可以“关闭”迭代器。可能的情况包括:
for-of
循环通过break
、continue
、return
或throw
提前退出- 解构操作并未消费所有值
下面以break
为例
const counter = new Counter(5);
for (let i of counter) {
if (i > 2) {
break;
}
console.log(i);
}
// 1
// 2
// Exiting early
有些迭代器是不能关闭的(比如,数组的迭代器就是不能关闭的)。设置了return()
方法不能保证迭代器一定会关闭,但是有触发关闭的语句执行时候,return()
方法还是会执行。如果迭代器没有关闭,则可以继续从上次离开的地方继续迭代。已数组为例:
const a = [1, 2, 3, 4, 5];
const iter = a[Symbol.iterator]();
for (let i of iter) {
console.log(i);
if (i > 2) {
break;
}
}
// 1
// 2
// 3
for (let i of iter) {
console.log(i);
}
// 4
// 5
生成器
生成器是ES6新增的一个及其灵活的结构,拥有在一个函数快内暂停和恢复代码执行的能力。
使用生成器可以自定义迭代器和实现协程。React的异步可中断更新就是使用协程的思想,感兴趣的可以看看大佬写的这篇文章:这可能是最通俗的 React Fiber(时间分片) 打开方式。
生成器基础
生成器的形式是一个函数,函数前面加一个星号(*)就表示它是一个生成器。只要可以定义函数的地方,就可以定义生成器。
下面是生成器的几种定义方式
function * generatorFn() {}
let generatorFn = function *() {}
星号左右两边只要要有一个个空格,在哪边都行。
注意:箭头函数不能用来定义生成器
生成器对象
调用生成器函数会产生一个生成器对象,生成器对象符合可迭代协议和迭代器协议,所以具有next()
方法
生成器对象一开始处于暂停状态(suspended
)的状态,只会在初次调用next()
后开始执行
function* generatorFn() {
console.log("foobar");
return "foo"; // 生成器函数的返回值做为next方法返回值中value属性的值
}
// 生成器对象,既是迭代器,也是可迭代对象
const g = generatorFn(); // 调用生成器函数不会执行函数体中的内容
console.log(g); // generatorFn {<suspended>}
console.log(g.next()); // 第一次调用next后才开始执行
// 'foobar'
// { value: 'foo', done: true }
// 生成器对象既是迭代器,也是可迭代对象
console.log(g === g[Symbol.iterator]()); // true
yield
关键词
yeild
关键词可以让生成器停止和开始执行,yeild
只能在生成器内部使用。
生成器函数在遇到yeild
关键字之前会正常执行。遇到这个关键字后,执行暂停,函数作用域的状态会被保留。停止执行生成器函数只能通过在生成器对象上调用next()
方法来恢复执行。
yeild
关键字生产的值会出现在next()
方法返回的对象里。
通过yeild
关键词退出的生成器函数会处在done: false
状态;通过return
关键字退出的生成器函数会处于done: true
状态。
function* generatorFn() {
yield "foo";
yield "boo";
return "baz";
}
const g = generatorFn();
console.log(g.next()); // { value: 'foo', done: false }
console.log(g.next()); // { value: 'boo', done: false }
console.log(g.next()); // { value: 'baz', done: true }
生成器对象作为迭代器使用
生成器函数会返回一个生成器对象,这个对象是一个可迭代对象,使用yield
可以定义每次迭代返回的值。
function* generatorFn() {
yield "foo";
yield "boo";
yield "baz";
}
for(const x of generatorFn()) {
console.log(x)
}
// foo
// boo
// baz
通过生成器定义自定义迭代对象很方便
function * counter(n) {
while(n--){
yield;
}
}
for (let _ of counter(3)) {
console.log('foo');
}
// foo
// foo
// foo
使用yield实现输入输出
输入
yeild
关键字会接受传入给next()
方法的第一个参数。
注意:第一次调用
next()
传入的值不会被使用,因为第一次调用时为了开始执行生成器函数
function * generatorFn(initial){
console.log(initial);
console.log(yield);
console.log(yield);
}
let generatorObject = generatorFn('foo');
// 第一次传入的值不会使用
generatorObject.next('bar'); // foo
generatorObject.next('baz'); // baz
generatorObject.next('qux'); // qux
输出
遇到yield
关键字会暂停执行并计算出要产生的值
function * generatorFn(){
yield 'foo'; // 这里要产生的值是foo
}
let generatorObject = generatorFn();
console.log(generatorObject.next()); // {value: 'foo', done: false}
console.log(generatorObject.next()); // {value: undefined, done: true}
产生可迭代对象
使用星号(*)可以增强yield
的行为,让它能够迭代一个可迭代对象。
function *generatorFn(){
yield* [1,2,3];
}
for (const x of generatorFn()) {
console.log(x);
}
// 1
// 2
// 3
因为yield*
实际上只是将一个可迭代对象序列化为一连串可以单独产生的值,所以这几跟把yield
放到一个循环里没有什么不同,所以上面的代码就等价于:
function *generatorFn(){
for (const x of [1,2,3]) {
yield x;
}
}
for (const x of generatorFn()) {
console.log(x);
}
// 1
// 2
// 3
生成器作为默认迭代器
因为生成器对象实现了Iterable接口,而且生成器函数和默认迭代器被调用之后都产生迭代器,所以生成器格外适合作为默认迭代器。
生成器函数() === 默认迭代器()
class Foo {
constructor() {
this.value = [1,2,3];
}
* [Symbol.iterator]() {
yield* this.values;
}
}
const f = new Foo();
for (const x of f) {
console.log(x);
}
// 1
// 2
// 3
提前终止生成器
类似迭代器,生成器也支持“可关闭”的改变。一个实现Iterator
接口的对象一定有next()
方法,还有一个可选的return()
方法。除此之外,生成器还支持throw()
方法
return()
方法和throw()
都可以用于强制生成器进入关闭状态。
return()
提供给return()
方法的值,就是终止迭代器对象的值:
function *generatorFn(){
for(const x of [1,2,3]){
yield x;
}
}
const g = generatorFn();
console.log(g); // generatorFn {<suspended>}
console.log(g.return(4)); // { done:true, value: 4 }
console.log(g);; // generatorFn {<closed>}
与迭代器不同,所有生成器都有return()
方法,只要通过它进入关闭状态,就无法恢复了。
throw()
throw()
方法会在暂停的时候将一个提供的错误注入到生成器对象中。如果错误未被处理,生成器就会被关闭:
function *generatorFn(){
for(const x of [1,2,3]){
yield x;
}
}
const g = generatorFn();
console.log(g); // generatorFn {<suspended>}
try{
g.throw('foo');
}catch(e){
console.log(e); // foo
}
console.log(g);; // generatorFn {<closed>}
如果生成器内部处理了这个错误,那么生成器就不会被关闭,而且还可以恢复执行。错误处理会跳过对应的yield
。
function *generatorFn(){
for(const x of [1,2,3]){
try{
yield x;
}catch(e){}
}
}
const g = generatorFn();
cosnole.log(g.next()); // { done: false, value: 1 }
g.throw('foo');
console.log(g.next()); // { done: false, value: 3 }
将异步代码转化为同步代码
因为生成器可以暂停和恢复执行状态,所以很适合将异步的代码转换成同步的写法(在数据返回之前暂停执行,等数据返回之后再恢复执行后续代码)redux-saga就是使用生成器进行异步处理。
下面举个例子:
/**
* 模拟一个请求
*/
function getData() {
return new Promise(resolve => {
setTimeout(() => {
resolve('数据');
}, 2000)
})
}
/**
* 生成器函数,在这里写业务逻辑,可以将异步代码的写法转化为同步代码写法
*/
function* task() {
console.log('获取数据中...');
let result = yield getData(); //将异步代码转化为同步的写法
console.log('得到数据:', result);
//对数据进行后续处理...
}
/**
* 运行生成器的通用函数
*/
function run(generatorFunc) {
const generator = generatorFunc();
next();
function next(nextValue) {
let result = generator.next(nextValue)
if (result.done) { //迭代结束
return;
} else {
const value = result.value;
if (value instanceof Promise) {
value.then(data => next(data));
} else {
next(value);
}
}
}
}
run(task); //执行生成器函数代码
// 获取数据中...
// 得到数据: 数据
引用:
- developer.mozilla.org/zh-CN/docs/…
- 《JavaScript设计模式与开发实践》
- 《JavaScript高级程序设计(第4版)》