迭代器(Iterator)与生成器(Generator)

287 阅读8分钟

概念

  • 迭代器以及迭代器对象
  • 迭代协议
  • 可迭代对象
  • 生成器、生成器函数以及生成器对象
  • 名称归纳
  • 应用场景

迭代器(Iterator)

迭代器(Iterator)是一种访问集合内元素的方式,它提供了一种按序访问集合内各个元素的方法,让我们可以方便地遍历集合内所有的元素

迭代器对象

迭代器对象是一个表示可迭代对象的迭代器的对象,它包括了 next() 方法,用于按序遍历可迭代对象中的每一个值。迭代器对象的 next() 方法返回一个具有 value 和 done 两个属性的对象,用于表示可迭代对象中的每一个值。done 属性为 boolean 类型,表示是否已经到达序列的末尾。

迭代器的用法:

// 迭代器的常见用法
let a = [1, 2, 3];
let it = a[Symbol.iterator](); // 获得迭代器对象
console.log(it.next()); // { value: 1, done: false }
console.log(it.next()); // { value: 2, done: false }
console.log(it.next()); // { value: 3, done: false }
console.log(it.next()); // { value: undefined, done: true }

迭代协议

在 JavaScript 中,要成为迭代器,必须遵循 Iterator 协议。Iterator 协议是一个包含 next() 方法和 done 属性的对象规范:

  • next() 方法用于返回序列中的下一个值。它返回的结果是一个包含两个属性的对象,即 value 和 done。

    • value:表示当前迭代出的值。
    • done:表示迭代是否完成。
  • 如果已经完成迭代操作,则 next() 方法需要返回 { done: true }

  • 如果迭代仍然进行中,则 next() 方法需要返回一个包含当前迭代出的值的对象,以及 done=false

自定义迭代器

我们也可以自己定义一个迭代器,只需要实现 Iterator 协议即可:

// 自定义迭代器
let it = {
  // 实现Iterator协议,必须有 next 方法
  next:function(){
      // 返回对象,必须包含 value 和 done 
    return {
      // 表示迭代后得到的结果
      value:"Hello",
      // 返回 false 表示迭代没有结束;返回 true 表示迭代已经结束
      done:false
    }
  }
};
console.log(it.next().value);
console.log(it.next().done);

可迭代对象(Iterable)

可迭代对象是指可以被迭代的对象,具有 Symbol.iterator 属性并返回迭代器的对象,例如数组、字符串、集合、生成器函数等,这些对象可以被遍历,并且可以使用 for...of 循环语句进行迭代

Symbol.iterator

Symbol.iterator 是一种内置的符号值,可以作为一个属性键存在于任何对象中,当一个对象的 Symbol.iterator 属性被执行时,该函数必须返回一个迭代器对象。

我们可以使用以下代码实现一个可迭代对象,用于迭代从 0 开始的斐波那契数列:

const fibonacci = {
  [Symbol.iterator]() {
    let [prev, curr] = [0, 1];
    return {
      next() {
        [prev, curr] = [curr, prev + curr];
        return { value: curr, done: false };
      }
    };
  }
}; // 经过Symbol.iterator方法对fibonacci实现迭代协议,fibonacci即是一个可迭代对象

for (const num of fibonacci) {
  if (num > 1000) break;
  console.log(num); // 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987
}

在 JavaScript 中,可迭代对象必须实现迭代协议。

可迭代对象的特点

  1. 惰性求值:可迭代对象的迭代器对象是惰性求值的,也就是说当迭代器对象的 next() 方法被调用时才会计算下一个值,这样可以避免一次性处理大量数据时产生的性能问题。
  2. 可遍历性:可迭代对象可以被多次遍历,每次遍历时都会从序列的第一个元素开始进行处理。这对于需要对同一个数据集进行多次操作的场景非常有用。

生成器(Generator)

生成器(Generator)是一种在需要时生成多个值的函数,生成器函数可以暂停(yield)并恢复(next)执行过程。每次调用生成器函数的 next() 方法都会执行生成器函数中的代码,执行到一个 yield 表达式时将返回一个包含当前值的对象,等待下一次调用 next() 方法继续执行。如果执行到函数结尾,则返回包含 undefinedtrue 属性的对象,表示生成器函数结束。

生成器函数

生成器函数(Generator Function)是一种创建生成器的函数,返回一个生成器对象。使用 function* 关键字来定义,里面可以包含一个或多个 yield 关键字,用于返回序列中的下一个值。生成器函数可以被看做是一种能够自动暂停和从暂停中恢复执行的函数,序列的元素可以通过逐个调用生成器函数的 next() 方法来获取,每次调用 next() 方法都会返回当前序列中的下一个值和一个表示序列是否已经迭代完毕的标志符。

