JavaScript 高级程序设计第 7 章 迭代器和生成器 学习笔记

Posted GoldenaArcher

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JavaScript 高级程序设计第 7 章 迭代器和生成器 学习笔记相关的知识,希望对你有一定的参考价值。

javascript 高级程序设计第 7 章 迭代器和生成器 学习笔记

一个 生成器(generator) 一定是 迭代器(iterator)。

大多数情况下来说,使用 生成器 就够了,因为 生成器 的底层已经变写好了对于子项的迭代,即 next() 中的 done,这样就能更专注的去处理 值(value)。

少数情况,当要传的值相对而言比较复杂的时候——例如说需要使用闭包,更适合使用 迭代器,因为 生成器 是一个函数,局限性较大,而 迭代器 是一个对象,可以将一些属性作为自有变量。

生成器 和 迭代器 诞生的目的都是为了能够更加通用化的处理 对象/集合 的迭代。诚然使用传统的循环体也能解决问题,但是对于 树、图 等结构来说,不了解其结构就无法遍历所有的组件也是一个令人头疼的问题。

什么是迭代

迭代的定义:

  • 按照顺序反复多次执行一段程序,通常有明确的终止条件。
  • 会在一个有序集合上进行,即集合中的所有项都可以按照既定的顺序被遍历到,特别是开始和结束项有明确的定义

例如说,计数循环就是一个最简单的迭代:

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

使用循环实现迭代也有不理想的地方:

  • 迭代之前必须要知道如何使用数据结构

    以数组为例,数组是一个线性的数据类型,因此循环体写起来比较轻松。但是如果是更复杂的图、树、链表,那么循环写起来就会越来越复杂了

  • 顺序遍历不是数据结构固有的

    以索引进行循环是数组的特性,同样,图、树、链表的顺序遍历和 数组 的顺序遍历是不一样的

ES5 新增了一个 forEach 的迭代方法,向通用需求迈进一步。但是 forEach 有一个问题,即没有任何的标识符去终止迭代操作,因此对性能上有一定的损耗。

迭代器

为了解决迭代产生的问题而出现的解决方案,使得开发者无需知道如何迭代——指数据结构以遍历方法——就能实现迭代操作。

迭代器模式 (特别是在 ECMAScript 这个语境下)描述了一个方案,既,可以把有些结构称为 “可迭代对象” (iterable),因为它们实现了正式的 Iterable 接口,而且可以通过迭代器 Iterator 消费(consume)。

迭代器 (iterator) 是按需创建的一次性对象,每个迭代器都会关联一个 可迭代对象,而迭代器会暴露其关联可迭代对象的 API。

迭代器 无需了解其关联的可迭代对象的结构,只需要如何取得连续的值。

这种概念上的分离正式 IterableIterator 的强大之处。

可迭代协议

实现 可迭代协议(iterable protocols) 要求同时具备两种能力:

  • 支持迭代的自我识别能力

  • 创建实例 Iterator 接口 的对象能力

    在 ECMAScript 之中,这就代表着这个对象必须拥有一个键为 Symbol.iterator 的默认迭代器属性。这个默认迭代器必须引用一个迭代器工厂函数,调用这个工厂函数必须返回一个新的迭代器。

    获取基本的 Symbol.iterator 方法为:

    const arr2 = [1, 2, 3, 4, 5, 6];
    
    // 获取 Symbol.iterator 函数
    console.log(arr2[Symbol.iterator]);
    // ƒ values() { [native code] }
    
    // 运行 Symbol.iterator 会生成一个迭代器
    console.log(arr2[Symbol.iterator]());
    // Array Iterator {}
    

迭代器协议

迭代器协议(iterator protocols) 定义了下列规则:

迭代器 时一种一次性使用的对象,用于迭代其关联的可迭代对象。迭代器 API 使用 next() 方法在可迭代对象中遍历数据。每次成功调用 next() 都会返回一个 IteratorResult 对象,其中包含迭代器返回的下一个值。

若不调用 next() 则无法知道迭代器的当前位置。

