红宝书学习笔记【第7章】迭代器与生成器

红宝书学习笔记【第7章】迭代器与生成器

1. 理解迭代

for循环的局限性:

  • 迭代之前需要事先知道如何使用数据结构. 例如: 数组每一项都只能通过引用取得数组对象, 在通过下标获取指定索引位置的项. 但这种情况不适用于所有数据结构.
  • 遍历顺序并不是数据结构固有的. 并不是所有数据结构都有递增的下标.

早期es版本中, 执行迭代必须使用循环或其他辅助结构, 导致代码凌乱.

2. 迭代器模式

迭代器模式: 可迭代对象 => 实现了正式的 Iterable 接口, 并且可以通过迭代器 Iterator 消费.

任何实现 Iterable 接口的数据结构, 都可以被实现 Iterator 接口的结构"消费".
迭代器是按需创建的一次性对象. 每个迭代器都会关联一个可迭代对象, 而迭代器会暴露迭代该对象的API.
迭代器无须考虑关联的对象的结构, 只需要知道如何获取连续的值.

2.1 可迭代协议

实现 Iterable 接口(可迭代协议) 要求同时具备:

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

在 es 中意味着, 让对象拥有一个"默认迭代器"的属性, 且键必须是Symbol.iterator.
默认迭代器属性必须引用一个迭代器工厂函数, 每次调用该工厂函数, 都返回一个新的迭代器.

实现了 Iterable 接口的内置类型: 字符串、数组、Map、Set、arguments、NodeList等DOM集合类型.

实际编写代码时, 并不需要显示的调用工厂函数以生成迭代器. 实现可迭代协议的类型, 可直接使用可迭代对象的任何语言特性.
例如: for-of循环、数据解构、扩展操作符、Array.from()、Promise.all()、Promise.race()、yield*操作符…

2.2 迭代器协议

迭代器是一个一次性使用的对象, 用于迭代与其关联的可迭代对象.

迭代器使用next()方法在可迭代对象中遍历数据.
next()成功执行后返回一个 IteratorResult 对象.

IteratorResult 对象包含两个属性: done 和 value.
done是一个布尔值, 表示是否可以再次调用next()获取下一个值.
value包含可迭代对象的下一个值 或 undefined (done 为 true).

let arr = ['foo', 'bar']; // 可迭代对象
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 }

每个迭代器实例互相之间没有关联, 都独立的遍历可迭代对象.
迭代器不和可迭代对象某个时刻的快照绑定, 如果可迭代对象在迭代期间发生了变化, 迭代器也会反映相应的变化.

注: 迭代器维护着一个指向可迭代对象的引用, 因此迭代器会阻止可迭代对象的回收.

2.3 自定义迭代器

与 Iterable 接口类似,任何实现 Iterator 接口的对象都可以作为迭代器使用。
示例:

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

let counter = new Counter(3); 
for (let i of counter) { console.log(i); } 
// 1 
// 2 
// 3 
for (let i of counter) { console.log(i); } 
// 1 
// 2 
// 3

每个以这种方式创建的迭代器也实现了 Iterable 接口. 迭代器的Symbol.iterator属性引用的工厂函数返回相同的迭代器.

let arr = ["foo", "bar", "baz"];
let iter1 = arr[Symbol.iterator]();
console.log(iter1[Symbol.iterator]);  // f values() { [native code] }
let iter2 = iter1[Symbol.iterator]();
iter2 === iter1;    // true

2.4 提前终止迭代器

可通过给迭代器设置return()方法来指定迭代器提前关闭的逻辑.
例如:

  • for-of 循环的break, continue, return 或 throw.
  • 解构操作并未消费所有值.

return()必须返回一个有效的 IteratorResult 对象.

class Counter {
  constructor(limit) {
    this.limit = limit;
  }
  [Symbol.iterator]() {
    next() {
      if (count <= limit) { 
        return { done: false, value: count++ }; 
      } else { 
        return { done: true }; 
      }
    },
    return() {
      console.log("Exiting early");
      return { done: true }
    }
  }
}

let counter1 = new Counter(5); 
for (let i of counter1) { 
  if (i > 2) { break }
  console.log(i); 
}
// 1
// 2
// Exiting early

如果迭代其没有关闭, 则还可以从上次离开的地方继续迭代, 例如数组:

let a = [1, 2, 3, 4, 5];
let iter = a[Symbol.iterator]()
for(let i of iter) {
  console.log(i)
  if(i > 2) { break }
}
// 1
// 2
// 3

for (let i of iter) {
  console.log(i)
}
// 4
// 5

3. 生成器

生成器提供给 js 在一个函数块内暂停和恢复代码执行的能力.

3.1 生成器基础

生成器的形式是函数, 通过函数名称前加*来表示.
箭头函数不能用来定义生成器.

function* generatorFn() {}

let generatorFn = function* () {}

let foo = {
  * generatorFn() {}
}

class Foo {
  * generatorFn() {}
  static * generatorFn() {}
}

*不受两侧空格的影响.