生成器函数的定义格式如下:

function* 函数名([参数]) {
  yield 值;
}

生成器对象

生成器对象是由生成器函数返回的。它是生成器函数的实例,同时也是一个迭代器,因此可以使用迭代器协议进行遍历。

在生成器函数内部,yield 关键字用来定义序列的下一个值,生成器函数执行到 yield 语句时,会自动将当前函数的执行进度保存下来(即函数的上下文),并返回 yield 关键字后面指定的值,等待下一次调用 next() 方法时再从之前的位置继续执行,直到函数内部执行完毕或返回了 return 语句。示例如下:

function* count(start, end) {
  for (let i = start; i <= end; i++) {
    yield i;
  }
}

const generator = count(1, 3);
console.log(generator.next());  // { value: 1, done: false }
console.log(generator.next());  // { value: 2, done: false }
console.log(generator.next());  // { value: 3, done: false }
console.log(generator.next());  // { value: undefined, done: true }

在上述代码中,我们先通过调用 count(1, 3) 创建了一个生成器对象,然后依次调用了 next() 方法,从序列中依次取出了值 1、2 和 3。最后一次调用 next() 方法返回的对象的 done 属性为 true,表示该序列已经迭代完毕。

生成器函数的主要特点如下:

  1. 可迭代:生成器函数可以创建一个可迭代的序列,让我们可以在函数内部返回一系列值,而不是在一次性返回。
  2. 自动保存上下文:生成器函数会自动保存函数当前的执行状态,使得它可以智能的暂停和恢复迭代。
  3. 支持异步操作:生成器函数可以与 Promise 一起使用来实现异步操作,并消除掉回调地狱的问题。
  4. 惰性计算:生成器函数是惰性计算的,只会在需要时才会产生序列中的下一个值,避免一次性计算大量的数据。

可迭代对象与生成器函数的关系

可迭代对象和生成器函数之间有很密切的关系,生成器函数可以生成一个可迭代的对象,而且这个对象是惰性计算的(lazy),也就是说只有在被需要的时候才会生成序列中的下一个值。

换句话说,生成器函数创建了一个内部迭代器,这个迭代器可以遍历函数体内部的值序列,并返回序列中下一个值。这个迭代器对象同时也是一个可迭代对象,因为它实现了遍历器(Iterator)接口,具有 Symbol.iterator 属性和 next() 方法。因此,我们可以直接用 for...of 循环或者扩展操作符 ... 来处理迭代器对象。

以下是一个用生成器函数创建的可迭代对象示例:

function* colors() {
  yield 'red';
  yield 'green';
  yield 'blue';
}

const iterable = colors();

for (let color of iterable) {
  console.log(color);
}
// 输出:
// red
// green
// blue

在上述代码中,我们通过 colors() 函数创建了一个迭代器对象 iterable,然后通过 for...of 循环遍历该对象,输出其中的每个元素。

相关名称的归纳

名称含义作用
迭代器(Iterator)迭代器是一种访问集合内元素的方式它提供了一种按序访问集合内各个元素的方法,让我们可以方便地遍历集合内所有的元素
迭代器对象迭代器对象是一个表示可迭代对象的迭代器的对象代器对象的 next() 方法返回一个具有 value 和 done 两个属性的对象,用于表示可迭代对象中的每一个值
迭代协议Iterator 协议是一个包含 next() 方法和 done 属性的对象规范迭代器必须实现迭代协议
可迭代对象(Iterable)具有 Symbol.iterator 属性并返回迭代器的对象可以使用 for...ofspreadArray.from() 等方式迭代可迭代对象
生成器(Generator)生成器是一种在需要时生成多个值的函数生成器可以让我们更加简单、可读的编写异步代码。
生成器函数生成器函数是一种创建生成器的函数,返回一个生成器对象。使用 function* 关键字来定义,里面可以包含一个或多个 yield 关键字,用于返回序列中的下一个值。生成
生成器对象生成器对象是由生成器函数返回的。它是生成器函数的实例,同时也是一个迭代器以使用迭代器协议进行遍历
Symbol.iteratorSymbol.iterator 是一种内置的符号值,可以作为一个属性键存在于任何对象中用于返回一个具有 next() 方法的迭代器对象
@@iteratorSymbol.iterator 实际上是同一个东西,只是表示方式不同@@iteratorSymbol.iterator 的区别主要在于表达方式和历史渊源,它们在功能上是等价的,都可以用来表示对象是否是可迭代对象,以及用来实现可迭代对象的迭代器接口

