[JS红宝书笔记]迭代器和生成器

·  阅读 1367
[JS红宝书笔记]迭代器和生成器

序言

这一章节篇幅不长,但是很多关键定义非常重要,需要通过拆分理解和实例验证来更具体的了解这些定义。那么首先抛出本章核心:

迭代器和生成器是 ES6 新增的特性,是用来更为方便、清晰地实现迭代。

MIND

迭代器和生成器

迭代

什么是迭代

迭代是按照顺序反复多次执行一段程序,通常会有明确的终止条件。

提取关键词:“顺序”、“反复多次”、“终止条件”,并通过例子解析:

const arr = [1, 2, 3, 4];
for (let i = 0, len = arr.length; i < len; i++) {
  console.log(i);
}
// 1
// 2
// 3
// 4
复制代码

上面的数组循环就是简单的迭代,而循环是迭代机制的基础,来检查三个关键词:

  • 顺序:迭代会在有序集合上进行,其中“有序”指集合中所有项都可以按照既定的顺序被遍历,特别是开始和结束项有明确的定义。(数组 arr 就是有序集合的最典型的例子)。
  • 反复多次:每次循环都会在下一次迭代开始之前完成。(执行语句 console.log(i) 会依次输出 arr 的元素)
  • 终止条件:循环指定迭代的次数。(for 的循环条件语句 i < len 定义循环次数)

数组迭代的缺陷

  • 迭代前需要事先知道如何使用数据结构。数据结构必须需要满足通过[]取特定索引位置的项
  • 遍历顺序并不是数据结构固有的。不适用于除数组以外的具有隐式顺序的数据结构

ES5的迭代

为了弥补缺陷,ES5 新增了些迭代方法,在上一章节集合引用类型有介绍过

  • forEach()
  • map()
  • some()
  • every()
  • filter() 以上方法无需通过[]取特定索引位置的项,但还是数组原型上的方法,不适用其他数据结构

迭代器

ES6 新增的迭代器模式,可以像 Python、Java 等开发语言通过原生语言结构解决迭代缺陷,让开发者无需事先知道如何迭代就能实现迭代操作。

迭代器模式描述了一种方案:将实现了正式的 Iterable 接口,并且可以通过迭代器 Iterator 消费的结构,称为“可迭代对象(iterable)”。

嗯~,这个解释不太容易理解,拆开来说:

ES6 新增了迭代协议,分为可迭代协议迭代器协议,其中,实现可迭代协议(Iterable 接口)的对象称为可迭代对象,实现迭代器协议的对象称为迭代器,而可迭代对象在迭代过程中会被迭代器“消费”。

下面先从协议来讲起。

可迭代协议

可迭代协议具备两种能力:

  • 支持迭代的自我识别能力
  • 创建实现可迭代协议的对象的能力

首先,为何是这两种能力:

  • 第一点,自我识别,这一点解决 ES6 之前需要事先知道迭代对象的缺陷;
  • 第二点,创建对象,这一点解决 ES6 之前迭代限定于数组的缺陷。

其实这两种能力可以概括为为 JS 对象能够定义或定制它们的迭代行为,来看看如何实现的:

可迭代对象满足可迭代协议,需要在对象或对象原型链上某个对象中暴露默认迭代器属性来定义迭代行为,这个属性用 Symbol.iterator 常量来作为键,并且引用一个迭代器工厂函数来返回一个新的迭代器。

这里的 Symbol.iterator,也叫 @@iterator,在第三章语言基础就出现过,它是一种常用内置符号,用于暴露语言内部行为,并且可以直接访问、重写和模拟这些行为。这些内置符号都以 Symbol 工厂函数字符串属性的形式存在。因此,还可以可以在自定义对象中重新定义 Symbol.iterator 的值,来实现定制迭代行为。

可迭代对象

了解了可迭代协议,来看看那些对象是可迭代对象:

  • 内置可迭代对象
    • String
    • Array
    • TypedArray
    • Map
    • Set
    • 函数的 arguments 对象
    • NodeList 等 DOM 集合类型
  • 需要可迭代对象的语法
    • for-of
    • 解构赋值
    • 扩展运算符
    • yield*
  • 接收可迭代对象的内置 API
    • new Map([iterable])
    • new WeakMap([iterable])
    • new Set([iterable])
    • new WeakSet([iterable])
    • Promise.all(iterable)
    • Promise.race(iterable)
    • Array.from(iterable)
  • 自定义可迭代对象
