《深入理解 JavaScript 迭代器与生成器:原理与应用》

238 阅读6分钟

前言

在 JavaScript 中,迭代器和生成器是两个非常重要的概念,它们不仅在语言的底层实现中扮演着关键角色,还为许多高级功能(如 for...of 循环、async/await 等)提供了支持。本文将从一个全新的角度出发,深入探讨迭代器和生成器的原理、实现以及它们在实际开发中的应用。

一、迭代器

背景知识

什么是迭代?

迭代是从一个数据集合中按照一定的顺序,不断地取出数据的过程。类比产品研发的迭代,产品的迭代是一次次做出来的,不确定做多少个迭代(有可能做到中途项目废弃等因素),只能知道依次迭代的动作。迭代强调的是一个过程。

迭代和遍历的区别

迭代:强调的是过程,依次取出数据的动作,不保证能取完数据。

遍历:强调的是结果,把整个数据依次取完。

JavaScript 中的迭代器

在 JavaScript 中,迭代器是一种特殊的对象,它必须具备以下两个特征:

  1. 具有 next() 方法。

  2. next() 方法返回一个对象,该对象包含两个属性:

    • value:当前迭代的值。
    • done:布尔值,表示迭代是否完成。

例如,以下是一个简单的迭代器实现:

const obj = {
  next() {
    return {
      value: "some value",
      done: false
    };
  }
};

迭代器的实际应用

数组的迭代器实现

以数组为例,我们可以将数组的遍历过程改写为迭代器模式:

const array = [1, 2, 3, 4, 5];
//for循环
for (let i = 0; i < array.length; i++) {
  console.log(array[i]);
}

function arrayIterator(arr) {
  let i = 0;
  return {
    next() {
      console.log(i);
      return {
        value: arr[i++],
        done: i > arr.length,
      };
    },
  };
}

//迭代器便利数组改造
let iterator = arrayIterator(array);
let data = iterator.next();
console.log(data);
while (!data.done) {
  console.log(data);
  data = iterator.next();
}

console.log("迭代完成");

虽然迭代器的代码看起来比传统的 for 循环更复杂,但它有一个显著的优点:隔离了调用者与数据源之间的直接操作,使得数据的处理更加灵活。

斐波那契数列的迭代器实现

迭代器还可以实现一些传统循环难以完成的需求,例如生成无限的斐波那契数列:

    function FbIterator() {
      let prev1 = 1,
        prev2 = 1,
        n = 1;
      return {
        next() {
          console.log(n);
          let value;
          if (n <= 2) {
            value = 1;
          } else {
            value = prev1 + prev2;
          }
          const result = {
            value,
            done: false,
          };
          prev2 = prev1;
          prev1 = result.value;
          n++;
          return result;
        },
      };
    }
    const fbIterator = FbIterator();

上面例子的FbIteratorarrayIterator都称为迭代器创建函数

可迭代协议

es6规定,如果一个对象具有知名符号属性 Symbol.iterator,并且属性值是一个迭代器创建函数,则该对象是一个可迭代的(iterable)

const obj = {
  [Symbol.iterator]() {
    return {
      next() {
        return { value: "some value", done: false };
      }
    };
  }
};

检测对象是否可迭代

可以通过检测对象是否具有 Symbol.iterator 属性来判断它是否是可迭代的

const arr = [1, 2, 3, 4];
console.log(typeof arr[Symbol.iterator]); // "function"

常用的数组就是一个可迭代对象,

很多的伪数组也是一个可迭代对象

 遍历可迭代对象

可以通过调用对象的 Symbol.iterator 方法来获取其迭代器,然后通过迭代器的 next() 方法逐个获取值:

const arr = [1, 2, 3, 4];
const iterator = arr[Symbol.iterator]();
let result = iterator.next();
while (!result.done) {
  console.log(result.value);
  result = iterator.next();
}
for of

for...of 循环是 ES6 引入的一种语法糖,专门用于遍历可迭代对象。它的底层实现就是通过调用对象的 Symbol.iterator 方法来获取迭代器,然后逐个获取值:

for (let item of iterable){
    //iterable 可迭代对象
    // item 每次迭代得到的数据
}
//这个语法糖实现原理其实就是上面遍历可迭代对象的过程

二、生成器(Generator)