迭代器的应用场景

  1. 遍历集合数据:如遍历数组、Set、Map 等数据结构中的元素,进行筛选、排序、去重等操作。
  2. 处理异步操作:可迭代对象可以用于实现一个基于异步操作的迭代器,从而实现异步任务的连续执行。
async function* dataProvider() {
  const pageSize = 10 // 每页数据量
  let pageNo = 0 // 当前页数

  while (true) {
    pageNo++
    const data = await fetch(`/api/data?pageNo=${pageNo}&pageSize=${pageSize}`).then(res => res.json())
    if (data.length === 0) {
      return // 数据已经全部返回完毕,则终止迭代器
    }
    yield data
  }
}

(async () => {
  for await (const data of dataProvider()) {
    console.log(data) // 每次输出一个页面的数据
  }
})()
  1. 实现数据流处理:可迭代对象可以用于实现诸如数据流的处理方式,如从一个可迭代的数据源中一次获取一定数量的数据,再使用管道(Pipeline)的方式进行处理和转换。
  2. 实现生成器函数:生成器函数 Generator 是一种基于可迭代对象的语言特性,通过 yield 关键字实现中断和恢复迭代的操作。

生成器的应用场景

  1. 处理异步操作:Vue 项目中,我们经常需要进行一些异步操作,比如通过异步请求获取远程数据、处理动画效果等等。在这种情况下,我们可以使用异步函数(async function)来序列化异步操作的执行,并且可以使用await操作符来等待异步操作的完成;也可以使用异步生成器函数(async function*)来实现一些异步迭代的场景,如异步分页加载。
async function getData(page) {
  const response = await fetch(`https://api.example.com/data?page=${page}`);
  const data = await response.json();
  return data;
}

async function* dataProvider() {
  let page = 1;
  while (true) {
    const data = await getData(page++);
    if (!data.length) {
      break; // 停止迭代
    }
    yield data;
  }
}

// 调用异步生成器函数获取数据
(async () => {
  for await (const res of dataProvider()) {
    // 等待每个分页数据获取完成后处理
    console.log(res);
  }
})();
在上述代码中,`dataProvider()`是一个异步生成器函数,用于获取分页数据在`dataProvider()`函数内部,我们使用`async/await`语法来发起异步请求获取数据,并通过`yield`关键字返回中间结果在使用该函数时,我们通过`for await...of`语句来遍历异步迭代器,调用`dataProvider()`函数时,实际上并没有执行整个异步生成器函数,而是异步地像迭代器一样不断返回数据,直到遇到`break`语句停止迭代该代码能够通过异步请求来加载数据并处理每个分页得到的数据
  1. 异步组件加载:对于一些复杂的组件,如果在初始化时直接渲染,可能会影响到页面的性能。在这种情况下,我们可以使用异步组件来延迟加载组件,提升页面的性能体验。异步组件使用的就是异步函数。
Vue.component('AsyncComponent', async () => {
  const data = await fetch('https://api.example.com/data');
  return {
    data: await data.json()
  };
});
在上述代码中,我们通过`async/await`语法加载了组件所需要的数据,并在加载完成后返回一个组件对象。该组件对象可以在需要的时候通过`Vue.component()`方法动态注册到 Vue 实例中。这样,在使用该组件时,Vue 就会按需进行异步加载,确保仅在需要时才会加载该组件。
  1. 生命周期钩子函数:Vue 中每个组件都有一些生命周期钩子函数,比如createdmountedupdated等等。这些生命周期函数可以在组件生命周期的各个阶段执行一些特定的操作,如初始化数据、渲染 DOM、添加事件监听等等。这些生命周期钩子函数也可以是异步函数,我们可以在这些生命周期函数中执行异步操作,比如通过异步请求获取数据、处理动画效果、缓存数据等等。
new Vue({
  el: '#app',
  data() {
    return {
      list: []
    };
  },
  created() {
    this.fetchData();
  },
  methods: {
    async fetchData() {
      const response = await fetch('https://api.example.com/data');
      const data = await response.json();
      this.list = data;
    }
  }
});
在上述代码中,`created`生命周期钩子函数中异步地调用`fetchData()`方法,该方法使用`async/await`语法发送异步请求并将返回结果更新到组件实例的`list`成员变量中这样就可以使组件渲染完成后立即加载数据,保证了用户存取数据时的无缝体验