《JavaScript 高级程序设计》第7章 迭代器和生成器

268 阅读5分钟

一、先来看看迭代器的例子

        class Counter {
            constructor(limit) {
                this.limit = limit;
            }
            [Symbol.iterator]() {
                let count = 1, limit = this.limit;
                return {
                    next() {
                        if (count <= limit) {
                            return { done: false, value: count++ };
                        } else {
                            return { done: true, value: undefined };
                        }
                    },
                    return(){
                        console.log("提前退出");
                        return {done:true};
                    }
                }
            }
​
        }
        let counter = new Counter(3);
        for (let i of counter) {
            console.log(i);
            if(i==2){
                break;  // 1  // 2  // 提前退出
            }
        } 

java中定义迭代器,一般是通过实现迭代器接口,重写next方法,在js中无需实现接口,可迭代对象的实现直接是通过定义[Symbol.iterator]方法实现。

看下Chat gpt对上述的描述:

JavaScript 中的迭代器(Iterator)协议并不是通过传统的接口(interface)来定义,而是通过约定一个具有特定名称的方法,即 [Symbol.iterator] 方法。这样的设计主要是为了保持语言的灵活性和简洁性。
​
在 JavaScript 中,对象可以被视为拥有一组特定行为(方法)的集合,而不必明确实现某个接口。这种约定式的设计允许开发者自由地定义对象的行为,而不受特定接口的限制。
​
关于 [Symbol.iterator] 方法的作用:
​
当一个对象被用于 for...of 循环时,或者使用 ... 扩展运算符时,JavaScript 将会调用该对象的 [Symbol.iterator] 方法,获取一个迭代器对象。
迭代器对象需要包含一个 next() 方法,该方法在每次迭代时返回一个包含 value 和 done 属性的对象,用于表示当前迭代的值和迭代是否已经结束。
使用这种约定的方式,JavaScript 可以支持各种不同类型的迭代器,而无需强制它们实现特定的接口。
接口(Interface)的实现通常需要明确定义一组方法和属性,而约定式的设计允许更大的灵活性,使得不同类型的对象可以在特定的上下文中表现出类似的行为,而无需强制它们实现相同的接口。这种设计哲学是 JavaScript 的一部分,它更注重对象的行为而非类型。

1.1 迭代器必须实现next方法,可选择实现return方法。

迭代器的获取,比如这里的counter对象 ,可以通过counter[Symbol.iterator]()获取,调用next方法返回的是IteratorResult对象

         let iter = counter[Symbol.iterator]();
         console.log(iter.next()); // {done: false, value: 1}
         console.log(iter.next()); // {done: false, value: 2}
         console.log(iter.return());// {done: true}
         console.log(iter.next()); // {done: false, value: 3}

如果一个迭代器对象实现了 return 方法,那么在迭代过程中可以调用它,从而执行清理操作或提前结束迭代。如果没有实现 return 方法,这个方法会简单地返回一个带有 done 属性为 true 的对象,表示迭代结束。

二、来看看生成器的例子

        function* nTimes(n) {
            if (n > 0) {
                yield* nTimes(n - 1);
                yield n - 1;
            }
        }
        for (const x of nTimes(3)) {
            console.log(x); // 0 1 2
        }

用生成器来实现一个递归函数0 1 2;

这里function* 表示它是一个生成器函数,可以使用yield生成值。

2.1 yield* 的返回值

我们来看看chat gpt对yield *的解释:

function* generator1() {
  yield 1;
  yield 2;
  return 3;
}
​
function* generator2() {
  yield 4;
  const result = yield* generator1();
  yield result + 1;
}
​
const generator = generator2();
​
console.log(generator.next()); // { value: 4, done: false }
console.log(generator.next()); // { value: 1, done: false }
console.log(generator.next()); // { value: 2, done: false }
console.log(generator.next()); // { value: 4, done: true }

如果将generator1中的return 3 删除,则会怎样呢?