最后 MDN 提到有格式不佳的可迭代对象,但因为其迭代工厂函数不能反悔迭代器对象,会出现异常。
复制代码

既然可迭代对象实现了可迭代协议,那么来一看究竟

  • 内置可迭代对象
let str = 'JayeZhu';
// 输出默认迭代器属性,返回迭代器工厂函数
console.log(str[Symbol.iterator]); // f values() { [native code] }
// 调用迭代器工厂函数,生成一个新的迭代器
console.log(str[Symbol.iterator]()); // StringIterator {}
复制代码
  • 原生语言结构(需要可迭代对象的语法和接收可迭代对象的内置 API)
let arr = [1, 2, 3]
// 在后台调用提供的可迭代对象的工厂函数,创建迭代器
console.log([...arr]); // [1, 2, 3]
复制代码

自定义可迭代对象需要先了解迭代器协议和迭代器再看看是什么骚操作。

迭代器协议

迭代器协议定义了产生一系列值(无论是有限个还是无线个)的标准方式。当值为有限个时,所有的值都被迭代完毕后,则会返回一个默认返回值。

这种标准方式需要满足迭代器协议的迭代器去实现,而只有实现 next() 方法的对象才能成为迭代器。

那么,来看看这个 next() 的语义是什么:next() 是一个无参函数,返回对象 IteratorReault,该对象拥有 donevalue 两个属性:

  • done,布尔值,false 表示还可以调用 next() 取得下一个值,true 代表“耗尽”
  • value,当 done 为 true 时包含可迭代对象的下一个值,否则显示 undefined

具体场景如下

let arr = [1, 2];
let iter = arr[Symbol.iterator](); // 获取迭代器
// 执行迭代器
console.log(iter.next()); // {value: 1, done: false}
console.log(iter.next()); // {value: 1, done: false}
console.log(iter.next()); // {value: undefined, done: true}
console.log(iter.next()); // {value: undefined, done: true}
复制代码

注意到,通过迭代器的 next() 方法可以按顺序迭代可迭代对象,并返回 IteratorReault。当 IteratorReault 的 done 属性为 true 后,会返回同样的 IteratorReault 对象。

因此,也就解释了,当值是有限的时候,所有值被迭代完毕后,会返回默认返回值,而无线的时候,done 值一直为 false,迭代一直进行下去。

迭代器

了解迭代器协议后,迭代器也该显露真身了。迭代器是一个对象,它定义了一个序列,并在终止时可能返回一个返回值。拥有以下特点:

  • 一次性使用的对象:每次调用迭代器工厂函数会生成新的迭代器对象
  • 关联一个可迭代对象:会阻止垃圾回收程序回收可迭代对象

那么,看看一个简单的迭代器长什么样子:

class Color {
  // 迭代器
  [Symbol.iterator] () {
    return {
    	// next() 函数
      next () {
      	// 返回 IteratorReault
        return { done: false, value: 'cyan' };
      }
    }
  }
}
const color = new Color();
console.log(color[Symbol.iterator]()); // { next: f () {} }
console.log(color[Symbol.iterator]().next()); // {done: false, value: "cyan"}
复制代码

其中 class 代表类,将在下一章中出现

自定义迭代器

为了能够将可迭代对象迭代多次,需要迭代器对象上创建对个迭代器,每一个迭代器对应一个新计数器,再将计数器变量放到闭包中,通过闭包返回迭代器。

class Counter {
  constructor (limit) {
    this.limit = limit;
  }

  [Symbol.iterator] () {
    let count = 1,
      limit = this.limit;
    retrun  {
      next () {
        if (count <= limit) {
          return { done: false, value: count++ };
        } else {
          return { done: true, value: undefined };
        }
      }
    }
  }
}
复制代码

提前终止迭代器

迭代器使用可选的 return() 方法可以让迭代器提前关闭执行。应用场景包括:

  • for-of 循环通过 break\continue\return\throw 等提前退出
  • 解构赋值并未消费所有值 将自定义迭代器将入 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 () {
      	console.log('迭代器关闭前执行操作');
        return { done: true };
      }
    }
  }
}

