【重走JavaScript之高级程序设计】迭代器与生成器

1 迭代器

  1. 什么是迭代?

    答:按照顺序反复多次执行一段程序,通常有明确的终止条件

  2. JavaScript为什么需要像Python、Java、C++新增了迭代器模式

    答:需要事先知道如何使用数据结构,遍历顺序并不是数据结构固有的(比如对象的属性是无序的)

  3. 什么是迭代器模式?

    答:特定结构的可迭代对象,对外暴露了Iterable(可迭代对象)接口,而且可以通过Iterator(迭代器)消费

1.1 可迭代协议

ECMA规定,暴露一个属性作为迭代器,这个属性必须使用特殊的Symbol.iterator作为key。

以下内置类型都实现了Iterable接口,1.字符串、2.数组、3.映射、4.集合、5.arguments对象、6.NodeList等Dom集合类型

以下代码仅演示和理解迭代器,实际开发不需要这样操作。

let obj = {};
let str = "abc";
let arr = ["a", "b", "c"];
let map = new Map().set("a", 1).set("b", 2);
let set = new Set().add("a").add("b");
let els = document.querySelectorAll("div");

// 未实现Iterable接口
console.log(obj[Symbol.iterator]); // undefiend

// 实现了Iterbale接口,通过了Iterable接口得到了一个函数,这个函数就是迭代器函数
console.log(str[Symbol.iterator]); // ƒ [Symbol.iterator]() { [native code] }
console.log(arr[Symbol.iterator]); // ƒ values() { [native code] }
console.log(map[Symbol.iterator]); // ƒ entries() { [native code] }
console.log(set[Symbol.iterator]); // ƒ values() { [native code] }
console.log(els[Symbol.iterator]); // ƒ values() { [native code] }

// 调用迭代器函数,得到迭代器
console.log(str[Symbol.iterator]); // ƒ [Symbol.iterator]() { [native code] }
console.log(arr[Symbol.iterator]); // ƒ values() { [native code] }
console.log(map[Symbol.iterator]); // ƒ entries() { [native code] }
console.log(set[Symbol.iterator]); // ƒ values() { [native code] }
console.log(els[Symbol.iterator]); // ƒ values() { [native code] }

以下方法可以直接使用上面这些可迭代对象,1.for-of、2.数组解构、3.扩展运算符、4.Array.from()、5.创建集合、6.创建集合和映射、7.Promise.all()和Promise.race()、8.在generator生成器中使用yield*操作符、

let arr = ["a", "b", "c"];
// for-of循环
for (const el of arr) {
  console.log(el);
} // a  b  c
// 数组解构
let [x, y, z] = arr;
console.log(x, y, z); // a b c
// 扩展运算符
let arr2 = [...arr];
console.log(arr2); // ['a', 'b', 'c']
// Array.from()
let arr3 = Array.from(arr);
console.log(arr3); // ['a', 'b', 'c']
// 集合和映射
let set = new Set(arr);
console.log(set); // Set(3) {'a', 'b', 'c'}
let pairs = arr.map((x, i) => [x, i]);
let map = new Map(pairs);
console.log(map); // Map(3) {'a' => 0, 'b' => 1, 'c' => 2}

1.2 迭代器协议

迭代器是一种一次性使用的对象。迭代器API使用next()方法,返回一个IterableResult对象,包含两个属性键名为done和value。

let arr = ["a", "b"]; // 可迭代对象
console.log(arr[Symbol.iterator]); // 迭代器工厂函数,ƒ values() { [native code] }
let iterFirst = arr[Symbol.iterator](); // 调用工厂函数生成的第一个迭代器
let iterSecond = arr[Symbol.iterator](); // 生成的第二个迭代器

console.log(iterFirst.next()); // {value: 'a', done: false}
console.log(iterSecond.next()); // {value: 'a', done: false}

console.log(iterFirst.next()); // {value: 'b', done: false}
console.log(iterSecond.next()); // {value: 'b', done: false}

console.log(iterSecond.next()); // {value: undefined, done: true}
console.log(iterSecond.next()); // {value: undefined, done: true}
console.log(iterSecond.next()); // {value: undefined, done: true}

根据以上代码可知

  1. 不同迭代器之间不会相互影响,可以独立的迭代对象
  2. done为一个布尔值只要done为false,则可以继续调用next()方法。value为迭代器返回的当前值。
  3. 当done为true时,后续调用next()value为undefiend

1.3 自定义迭代器

自定义封装一个迭代器,实现反复计数功能

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

  [Symbol.iterator]() {
    // 定义[Symbol.iterator]方法,
    let count = 1, // 这里通过闭包达到一个效果,即使同时创建多个迭代器时也保证计算器count从1 开始(闭包作用之一,避免全局变量count被污染)
      limit = this.limit;
    return {
      next() {
        // 对迭代器使用next()方法
        if (count <= limit) {
          return { done: false, value: count++ };
        } else {
          return { done: true, value: undefined };
        }
      }
    };
  }
}