function* generator1() {
  yield 1;
  yield 2;
}
​
function* generator2() {
  yield 4;
  const result = yield* generator1();
  yield result + 1;
  yield result + 2;
  yield result + 3;
}
​
const generator = generator2();
​
console.log(generator.next()); // { value: 4, done: false }
console.log(generator.next()); // { value: 1, done: false }
console.log(generator.next()); // { value: 2, done: false }
console.log(generator.next()); // { value: undefined, done: true }

2.2 nTimes(3)为什么可以在可迭代对象的位置?

nTimes 是一个生成器函数,而生成器函数默认就实现了 Symbol.iterator 方法。生成器函数返回一个迭代器对象,该对象具有 next() 方法,因此它是一个可迭代对象。

2.3 使用yield实现输入和输出

上一次让生成器函数暂停的yield关键字会接收到传给next方法的第一个值。

         function * generator(){
            return yield 'foo';
         }
         let generatorObj = generator();
         console.log(generatorObj.next("aaa"));// {value: 'foo', done: false}
         console.log(generatorObj.next("bbb"));// {value: 'bbb', done: true}
         console.log(generatorObj.next("ccc"));// {value: undefined, done: true}

在你的生成器函数中,return yield 'foo'; 中的 yield 'foo' 部分实际上并没有在第一次调用 next 时执行。生成器函数的执行是惰性的,只有在第一次调用 next 时才开始执行,并且执行到第一个 yield 语句时会暂停,等待下一次调用 next

分析代码执行过程:

  • 第一次调用 generatorObj.next("aaa")

  • 开始执行生成器函数,执行到 yield 'foo'; 时暂停,并返回 { value: 'foo', done: false }

  • 注意,这时候 yield 'foo' 并没有真正产生值 'foo',而是等待下一次调用 next。2. 第二次调用 generatorObj.next("bbb")

  • 传递参数 "bbb" 给上一次暂停的 yield 'foo';,此时 yield 'foo' 表达式的值为 "bbb"

  • 生成器函数执行到最后,遇到 return yield 'foo';,这时候 return 的值为 "bbb",生成器函数继续执行,并返回 { value: 'bbb', done: true }。3. 第三次调用 generatorObj.next("ccc")

  • 由于生成器函数已经执行完毕,再次调用 next 不会再执行生成器函数内部的代码,而是直接返回 { value: undefined, done: true }有时候我在想,怎么理解这里的yield呢?

         function * generator(){
              'foo' yield;
              return (yield的值);
         }

首次调用next触发函数执行,遇到yield就返回,这里返回”foo", 然后再次调用next,则返回“bbb"。

2.4 yield*产生可迭代的值

        function* generator() {
            yield* [1, 2];
            yield* [3];
            yield* [4, 5];
        }
        for (let i of generator()) {
            console.log(i); // 1 2 3 4 5
        }

这里就相当于串行迭代了。

2.5 生成器也可以是迭代器

        class Foo {
            *[Symbol.iterator]() {
                yield* [1, 2, 3];
            }
        }
        let foo = new Foo();
        for (let i of foo) {
            console.log(i);// 1,2,3
        }

2.6 生成器调用return可以提前终止,遇到throw()也会关闭,捕捉之后可以继续。

        function* gen(){
            yield* [1,2,3];
        }
        let foo = gen();
        console.log(foo.next());// {value: 1, done: false}
        console.log(foo.return());// {value: undefined, done: true}
        console.log(foo.next());// {value: undefined, done: true}
        function* gen() {
            for (const x of [1, 2]) {
                yield x;
            }
        }
        let foo = gen();
        console.log(foo.next());// {value: 1, done: false}
        try{
           foo.throw('exception'); // Uncaught exception
        }catch(e){
            console.log(e);
        }
        console.log(foo); // gen {<closed>}
        function* gen() {
            for (const x of [1, 2, 3]) {
                try {
                    yield x;
                } catch (e) {
                    console.log(e);
                }
            }
        }
        let foo = gen();
        console.log(foo.next());// {value: 1, done: false}
        foo.throw('exception'); // Uncaught exception
        console.log(foo); // gen {<suspended>}
        console.log(foo.next());// {value: 3, done: false}
        console.log(foo.next());// {value: undefined, done: true}

如果throw过,在yield捕捉异常,则当前yield会跳过。