next() 方法返回的迭代器对象IteratorResult 包含两个属性:

  • done

    布尔值,表示是否可以再次调用 next() 取得下一个值

  • value

    done 为 false, value 包含可迭代对象的下一个值。

    done 为 true, value 的值为 undefined。当 done 为 true 的状态为“耗尽”,即所有的 可迭代对象 均已迭代过。

运行迭代器的方法:

const arr2 = [1, 2, 3, 4, 5, 6];
// Array Iterator {}
let iter = arr2[Symbol.iterator]();

console.log(iter.next());
// {value: 1, done: false}
console.log(iter.next());
// {value: 2, done: false}
// 调用多次后,到值为6的情况也迭代过后
console.log(iter.next());
// {value: undefined, done: true}
// 再调用 next() 也只会产出同样的值
console.log(iter.next());
// {value: undefined, done: true}

迭代器不与某个时刻的 快照(snapshot) 绑定,仅仅是使用标记来记录遍历可迭代对象的历程。

如果可迭代对象在迭代期间被修改了,那么迭代器也会反映相应的变化:

const arr2 = [1, 2, 3, 4, 5, 6];
// Array Iterator {}
let iter = arr2[Symbol.iterator]();

console.log(iter.next());
// {value: 1, done: false}
arr2[1] = 7;
// 修改后的数组为:
// (6) [1, 7, 3, 4, 5, 6]
console.log(iter.next());
// {value: 7, done: false}

自定义迭代器

可迭代协议定义了满足两个条件的对象即可实现迭代器:

  • 支持迭代的自我识别能力
  • 创建实例 Iterator 接口 的对象能力

以书中的实例 Counter 为例,它的迭代指的是 以计数器为起始条件,上限为终止条件,循环输出当前的计数器 Counter,因此满足支持迭代的自我识别能力

创建实例的能力以实现 Symbol.iterator 为准,只要实现了即可满足第二点。

因此给予 Counter 的自定义迭代器如下:

class Counter {
  // 设定上限 和 下限
  constructor(limit) {
    this.counter = 1;
    this.limit = limit;
  }

  // 满足 即迭代的自我识别能力
  // 实现 迭代需要执行的方法
  // 满足 迭代器协议的实现方法—— next()
  next() {
    if (this.counter <= this.limit) {
      return { done: false, value: this.counter++ };
    } else {
      return { done: true, value: undefined };
    }
  }

  // 实现 可迭代协议 第2点
  // 即 Symbol.iterator 的实现
  [Symbol.iterator]() {
    return this;
  }
}

let counter = new Counter(3);

// for of 会调用迭代器方法
for (let i of counter) {
  console.log(i);
  // 1
  // 2
  // 3
}

上面的例子不太完善,因为每个实例只能被执行一次,如果再执行一次 for of 函数,就会出现 undefined 的问题。

即,当再一次调用 for of 时,当前 counter 的值已经超过了 limit,所以会直接走到 else 之中,返回 undefined

为了能够可以重复地调用迭代器,可以利用闭包,将 next() 方法体放到闭包中,然后通过闭包返回迭代器:

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 };
        }
      },
    };
  }
}

提前终止迭代器

通常情况下有以下情况会提前关闭迭代器:

  • for-of 循环通过 break, continue, return, 或 throw 提前退出
  • 解构操作并未消费所有的值

内置语言结构在发现还有更多值可以跌单,但不会消费这些值时,就不会调用 next(), 而是会自动调用 return() 方法。

因此可以这样实现提前终止:

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() {
        return { done: true };
      },
    };
  }
}

let counter = new Counter(5);

for (const i of counter) {
  console.log(i);
  // 1
  // 2
  if (i === 2) {
    break;
  }
}

如果迭代器没有关闭,则还可以从继续上次离开的地方迭代。

生成器

声称骑士 ECMAScript5 新增的一个极为灵活的结构,拥有一个在函数块内暂停和恢复代码执行的能力。这种能力具有深远的影响,如:

  • 使用生成器可以自定义迭代器
  • 实现协程序

生成器基础

生成器的形式是一个函数,函数名称前加一个 星号(*) 来表示它是一个生成器。

注:生成器的语法为 function* func(){} 代表着箭头函数无法用来实现生成器。

只要是可以定义函数的地方,就可以定义生成器:

// 声明生成器
function* generator() {}

// 生成器作为方法
let foo = {
  *generator() {},
};