生成器是 JavaScript 中一种特殊的函数,它返回一个生成器对象,该对象实现了迭代器协议。生成器的出现主要是为了简化迭代器的编写过程,但它也带来了一些新的特性。

什么是生成器

生成器是一个通过构造函数 Generator 创建的对象,但它不能直接通过 new Generator() 创建,而是通过生成器函数(Generator Function)来创建。

生成器的特点

1.生成器是一个迭代器,一定有 next() 方法。 2.生成器是一个可迭代对象,一定有 Symbol.iterator 属性。 3.生成器可以使用 for...of 循环遍历。

生成器的创建

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

function *method() {} //该函数一定返回一个生成器

在函数名前加上 *,表示这是一个生成器函数。调用生成器函数会返回一个生成器对象:

生成器函数的执行逻辑

生成器函数内部的代码不会在函数调用时立即执行,而是等待生成器的 next() 方法被调用时才会逐步执行。每次调用 next() 方法,生成器函数会运行到下一个 yield 关键字的位置,并返回一个对象,包含两个属性:

1、value:当前迭代的值。 2、 done:布尔值,表示迭代是否完成。

例如:

function* method() {
  console.log("第一次运行");
  yield 1;
  console.log("第二次运行");
  yield 2;
  console.log("第三次运行");
}

const generator = method();
console.log(generator.next()); // { value: 1, done: false }
console.log(generator.next()); // { value: 2, done: false }
console.log(generator.next()); // { value: undefined, done: true }
function *method() { //该函数一定返回一个生成器
    console.log("test") //直接调用,不会执行 因为这里并没有迭代
} 

生成器的使用示例

使用生成器简化数组迭代器

const array = [1, 2, 3, 4, 5];

// 迭代器实现
function arrayIterator(arr) {
  let i = 0;
  return {
    next() {
      return {
        value: arr[i++],
        done: i > arr.length
      };
    }
  };
}

// 使用生成器实现
function* arrayGenerator(arr) {
  for (let item of arr) {
    yield item;
  }
}

const generator = arrayGenerator(array);
for (let item of generator) {
  console.log(item);
}

生成器的高级特性

生成器的返回值

生成器函数可以有返回值,返回值出现在第一次 donetrue 时的 value 属性中:

function* method() {
yield 1;
yield 2;
return 10;
}

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

向生成器传递参数

调用生成器的 next() 方法时,可以传递参数,传递的参数可以交给 yield 的表达式:

function* method() {
  //该函数一定返回一个生成器
  let num = yield 1;
  console.log(num); //这个num就是next参数传进来的参数
  num = yield 2 + num;
}
const generator = method();

image.png

其他 API

return() :调用该方法可以提前结束生成器函数,从而提前结束整个迭代过程。

   function* method() {
     yield 1;
     yield 2yield 3;
   }
   const generator = method();
   generator.next()
   generator.return()//直接结束迭代

throw() :调用该方法可以在生成器中产生一个错误。

生成器的实际应用

异步流程控制

在 ES6 刚出来的时候,还没有 async/await 关键字,使用 Promise 写异步代码比较麻烦。当时,有人利用生成器来解决这个问题:

function* task() {
  const d = yield 1;
  const a = yield "abc";
  const resp = yield fetch("....");
  const result = yield resp.json();
}
function run(generatorFunc) {
  const generator = generatorFunc();
  let result = generator.next();
  handleResult();
  function handleResult() {
    if (result.done) {
      return;
    }
    if (typeof result.value.then === "function") {
      result.value.then(
        (data) => {
          result = generator.next(data);
          handleResult();
        },
        (error) => {
          result = generator.throw(error);
          handleResult();
        }
      );
    } else {
      result = generator.next(result.value);
      handleResult();
    }
  }
}
run(task);

通过这种方式,生成器可以用来简化异步代码的编写,使得异步流程更加清晰。

四、总结

本文从迭代器和生成器的基本概念出发,详细介绍了它们的实现原理、特点以及在实际开发中的应用。迭代器和生成器是 JavaScript 中非常强大的工具,它们不仅能够简化代码的编写,还能实现一些传统方法难以完成的功能。希望本文能够帮助你更好地理解和使用迭代器与生成器,提升你的 JavaScript 编程能力。