💙《JavaScript 高级程序与设计》 | 迭代器与生成器

74 阅读3分钟

7.1 理解迭代

JavaScript 中,计数循环就是一种最简单的迭代:

for (let i = 1; i <= 10; ++i) {
console.log(i);
}

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

由于如下原因,通过这种循环来执行例程并不理想:

  • 迭代之前需要事先知道如何使用数据结构。
  • 遍历顺序并不是数据结构固有的。

ES5 新增了 Array.prototype.forEach() 方法,向通用迭代需求迈进了一步(但仍然不够理想):

let collection = ['foo', 'bar', 'baz']; 
collection.forEach((item) => console.log(item));
// foo bar baz

7.2 迭代器模式

迭代器模式描述了一个方案,即可以把有些结构称为“可迭代对象”

可迭代对象是一种抽象的说法。可以把可迭代对象理解成数组或集合这样的集合类型的对象。

可迭代对象不一定是集合对象,也可以是仅仅具有类似数组行为的其他数据结构,比如本章开头提到的计数循环。计数循环和数组都具有可迭代对象的行为。

接收可迭代对象的原生语言特性包括:

  • for-of 循环
  • 数组解构
  • 扩展操作符
  • Array.from()
  • 创建集合
  • 创建映射
  • Promise.all() 接收由期约组成的可迭代对象
  • Promise.race() 接收由期约组成的可迭代对象
  • yield*操作符,在生成器中使用

迭代器 API 使用 next() 方法在可迭代对象中遍历数据。每次成功调用 next(),都会返回一个 IteratorResult 对象,其中包含迭代器返回的下一个值。若不调用 next(),则无法知道迭代器的当前位置。

next() 方法返回的迭代器对象 IteratorResult 包含两个属性:donevaluedone 是一个布尔值,表示是否还可以再次调用 next() 取得下一个值;value 包含可迭代对象的下一个值 (done 为false ,或者 undefined(done 为 true)done: true 状态称为“耗尽”。可以通过以下简单的数组来演示:

// 可迭代对象
let arr = ['foo', 'bar']; 
// 迭代器工厂函数
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: 'foo' } 
console.log(iter.next()); // { done: false, value: 'bar' } 
console.log(iter.next()); // { done: true, value: undefined }

// 如果可迭代对象在迭代期间被修改了,那么迭代器也会反映相应的变化:
let arr = ['foo', 'baz']; 
let iter = arr[Symbol.iterator](); 
console.log(iter.next()); // { done: false, value: 'foo' } 
// 在数组中间插入值
arr.splice(1, 0, 'bar'); 
console.log(iter.next()); // { done: false, value: 'bar' } 
console.log(iter.next()); // { done: false, value: 'baz' } 
console.log(iter.next()); // { done: true, value: undefined }

7.3 生成器

生成器的形式是一个函数,函数名称前面加一个星号(*)表示它是一个生成器。只要是可以定义函数的地方,就可以定义生成器。

箭头函数不能用来定义生成器函数。标识生成器函数的星号不受两侧空格的影响;

调用生成器函数会产生一个生成器对象。生成器对象一开始处于暂停执行(suspended)的状态。与迭代器相似,生成器对象也实现了 Iterator 接口,因此具有 next() 方法。调用这个方法会让生成器 开始或恢复执行。

next() 方法的返回值类似于迭代器,有一个 done 属性和一个 value 属性。函数体为空的生成器函数中间不会停留,调用一次 next() 就会让生成器到达 done: true 状态。

function* generatorFn() {} 
let generatorObject = generatorFn(); 
console.log(generatorObject); // generatorFn {<suspended>} 
console.log(generatorObject.next()); // { done: true, value: undefined } 
value 属性是生成器函数的返回值,默认值为 undefined,可以通过生成器函数的返回值指定:
function* generatorFn() { 
 return 'foo'; 
} 
let generatorObject = generatorFn(); 
console.log(generatorObject); // generatorFn {<suspended>} 
console.log(generatorObject.next()); // { done: true, value: 'foo' }

yield 关键字可以让生成器停止和开始执行,也是生成器最有用的地方。

yield 关键字只能在生成器函数内部使用,用在其他地方会抛出错误。

提前终止生成器:

  • 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>}
    
  • 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>}
    

如果生成器对象还没有开始执行,那么调用 throw()抛出的错误不会在函数内部被捕获,因为这相当于在函数块外部抛出了错误。