class Foo {
  *generator() {}
}

class Bar {
  static *generator() {}
}

调用生成器函数会生成一个 生成器对象,生成器对象一开始出于 暂停执行(suspended) 的状态。

与迭代器相似,生成器对象也实现了 Iterator 接口,因此具有 next() 方法。调用这个方法会让生成器开始或恢复执行。

生成器的 next() 方法 与迭代器的相似,也会返回一个 done 属性和 value 属性。

像迭代器的状态可以由迭代器管理,生成器的 next() 方法中的 value 也可以由生成器指定:

function* generator() {
  return "test";
}

let generatorObj = generator();
console.log(generatorObj);
// Object [Generator] {}

console.log(generatorObj.next());
// { value: 'test', done: true }

通过 yield 中断执行

迭代器通过 return 可以中断迭代,生成器也有相似功能,不过它能通过 yeildreturn 实现中断。

通过 yeild 实现中断的生成器状态会停留在 done: false; yeild 关键字只能在生成器内部使用。

通过 return 实现中断的生成器状态会停留在 done: true

生成器作为默认迭代器

直接上个简单的例子:

class Foo {
  constructor() {
    this.values = [1, 2, 3];
  }
  *[Symbol.iterator]() {
    yield* this.values;
  }
}

const f = new Foo();
for (let val of f) {
  console.log(val);
}
// 1
// 2
// 3

注:yeild* iterable 的语法,即 yeild 后跟 * 与一个可迭代对象,实质上是将一个可迭代对象序列为一连串可以单独产出的值。

在这个例子中,for-of 循环调用了默认迭代器,而默认迭代器正好又是一个生成器函数 (function* 语法可以生成一个生成器函数),并产生了一个可迭代的生成器对象。

提前终止生成器

生成器对象可以使用 return 方法切实终止迭代器的运行。调用 return 方法会使返回值中 done 的值变为 true

生成器对象可以使用 throw 方法终止迭代器,即使用抛出一个未捕获的异常导致程序短路的方式终止生成器的运行。如果在调用函数的时候捕获了该异常,那么生成器就不会被关闭,而且还可以恢复运行。

总结

  1. 迭代:在一个有序的集合上进行,有明确的开始和结束项,按照顺序反复多次执行一段程序的过程

  2. 迭代器是将迭代通用化的方法

    迭代器是一个 对象,需要自己管理计数

    迭代器相关知识总结:

    1. 可迭代协议定义了什么样的对象是可以迭代的

    2. 迭代器协议定义了迭代的具体实现,及返回值

    3. 迭代器的返回值为:

      {
        "done": true,
        "value": "value"
      }
      
    4. 不同迭代器实例之间没有联系

    5. 原生对象的改变会影响迭代器迭代的值

    6. 迭代器会阻止垃圾回收程序回收可迭代对象

    7. 迭代器可移植通用的迭代,迭代接口,正式的迭代器类型

    8. 迭代器可以提前停止

    9. 如果迭代器没有关闭,则还可以从继续上次离开的地方迭代。

  3. 生成器更加的灵活,可以自动维护自己的状态,自定义迭代器和实现协程

    生成器是一个 函数,不需要自己管理计数

    生成器相关知识总结:

    1. 调用生成器会生成一个 生成器对象,其结构与 迭代器 相似:都有 next() 方法,方法中返回一个 done 属性和 value 属性

    2. 生成器不需要管理 done 状态,可以指定返回 value 的值

    3. 生成器可以提前停止

      1. 使用 return 可以关闭生成器

      2. 使用 throw 可以使生成器短路从而终止运行

        但是如果外部有对异常进行捕获,那么生成器就可以继续运行下去

以上是关于JavaScript 高级程序设计第 7 章 迭代器和生成器 学习笔记的主要内容,如果未能解决你的问题,请参考以下文章

再看《JavaScript高级程序设计》第6-7章

javaScript高级程序设计--第7章函数表达式

JavaScript设计模式与开发实践

《javascript高级程序设计》学习笔记 | 7.2.迭代器模式

《JavaScript高级程序设计(第3版)》笔记-第1章-JavaScript 简介

《JavaScript高级程序设计(第四版)》学习笔记第3章