function* generatorFnA() {}
function *generatorFnB() {}
function * generatorFnC() {}

class Foo { 
  *generatorFnD() {} 
  * generatorFnE() {} 
}

生成器函数的用法和迭代器相似:

调用生成器函数会返回一个生成器对象.
生成器对象也实现了Iterator接口, 具有next()方法. 生成器对象一开始处于暂停状态(suspended), 执行next()方法会让生成器开始或恢复执行.
next()方法执行后会返回一个结构如{value: any, done: boolean}的对象.
函数体为空的生成器中间不会停留, 调用next()后会让生成器到达done: true
value 属性默认为undefined, 可通过生成器函数的返回值指定.

function* generatorFn() {}
const g = generatorFn();
console.log(g);    // generatorFn {<suspended>}
console.log(g.next);   // f next() { [native code] }
console.log(g.next())   // { done: true, value: undefined }

// ------
function* generatorFn() {
  return "foo";
}
const g = generatorFn();
console.log(g.next());  // { done: true, value: 'foo' }

生成器函数只会在初次调用next()方法后开始执行

function* generatorFn() {
  console.log("foobar");
}
const g = generatorFn();
g.next();   // foobar

3.2 通过 yield 中断执行

生成器函数再遇到yield之前正常执行, 遇到yield之后执行暂停, 函数作用域的状态被保留, 调用next()方法后, 函数恢复执行.

function* generatorFn() { 
 yield; 
} 
let generatorObject = generatorFn();
console.log(generatorObject.next()); // { done: false, value: undefined }
console.log(generatorObject.next()); // { done: true, value: undefined }

可以通过yield关键字返回数据, 它生成的值会作为next()方法的返回对象的value属性.

function* generatorFn() {
 yield 'foo';
 yield 'bar';
 return 'baz';
}
let generatorObject = generatorFn();
console.log(generatorObject.next()); // { done: false, value: 'foo' }
console.log(generatorObject.next()); // { done: false, value: 'bar' }
console.log(generatorObject.next()); // { done: true, value: 'baz' }

同一生成器函数的不同生成器对象区分作用域, 互不影响.

yield关键字只能在生成器函数内部直接使用.

// 无效
function* invalidGeneratorFnA() { 
  function a() { 
    yield; 
  } 
}
3.2.1 生成器对象作为可迭代对象
function* nTimes(n) {
  while(n--) {
    yield;
  }
}
for(let item of nTimes(3)) {
  console.log("foo")
}
// foo
// foo
// foo
3.2.2 使用 yield 实现输入输出

上一次让生成器函数暂停的 yield 会接收到传给next()方法的第一个值.
注: 第一次调用next()传入的值不会被使用, 因为这一次的调用是为了开始执行生成器函数.

function* generatorFn(initial) {
  console.log(initial);
  console.log(yield);
  console.log(yield);
}
const g = generatorFn("foo")
g.next("bar");  // foo
g.next("bax");  // bax
g.next("qux");  // qux

yield 可以同时用于输入和输出.

function* generatorFn() {
  return yield "foo"
}
const g = generatorFn()
console.log(g.next())   // { done: false, value: "foo" }
console.log(g.next("bar"))   // { done: true, value: "bar" }
3.2.3 产生可迭代对象

用星号增强yield, 让它可以迭代一个可迭代对象, 每次产出一个值.

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

yield* 的值是关联迭代器返回done: true时的 value 属性值.

function* generatorFn() {
  console.log("iter value:", yield* [1, 2, 3]);
}
for(const item of generatorFn()) {
  console.log("value:", item)
}
// value: 1
// value: 2
// value: 3
// iter value: undefined      (数组的默认迭代器在done: true时, value是undefined)

// -----------------

function* innerGeneratorFn() {
 yield 'foo';
 return 'bar';
}
function* outerGeneratorFn(genObj) {
 console.log('iter value:', yield* innerGeneratorFn());
}
for (const x of outerGeneratorFn()) {
 console.log('value:', x);
}
// value: foo
// iter value: bar
3.2.4 使用 yield* 实现递归算法
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

3.3 生成器作为默认迭代器

由于生成器对象实现了 Iterable 接口, 而且生成器函数和默认迭代器被调用后都产生迭代器, 所以可以将生成器作为默认迭代器.

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

3.4 提前终止生成器

类似迭代器, 生成器也支持 “可关闭” 的概念.
实现 Iterator 接口的对象一定有next()方法和可选的return()方法, 生成器还有throw().

return()throw()方法都可以用于强制生成器进入关闭状态.

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>}
console.log(g.return());   // { done: true, value: undefined }
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>}

如果在生成器函数内部处理了这个错误, 那么生成器就不会关闭, 而且可以恢复执行.
错误处理会跳过对应的 yield.

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

const g = generatorFn();
console.log(g.next())   // { done: false, value: 1 }
g.throw()
console.log(g.next())   // { done: false, value: 3 }

注: 如果生成器对象还没有开始执行就调用了throw()方法, 不会在函数内部被捕获, 因为这相当于在函数外部抛出了异常.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值