一文读懂迭代器和生成器

665 阅读10分钟

导读

读完本篇文章,你可以学到:迭代器和生成器的概念、作用和基本使用方法。

迭代器

迭代

迭代就是反复执行某一个步骤。

循环是迭代的基础,它可以指定迭代的次数,以及每次迭代要执行的操作。

每次循环都会在下一次迭代开始之前完成,而每次迭代的顺序都是事先定义好的。

for (let i = 0; i <= 5; ++i) {
    console.log(i);
}

但是,通过循环来进行迭代有的时候并不理想,会有如下的限制:

  1. 迭代之前需要事先知道要迭代目标的数据结构。如这里使用的是数组,所有通过[]操作符获取数据,但是如果不是数组呢。
  2. 需要实现知道数据结构的遍历顺序。通过递增索引来访问数据是数组独有的方式,并不适用于其它具有隐式顺序结构的数据结构。

那么有没有办法在不知道对象内部结构的时候,也可以按顺序访问其中的每个元素呢?

迭代器模式

迭代器模式是设计模式的一种,也是上述问题的解决方案。

迭代器模式能够提供一种方案,把迭代的过程从业务逻辑中分离出来,顺序访问一个聚合对象中的各个元素,而又不需要暴露该对象的内部表示。

方案内容:

定义了两个协议,可迭代协议迭代器协议。只要某个结构实现了可迭代协议(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对象包含两个值:

  1. value: 包含可迭代对象的下一个值,donetrue的时候是undefined
  2. done: 表明是否迭代完成,truefalse

根据迭代器协议完善一下上面可迭代对象的代码:

// 可迭代对象
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循环通过breakcontinuereturnthrow提前退出
  • 解构操作并未消费所有值

下面以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); //执行生成器函数代码
// 获取数据中...
// 得到数据: 数据

引用: