阅读 236

[JS犀牛书英文第七版笔记]12. 迭代器和生成器

GitHub 链接:javascript-the-definitive-guide

上一章链接:JavaScript 标准库

迭代器和生成器(Iterators and generators)

可迭代对象(Iterable objects)和其迭代器(iterators)是在 ES6 中新增的特性。例如数组,类型化数组,字符串,Set 和 Map 都是可迭代对象。这意味着我们可以使用 for of 循环来对它们进行迭代:

let sum1 = 0;
for (let i of [1,2,3]) { // 对数组元素的值进行迭代
  sum1 += i;
}
sum1 // 6

let sum2 = 0;
for (let s of '123') { // 对字符串的字符进行迭代
  sum2 += +s;
}
sum2 // 6 
复制代码

迭代器也使得我们可以对可迭代对象进行展开操作和解构赋值:

let chars = [..."abcd"]; // chars == ["a", "b", "c", "d"]
let [w, x, y, z] = chars; // z = "d"
复制代码

Map 会以 [key, value] 形式进行迭代,或者可以通过 keys() 和 values() 方法进行 key 或者 value 的迭代;Set() 构造函数可以接收一个可迭代对象作为 argument:

let m = new Map([["one", 1], ["two", 2]]);
[...m] // [["one", 1], ["two", 2]]
[...m.entries()] // [["one", 1], ["two", 2]]
[...m.keys()] // ["one", "two"]
[...m.values()] // [1, 2]

new Set("abc"); // 等同于 new Set(["a", "b", "c"])
复制代码

迭代器是怎么工作的(How Iterators Work)

套了解迭代器的工作原理,我们要从下面三个方面展开:

  • 可迭代对象(iterable object)
  • 迭代器(itertor)对象本身,他会被用于进行迭代
  • 迭代结果对象(iteration result object),它会用于存放每一次迭代的结果
  1. 可迭代对象就是任何拥有迭代方法(Symbol Symbol.iterator 为方法名)的对象,该方法可返回迭代器对象。

  2. 迭代器对象是任何拥有 next() 方法的对象,该方法会返回一个迭代结果对象。

  3. 迭代结果对象是一个拥有属性 value 和 done 的对象。

若想要迭代一个可迭代对象,我们需要先调用其迭代方法去获得一个迭代器对象,然后调用该迭代器对象的 next() 方法来迭代对象,直到返回值的 done 属性变成了 true。所以我们可以如下实现 for of 循环:

let a = [1,2,3,4,5]; // 创建一个数组(可迭代对象)
let iteratorOfa = a[Symbol.iterator](); // 储存数组的迭代方法
// 将 result 赋值为 next() 的返回值,即迭代结果对象。在迭代结果对象的 done 属性不为 false 时,进行迭代
for (let result = iteratorOfa.next(); !result.done; result = iteratorOfa.next()) {
  console.log(result.value); // 打印迭代结果对象的 value 属性,即数组元素值
}
复制代码

实现一个可迭代对象(Implementing Iterable Objects)

想要让一个对象可以被迭代的话,我们需要为其实现一个迭代方法。该方法名是一个叫做 Symbol.iterator 的 Symbol;这个方法又必须返回一个可拥有 next() 方法的迭代对象;next() 方法又必须返回一个拥有 value 和 done 属性的迭代结果对象:

class Range { // 创建一个新的类
  constructor(lower, upper) { // 定义其构造函数
    this.lower = lower;
    this.upper = upper;
  }
  [Symbol.iterator]() { // 定义其迭代方法
    let next = this.lower; // 下一个返回值
    let last = this.upper; // 最后的返回值
    return { // 返回可迭代对象
      next() { // 可迭代对象的 next() 方法,其返回值是迭代结果对象
        if ( next <= last) { // 在迭代未结束时返回拥有 value 属性的迭代结果对象
          return { value : next++};
        } else { // 否则返回拥有 done 为 true 的迭代结果对象
          return { done : true};
        }
      },
      [Symbol.iterator]() { return this; } // 可以将迭代器本身设置为可迭代对象
    }
  }
}
[...new Range(-3,5)]; // [-3, -2, -1, 0, 1, 2, 3, 4, 5]
复制代码

除去定义类时让其可被迭代,我们也可以让函数返回可迭代的值,就比如我们可以用这种方法来实现数组的 map() 或者 filter() 方法:

function map(iterable, f) { // 实现 map 方法,传入可迭代对象和其 map 时调用的函数
  let iterator = iterable[Symbol.iterator](); // 取得可迭代对象的迭代器
  return { //返回一个对象,它既是可迭代对象,也是迭代器
    [Symbol.iterator]() {
      return this;
    },
    next() {
      let v = iterator.next();
      if ( v.done ) {
        return v;
      } else {
        return { value: f(v.value) };
      }
    }
  };
}
[...map([1,2,3,4],a => 2*a )] // [2, 4, 6, 8]
复制代码