let counter = new Counter (5);

for (let i of counter) {
  if (i > 2) break;
  console.log(i);
}
// 1
// 2
// 迭代器关闭前执行操作
复制代码

注意:

  • 没有设定 return() 关闭迭代器或迭代器不能关闭的情况下,迭代退出后还可从上次离开的地方继续迭代
  • 设定了 return() 的迭代器不会强制进入关闭状态,但 return() 会被调用

生成器

生成器是一个能够函数内暂停和恢复执行的结构。

其形式是一个函数(第十章详解),并且在函数面前加一个星号(*)。除了箭头函数,其他函数可以定义生成器函数。

生成器基础

  • 声明带 *,星号两边空格不影响
  • 调用生成器函数会产生生成器对象
  • 生成器对象一开始处于暂停状态 suspended
  • 生成器对象拥有和迭代器相似 的 next() 方法
  • 生成器函数只会在初次调用 next() 方法后开始执行
  • 生成器函数实现了可迭代协议,默认迭代器是自引用的

上手写一个

let generatorFn = function* () {
  console.log('执行');
  return 'generator'
};

const g  = generatorFn();
console.log(g); // generatiorFn {<suspended>}
console.log(g.next);// ƒ next() { [native code] }
console.log(g.next());
// 执行
// {value: "generator", done: true}

console.log(generatorFn);
/* ƒ* () {
	console.log('执行');
    return 'generator'
}*/
console.log(generatorFn()[Symbol.iterator]); // ƒ [Symbol.iterator]() { [native code] }
console.log(generatorFn()); // generatorFn {<suspended>}
console.log(generatorFn()[Symbol.iterator]()); // generatorFn {<suspended>}
复制代码

yield

回顾一下生成器的概念:可以在函数内部暂停和恢复的结构。这个内部暂停和回复就是通过 yield 关键字做到的,因此,yield 是生成器中最有用的地方。

yield 的特性是:

  • yield 只能在生成器内部使用,其他地方会报错
  • 生成器函数遇到 yield 之前会正常执行
  • 遇到之后,执行会停止,函数作用域会被保留
  • 通过生成器对象调用 next() 恢复 yield 暂停的生成器执行
  • yield 将生成的值返回给 next() 返回的对象里
  • 直到遇到 return,生成器退出,并处于 done: true 状态
function* generatorFn() {
  console.log('generator');
  yield '1';
  yield '2';
  return 'return';
}
const g = generatorFn();

console.log(g.next());
// generator
// {value: "1", done: false}
console.log(g.next()); // {value: "2", done: false}
console.log(g.next()); // {value: "return", done: true}
复制代码

那么这种机制能够起到什么作用呢?

生成器对象作为迭代器对象

使用生成器对象生成自定义迭代对象更加方便,避免因为控制循环而需要使用不太建议的闭包情况

function* nTimes (n) {
  while (n--) {
    yield;
  }
}
for (let time of nTimes(3)) {
  console.log(`time:${time}`);
}
// time:undefined
// time:undefined
// time:undefined
复制代码

yield 实现输入和输出

yield 作为函数的中间参数使用,上一次的 yield 会接收到当前 next() 方法的第一个值,但第一次调用的 next() 仅仅作为开始执行生成器函数,因此第一次 next() 的第一个值不会被 yield 接收使用。

function* generatorFn (value) {
	console.log(value);
	console.log(yield);
	console.log(yield);
}
const g = generatorFn(100);
g.next(1); // 100 => value,第一次不接收
g.next(2); // 2
g.next(3); // 3
复制代码

借此,将上面迭代器对象进行修改,实现输入输出

function* nTimes (n) {
  let i = 0;
  while (n--) {
    yeild i++;
  }
}
for (let time of nTimes(3)) {
  console.log(`time:${time}`);
}
// time:0
// time:1
// time:2
复制代码

产生可迭代对象

yield 同样可以使用 *,作用是用来增强 yield,让它能够迭代一个可迭代对象,从而产生一个值。

因此,可以把上面例子再优化

function* nTimes (n) {
  yield* Array.from({ length: n }, (x, i) => i);
}
for (let time of nTimes(3)) {
  console.log(`time:${time}`);
}
// time:0
// time:1
// time:2
复制代码

yield* 实现递归

