JavaScript iterators and generators:迭代器和生成器

随着ES6的引入,迭代器和生成器已经正式添加到JavaScript中。

迭代器能让你遍历任何符合规范的对象。在文章的第一部分,我们会探讨如何使用迭代器,以及如何让任意对象变得可迭代。
这篇博客的第二部分将完全聚焦于生成器(Generators):它们是什么、如何使用,以及在哪些场景下能派上用场。
我一向喜欢探究事物背后的运行原理。之前我写过一个系列,讲解了 JavaScript 在浏览器中是如何工作的。作为延续,本文就来深入剖析一下 JavaScript 中迭代器和生成器的底层机制。
一、什么是迭代器?
在深入理解生成器之前,我们必须先透彻掌握 JavaScript 中的迭代器,因为这两个概念是密不可分的。读完本节后,你就会明白:生成器本质上只是一种更安全、更便捷的编写迭代器的方式。
顾名思义,迭代器允许你遍历一个对象(数组本质上也是对象)。
你很可能已经用过 JavaScript 迭代器了。比如,每次你遍历数组时,其实就是在使用迭代器。除此之外,你还能遍历 Map 对象,甚至字符串。
for (let i of 'abc') {
  console.log(i);
}

// Output
// "a"
// "b"
// "c"
任何实现了可迭代协议(iterable protocol)的对象,都可以通过 for...of 循环来进行遍历。
如果我们往深了挖一挖,可以通过实现 @@iterator 函数(即 Symbol.iterator)来让任意对象变得可迭代,这个函数的作用是返回一个迭代器对象
二、使任何对象都可迭代
要透彻理解这一点,最好的方式就是来看一个具体的例子,演示如何让一个普通的对象变得可迭代。
我们先从一个包含用户姓名且按城市分组的对象开始:
 
const userNamesGroupedByLocation = {
  Tokio: [
    'Aiko',
    'Chizu',
    'Fushigi',
  ],
  'Buenos Aires': [
    'Santiago',
    'Valentina',
    'Lola',
  ],
  'Saint Petersburg': [
    'Sonja',
    'Dunja',
    'Iwan',
    'Tanja',
  ],
};
我之所以选这个例子,是因为如果数据是这种结构,想要遍历所有用户会非常麻烦;我们通常需要写多重循环才能把所有用户都取出来。
如果我们尝试直接遍历这个对象,就会得到下面这个错误信息:
 Uncaught ReferenceError: iterator is not defined

要让这个对象变得可迭代,我们首先得给它加上那个 @@iterator 函数。在代码里,我们是通过 Symbol.iterator 这个符号来访问它的。

userNamesGroupedByLocation[Symbol.iterator] = function() {
  // ...
}

正如我之前提到的,迭代器函数会返回一个迭代器对象。这个对象里包含一个 next 方法,该方法在被调用时也会返回一个对象,这个对象包含两个属性:donevalue 

userNamesGroupedByLocation[Symbol.iterator] = function() {
  return {
    next: () => {
      return {
        done: true,
        value: 'hi',
      };
    },
  };
}
这里的 value 包含了当前迭代的值,而 done 则是一个布尔值,用来告诉我们迭代是否已经结束了。
在实现这个函数时,我们需要格外地小心处理 done 的值,因为如果它一直返回 false,就会导致死循环。
上面的代码示例已经正确实现了可迭代协议。我们可以通过调用迭代器对象的 next 函数来测试一下:

 

// 调用迭代器函数返回迭代器对象
const iterator = userNamesGroupedByLocation[Symbol.iterator]();
console.log(iterator.next().value);
// "hi"

其实,用 for...of 循环来遍历对象,其底层原理就是调用 next 函数。 

 总结:


const userNamesGroupedByLocation = {
  Tokio: [
    'Aiko',
    'Chizu',
    'Fushigi',
  ],
  'Buenos Aires': [
    'Santiago',
    'Valentina',
    'Lola',
  ],
  'Saint Petersburg': [
    'Sonja',
    'Dunja',
    'Iwan',
    'Tanja',
  ],
};

userNamesGroupedByLocation[Symbol.iterator] = function() {
  return {
    next: () => {
      return {
        done: true,
        value: 'hi',
      };
    },
  };
}

// Calling the iterator function returns the iterator object
const iterator = userNamesGroupedByLocation[Symbol.iterator]();
console.log(iterator.next().value);
// "hi"

运行结果如下:

image 

如果这样写的话,用 for...of 循环是遍历不出任何东西的,因为我们直接把 done 设成了 false(导致循环立即结束)。而且,用这种方式实现的话,我们也根本拿不到任何用户名。这恰恰就是我们一开始想要让这个对象变得可迭代的原因——为了能真正拿到数据。

 三、实现迭代器
 
 
首先,我们需要获取对象的键,也就是那些代表城市的名字。我们可以通过对 this 关键字调用 Object.keys 来实现,这里的 this 指向该函数的父级对象,在本例中就是 userNamesGroupedByLocation 对象。
只有当我们使用 function 关键字来定义这个可迭代函数时,才能通过 this 访问到这些键。如果我们用了箭头函数,这招就行不通了,因为箭头函数会继承其父级的作用域,导致 this 指向不对。
const cityKeys = Object.keys(this);
我们还需要两个变量来追踪当前的迭代状态。
let cityIndex = 0;
let userIndex = 0;
我们把这些变量定义在 iterator 函数里,但处于 next 函数之外。这样做能让我们在多次迭代之间保留这些数据。 
next 函数里,我们首先需要利用之前定义的索引,取出当前城市对应的用户数组以及当前具体的用户。
我们可以利用这些数据来调整现在的返回值了。
 
 
return {
  next: () => {
    const users = this[cityKeys[cityIndex]];
    const user = users[userIndex];

    return {
      done: false,
      value: user,        
    };
  },
};
 
 
 
 
posted @ 2026-03-11 19:41  chenlight  阅读(1)  评论(0)    收藏  举报