终止迭代器(“Closing” an Iterator: The Return Method)

在某些时候,迭代器一路迭代到底,而是在中间就停止,比如 for of 循环中的 break 或者 return 或者一个异常;或者解构赋值时不需要所有可迭代对象中的元素。

在迭代器对象上我们也可以实现一个 return() 方法,如果迭代器在 next() 还没有返回到 done 为 true 的迭代结果对象时就停止了的话,编译器便会开始查找是否有 return() 方法,若有,则会不传参数然后调用它。

生成器(Generators)

虽然迭代器使我们可以使用 for of 循环和展开操作符之类便捷的新特性。但是对于实现新的可迭代对象、其迭代器、以及其迭代结果对象则会十分复杂。这就要介绍到生成器了(generators),它使我们可以更便捷的实现新的可迭代对象。

生成器其实也是一种迭代器,不过拥有了 ES6 新增的句法,使其变得更为灵活和便捷。

如果想要定义一个生成器,我们需要先定义一个生成器函数。生成器函数的句法和普通函数无异,只不过是通过 function* 而非 function 关键字而定义。当我们调用生成器函数时,他并不会执行函数体,而是返回一个生成器对象,而这个生成器对象也是一个迭代器。在调用该迭代器的 next() 方法时,会继续执行生成器函数体中的语句,直到遇到下一个 yield 关键字。yield 是一个 ES6 新增的关键字,它类似于 return,所以 yield 之后的值便会成为调用 next() 的返回值:

function* genF() { // 定义一个生成器函数,但调用该函数不会直接运行其函数体,而是创建新的生成器对象
  yield 1; // 生成器对象的 next() 方法会执行函数体,直到遇见 yield 关键字然后停下
  yield 2; // 然后该 yield 之后的值便会作为返回值返回
  yield 3;
  yield 4;
  yield 5;
}
let gen = genF(); // 在调用该函数时,获得一个生成器
gen.next().value; // 1,生成器也是一个可迭代对象,并且可以迭代 yield 的值
gen.next().value; // 2
gen.next().done; // false
gen.next().value; // 4
gen.next().value; // 5
gen.next().done; // true

gen[Symbol.iterator]() // 该生成器也有迭代方法,所以也可以使用 for of 或解构赋值等
复制代码

在对象或者类中,我们也可以使用函数简写来实现生成器:

let o = {
  x: 1, y: 2, z: 3,
  *g() {
    for(let key of Object.keys(this)) {
      yield key;
    }
  }
};
[...o.g()] // ["x", "y", "z", "g"]
复制代码

不过需要注意的是,箭头函数不能用来创建生成器函数。

使用生成器函数可以使我们在定义一个可迭代类时变得更为简单,我们不需要一层一层编写迭代方法、迭代器、迭代结果对象等等,我们只需要编写 *[Symbol.iterator]() 方法即可:

*[Symbol.iterator]() { // 使用生成器函数编写迭代方法
  for (let x = this.lower; x < this.upper; x++) {
    yield x;
  }
}
// 等同于前面的例子:
[Symbol.iterator]() { // 定义其迭代方法
  let next = this.lower; // 下一个返回值
  let last = this.upper; // 最后的返回值
  return { // 返回可迭代对象
    next() { // 可迭代对象的 next() 方法,其返回值是迭代结果对象
      if ( next <= last) { // 在迭代未结束时返回拥有 value 属性的迭代结果对象
        return { value : next++};
      } else { // 否则返回拥有 done 为 true 的迭代结果对象
        return { done : true};
      }
    },
    [Symbol.iterator]() { return this; } // 可以将迭代器本身设置为可迭代对象
  }
}
复制代码

可见使用生成器会变得便捷得多。

yield*

使用 yield* 使我们可以更为方便的迭代多个可迭代对象的元素:

function* genF(...iterables) { // 不使用 yield*
  for(let iterable of iterables) {
    for(let element of iterable) {
      yield element;
    }
  }
}
[...genF('abcd',[1,2,3])]; // ["a", "b", "c", "d", 1, 2, 3]

function* genF(...iterables) { // 不使用 yield*
  for(let iterable of iterables) {
    yield* iterable;
  }
}
[...genF('abcd',[1,2,3])]; // ["a", "b", "c", "d", 1, 2, 3]

// 两个方法效果一样
复制代码

进阶生成器特性(Advanced Generator Features)