let counter = new Counter(5);
for (const el of counter) {
  console.log(el);
} // 1 2 3 4 5

1.4 提前终止迭代器

ES5中提出的forEach是没有终止的(除了trycatch),return()方法可以提前终止迭代。

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


2 生成器

  1. 什么是生成器?

    生成器本质是函数,在函数名称前加一个星号(*)表示为生成器,星号不受两侧空格的影响。调用之后返回一个生成器对象。

  2. 生成器有什么用?

    生成器是ES6新增的结构,拥有在一个函数块内暂停和恢复代码执行的能力。生成器可以自定义迭代器。

2.1 生成器函数的定义

function* generatorFn() {}
let generatorFn = function* () {};
let foo = {
  *generatorFn() {}
};
class Foo {
  *generatorFn() {}
}
class Bar {
  static *generatorFn() {}
}

生成器的调用与执行

function* generatorFn() {
  console.log("foo");
  return "foo";
}
const result = generatorFn();
console.log(result); // generatorFn {<suspended>},调用生成器函数产生一个生成器对象(暂停执行),直接调用生成器函数并不会执行内部代码
console.log(result.next); // ƒ next() { [native code] },和迭代器一样有next方法
console.log(result.next()); // 控制台打印foo  {value: 'foo', done: true},生成器函数只会在初次调用next()方法开始执行,执行内部代码,返回结果类似迭代器
console.log(result.next()); // {value: undefined, done: true},

2.2 yield关键字中断执行

yield关键字可以控制生成器的停止和开始执行。遇到这个关键字,执行停止。停止后只能通过next()方法恢复执行。

function* generatorFn() {
  console.log("a");
  yield "foo";
  console.log("b");
  yield "bar";
  console.log("c");
  return "baz";
  console.log("d");
}
const result = generatorFn();
console.log(result.next()); // a {value: 'a', done: false}  遇到yield关键字退出的生成器函数会处在done:false状态。
console.log(result.next()); // b {value: 'b', done: false}
console.log(result.next()); // c {value: 'foo', done: true} 遇到return关键字退出的生成器函数会处在done:true状态。
console.log(result.next()); //   {value: undefined, done: true} done的状态已经转变为true,所以字符串d并不会打印

yield关键字使用的注意点

  1. 和迭代器一样,调用同一个生成器函数生成多个生成器对象,各个对象之间不会影响
  2. yield关键字只允许直接在生成器函数内部使用,并且不允许嵌套层级。否在会无效

直接显式的调用next()的方法用处不大,如果把生成器对象当成可迭代对象,使用起来会更方便。

function* generatorFn() {
  yield 1;
  yield 2;
  yield 3;
}

for (const iterator of generatorFn()) {
  console.log(iterator); // 1 2 3
}

2.3 使用yield实现输入和输出

生成器传递参数

function* generatorFn(init) {
  console.log(init);
  console.log(yield);
  console.log(yield);
}

let result = generatorFn("foo");

result.next("bar"); // foo  第一次调用next传入的值不会被使用,因为这次调用是为了开始执行生成器函数
// result.next()  // foo  第一次调用next不传参返回的值和上面代码一样
result.next("baz"); // baz
result.next("qux"); // qux
// 迭代索引
function* nTimes(n) {
  for (let i = 0; i < n; i++) {
    yield i;
  }
}

for (const iterator of nTimes(3)) {
  console.log(iterator); // 0 1 2
}

// 实现范围输出
function* range(start, end) {
  while (end > start) {
    yield start++;
  }
}

for (const iterator of range(4, 8)) {
  console.log(iterator); // 4 5 6 7
}

// 实现填充数组
function* fill(n) {
  while (n--) {
    yield 0;
  }
}

console.log(Array.from(fill(8))); // [0, 0, 0, 0, 0, 0, 0, 0]

2.4 使用yield*可以一次性迭代可迭代对象

yield* 本质是语法糖,yield*本质上将一个可迭代对象序列化为一连串可以单独产出的值,所以等同于把yield放入循环。

// 这种写法可以直接用 yield* 来代替
// function* generatorFn() {
//     for (const iterator of [1, 2, 3]) {
//         yield iterator
//     }
// }

function* generatorFn() {
  yield* [1, 2, 3];
  yield* [4, 5];
  yield* [6, 7];
}

let result = generatorFn();

for (const iterator of generatorFn()) {
  console.log(iterator); // 1,2,3,4,5,6,7
}

yield*关联迭代器返回done:true的value属性,对普通迭代器这个值为undefined。而对于生成器函数来说,这个值就是生成器函数返回的值。

function* generatorFn() {
  console.log("iter value", yield* [1, 2, 3]); // yield* 关联普通迭代器
}

for (const iterator of generatorFn()) {
  console.log("value", iterator);
}
/* 
    value 1
    value 2
    value 3
    iter value undefined
*/

function* innerGeneratorFn() {
  yield "foo";
  return "bar";
}

function* outerGeneratorFn() {
  console.log("iter value:", yield* innerGeneratorFn()); // yield* 关联生成器函数
}