这一点是 yield* 最有用的地方,能够实现递归的原因是生成器可以产生自己

再将上例进行改造

function* nTimes (n) {
  if (n > 0) {
    yield* nTimes(n - 1);
    yield n -1;
  }
}
for (let time of nTimes(3)) {
  console.log(`time:${time}`);
}
// time:0
// time:1
// time:2
复制代码

这里的 yield* nTimes(n - 1) 每次都会生成一个新的可迭代对象,而这个生成的可迭代对象是自己,就会继续迭代,并且产生一个 yield* 产出的整数。这样就相当于创建了一个可迭代对象并返回递增的整数

生成器作为默认迭代器

因为 yield* 可以产生可迭代对象,因此实现了可迭代协议,很适合作为默认迭代器。

因此可以将上文的自定义迭代器修改为更简洁的生成器版本

class Counter {
  constructor (limit) {
    this.values = Array.from({ length: n }, (x, i) => i);
  }

  * [Symbol.iterator] () {
    yield* this.values;
    }
  }
}

const counter = new Counter(3);
复制代码

提前终止生成器

生成器都可以作为默认迭代器了,那么一定也支持提前关闭。生成器除了 return() 一种终止方法,还支持 throw() 方法强制生成器进入关闭状态。

阮一峰老师 ES6 系列中写明迭代器也有 throw 方法,但是是搭配 Generator 使用的
复制代码

return()

生成器提供的 return() 方法相比于迭代器的有很大不同:

  • 可选:生成器都有 return() 方法,而迭代器是可选的
  • 强制:生成器的 return() 方法会强制进入关闭状态,而迭代器不会强制,但方法会执行
  • 传值:生成器的 return() 方法可以传值,且该值是终止迭代器对象的值,而迭代器必须返回一个对象
function* generatorFn() {
  for (const x of [1, 2, 3]) {
    yield x;
  }
}

const g1 = generatorFn();
console.log(g1); // generatorFn {<suspended>}
console.log(g.return(4)); // { done: true, value: 4 }
console.log(g); // generatorFn {<closed>}

// for-of 循环会忽略状态为 done: true 的迭代器对象内部返回的值
const g2 = generatorFn();
for (const x of g2) {
  if (x > 1) g.return(4);
  console.log(x);
}
// 1
// 2
复制代码

throw()

该方法会将一个提供的错误注入到生成器对象中。

如果错误未被处理,生成器就会关闭。

function* generatorFn () {
  for (const x of [1, 2, 3]) {
  yield x;
}

const g = generatorFn();
console.log(g); // generatorFn {<suspended>}
try {
  g.throw('err');
} catch (e) {
  console.log(e); // "err"
}
console.log(g); // generatorFn {<closed>}
复制代码

如果生成器函数内部处理了这个错误,生成器不但不会关闭,还会恢复执行。

function* generatorFn () {
  for (const x of [1, 2, 3]) {
    try {
      yield x;
    } catch (e) {}
  }
}

const g = generatorFn();
console.log(g.next()); // { done: false, value: 1 }
g.throw('err');
console.log(g.next()); // { done: false, value: 2 }
复制代码

错误处理跳过对应的 yield,因此会跳过 2,并且后续会恢复执行。

总结

ES6 新增的迭代器和生成器可以很好地解决 ES6 之前的迭代问题。

实现可迭代协议的可迭代对象,在迭代过程中,会被实现迭代器协议的迭代器“消费”。

可迭代对象拥有默认迭代器属性 Symbol.iterator,该属性引用会返回迭代器对象的迭代器工厂函数。

迭代器通过连续调用 next() 获取连续值,返回 IteratorrObject,包含 done 和 value 两个属性。

迭代器通过可选的 return() 执行终止前的操作,但不一定会终止迭代。

生成器是特殊函数,调用后返回生成器对象。这种对象实现可迭代协议,可作为默认迭代器。

生成器对象支持 yield 能够暂停生成器函数,还能够做很多输入输出和递归等事情。

所有生成器对象有 return() 方法关闭迭代器,并返回值。还有 throw() 方法处理异常。

往期文章

如果觉得文章不错或者有帮助的话,希望能获得一个点赞(砰砰砰),觉得有问题的话,也希望提出改进建议,谢谢大家!

分类:
前端
标签:
收藏成功!
已添加到「」, 点击更改