虽然生成器最常被用于创建迭代器,但其实它还有一个很重要的特性便是可以暂停和重新开始运行,并在其中产出中间值。

生成器函数的返回值

在生成器函数中,我们并没有提到它的返回值,即其 return 关键字。return 可以在生成器函数中被使用,但是它并不会直接返回值,而是终止生成器的执行。这个返回值虽然不会直接返回,但是可以在等下被用到:

回想一下,迭代器的 next() 方法的返回值是一个拥有 value 和 done 属性的迭代结果对象,对于正常的迭代器和生成器来说,如果 value 被定义,则 done 因为 false 或 undefined;若 done 为 true,则 value 因为 undefined。

但如果生成器返回了一个值,则最后一次调用 next() 返回的迭代结果对象的 value 和 done 都会被定义:value 会包含该返回值,而 done 为 true。他虽然会被 for of 循环或者解构赋值等忽略,但是我们可以显示的去调用其 next() 方法来取得该值:

function* genF() { 
  yield 1; 
  yield 2;
  return 3;
  yield 4;
}
[...genF()] // [1, 2],return 3 终止迭代,并且返回值也会被忽略,之后的 4 也会被忽略

let gen = genF();
gen.next(); // {value: 1, done: false}
gen.next(); // {value: 2, done: false}
gen.next(); // {value: 3, done: true} // 可以显示的调用 next() 取得返回值
gen.next(); // {value: undefined, done: true},迭代终止,无法取得 4
复制代码

yield 表达式的值

在前面,我们将 yield 用作为一个产出值的语句,但其实它是一个表达式,也可以拥有自己的值。

在生成器的 next() 方法被调用时,它会一路运行到 yield 表达式,其之后紧跟的表达其会被运算然后作为 next() 的返回值。然后生成器函数会停止执行,直到下一次 next() 的调用。然后该 next() 的 argument 就会成为 yield 的值。

所以对应关系是:

  • 生成器会将 yield 之后的值返回给其调用者
  • 调用者又可以在 next() 方法中传入 argument 作为 yield 的值
function* genF() {
  let y1 = yield 1;
  let y2 = yield 2;
  let y3 = yield 3;
  console.log(y1+y2+y3);
  return 4;
}

let g = genF();
g.next(); // 第一次调用 next 时,结果对象的 value 为 1
g.next(4); // 第二次调用 next 时,结果对象的 value 为 2,同时为 y2 进行了 y1 = 4 的赋值
g.next(5); // 第三次调用 next 时,结果对象的 value 为 3,同时为 y2 进行了 y2 = 5 的赋值
g.next(6); // 第四次调用 next 时,结果对象的 value 为 4,同时为 y2 进行了 y3 = 6 的赋值,并且返回 15
复制代码

return() 和 throw() 方法

在生成器上,它除了拥有 next() 方法,也拥有了 return() 和 throw() 方法。如它们的名字一样,调用它们会返回值或者抛出异常,就如同在生成器中加入了 return 或者 throw 关键字一样:

function* genF() { 
  yield 1; 
  yield 2;
  yield 3;
}
let g = genF();
g.return(); // 提前结束迭代,返回 {value: undefined, done: true}
g.next() // 迭代已经结束,返回 {value: undefined, done: true}
let g2 = genF();
g2.throw(); // 提前结束迭代,抛出 Uncaught undefined
复制代码

throw() 方法会在生成器内部抛出一个异常,但如果生成器函数内部拥有异常捕获机制,则异常就不一定会是致命的,这意味着生成器可以接着执行:

function* count() { // 计数生成器函数,在遇到异常时重置计数器
  let i = 1;
  while(true) {
    try {
      yield i++;
    }
    catch {
      i = 0;
    }
  }
}
let counter = count();
counter.next();
counter.next(); // {value: 2, done: false}
counter.throw(); // {value: 0, done: false},重置计时器,不会抛出异常,因为被生成器处理了
counter.next(); // {value: 1, done: false}
复制代码

小结

这一章的要点:

  • for of 循环和 ... 展开操作符可以对可迭代对象使用
  • 具有 Symbol 方法 [Symbol.iterator] 的对象,且该方法会返回一个迭代器,为可迭代对象
  • 迭代器对象拥有 next() 方法用于返回迭代结果对象
  • 迭代结果对象有一个 value 属性保存迭代值,和一个 done 属性来确定迭代是否完成
  • 生成器函数(function*)可以使我们更便捷的定义迭代器
  • 在调用生成器函数时,函数体不会直接执行,而是返回一个可迭代的迭代器对象
  • 每次调用生成器的生成的迭代器的 next() 时,会执行函数体直到遇见 yield 并将其返回

下一章链接:Promise,async/await 和异步迭代器