for (const iterator of outerGeneratorFn()) {
  console.log("value", iterator);
}
/* 
    value foo
    iter value: bar
*/

使用yield*实现递归算法

function* nTimes(n) {
  if (n > 0) {
    yield* nTimes(n - 1);
    yield n - 1;
  }
}

for (const iterator of nTimes(3)) {
  console.log(iterator); // 0 1 2
}

2.5 生成器作为默认迭代器

for-of调用了默认迭代器(它恰好又是一个生成器函数)并产生了一个生成器对象。这个生成器对象是可迭代的,所以完全可以在迭代中使用。

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

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

2.6 提前终止生成器

与迭代器类似,生成器也支持可关闭的概念。一个实现Iterator接口的对象一定有next()方法,还有一个可选的return()方法用于提前终止迭代器。除了以上方法还有throw()

2.6.1 return()

return()方法会强制生成器进入关闭状态。return 传入的值就是终止迭代器对象的值

function* generatorFn() {
  yield* [1, 2, 3];
}

const result = generatorFn();

console.log(result); // generatorFn {<suspended>}
console.log(result.return(4)); // {value: 4, done: true}
console.log(result); // generatorFn {<closed>}
console.log(result.next()); // {value: undefined, done: true}
console.log(result.next()); // {value: undefined, done: true}
console.log(result.next()); // {value: undefined, done: true}

2.6.2 throw()

throw()方法会在暂停的时候将一个提供的错误注入到生成器对象中。如果错误未被处理,生成器就会关闭。

function* generatorFn() {
  yield* [1, 2, 3];
}

const result = generatorFn();

console.log(result); // generatorFn {<suspended>}
try {
  result.throw("error");
} catch (error) {
  console.log(error); // error
}
console.log(result); // generatorFn {<closed>}

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

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

const result = generatorFn();

console.log(result.next()); // {value: 1, done: false}
result.throw("foo");
console.log(result.next()); // {value: 3, done: false}


3 实战

3.1 遍历树形结构

// 遍历树形数据结构时,生成器函数能够极大地简化代码。
function* treeRecursion(tree) {
  yield tree.value;
  if (tree.children) {
    for (const child of tree.children) {
      yield* treeRecursion(child);
    }
  }
}

const tree = {
  value: "root",
  children: [{ value: "children1" }, { value: "children2", children: [{ value: "grandchild" }] }]
};

for (const value of treeRecursion(tree)) {
  console.log(value);
}

3.2 控制流

// 生成器可以用于直观地处理复杂的控制流。
function* flowControlController() {
  const first = yield "Pls finish First Step";
  console.log(first);

  const second = yield "Pls finish Second Step";
  console.log(second);

  return "Done";
}

const controller = flowControlController();

console.log(controller.next()); // { value: 'Pls finish First Step', done: false }
console.log(controller.next("First Step")); // First Step
console.log(controller.next("Second Step")); // Second Step

3.3 暂停和恢复代码

// 在复杂的异步逻辑中,生成器允许我们暂停和恢复代码的执行,特别是在等待异步操作完成时。
function* asyncDataFetcher() {
  const data = yield fetchDataFromAPI();
  console.log(data);
}

const fetcher = asyncDataFetcher();
fetcher.next().value.then(data => {
  fetcher.next(data);
});

3.4 无限生成序列

// 生成器函数可以创建无限的序列,而不需要存储整个序列。
function* infiniteSequence() {
  let i = 0;
  while (true) {
    yield i++;
  }
}

const sequence = infiniteSequence();
console.log(sequence.next().value); // 0
console.log(sequence.next().value); // 1
// ... 无限执行下去,每次调用返回一个新值

3.5 搭建通信管道

// 生成器可以实现两个函数间的双向通信。
function* generatorA() {
  const valueFromB = yield "Data from A";
  console.log("Received data from B:", valueFromB);
}

function* generatorB(genA) {
  const valueFromA = genA.next().value;
  genA.next("Data from B");
  console.log("Received data from A:", valueFromA);
}

const genA = generatorA();
const genB = generatorB(genA);

genB.next();

3.6 批量处理任务

// 对于处理数据的任务可以分批次进行,以节省内存占用。
function* batchProcessor(batchSize) {
  let data = null;
  while ((data = fetchData(batchSize))) {
    yield processBatch(data);
  }
}

const processor = batchProcessor(100);
let processedData = processor.next();
while (!processedData.done) {
  console.log("Processed batch:", processedData.value);
  processedData = processor.next();
}

3.7 生成唯一ID

// 生成器可以用于生成唯一ID序列。
function* uniqueIdGenerator() {
  let id = Date.now();
  while (true) {
    yield id++;
  }
}

const ids = uniqueIdGenerator();
console.log(ids.next().value); // 一个基于时间戳的ID
console.log(ids.next().value); // 下一个ID
posted @   wanglei1900  阅读(49)  评论(0编辑  收藏  举报
编辑推荐:
· AI与.NET技术实操系列:基于图像分类模型对图像进行分类
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 25岁的心里话
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示