JavaScript中的迭代器和生成器

75 阅读7分钟

迭代器

迭代器是一种遵循迭代器协议的对象,该协议要求迭代器必须实现一个next()方法。调用next()方法时,它会返回一个对象,该对象具有两个属性:valuedonevalue属性表示迭代器返回的当前值,而done属性是一个布尔值,表示迭代器是否已遍历完其序列,当我们使用for...of循环时,遍历的目标就是可迭代对象。

任何对象,只要它具有可访问的Symbol.iterator属性,他就是可迭代对象。

JavaScript中的可迭代对象有:数组(Array)字符串MapSetArguments 对象Generator 函数NodeList 和 HTMLCollection(浏览器特有)

这些数据结构都有可访问的Symbol.iterator属性。

JavaScript普通的object对象由于不是迭代器对象,是不支持for of的,当给对象加上Symbol.iterator属性,就可以让它变成可迭代的对象了。

在下面的例子中,为普通对象添加了一个Symbol.iterator,这个对象就可以通过for of循环了,Symbol.iterator的值是一个函数,函数返回一个对象,对象里面有一个next函数。for of内部调用这个next方法获取到next函数返回的对象,这个对象包含两个属性,value就是遍历拿到的值,done是一个迭代是否完成的标识,如果done为true,则结束迭代。

const obj = {
  name: "Tom",
  age: 10,
  [Symbol.iterator]: function () {
    // 获取所有属性名,这里的this就是obj对象 iterator=["name","age"]
    const iterator = Object.keys(this);
    return {
      next: () => {
        const value = iterator.shift();
        return {
          done: !value,
          value: this[value],
        };
      },
    };
  },
};
for (const x of obj) {
  console.log(x); // 打印:Tom 10
}

生成器

生成器是ES6引入的一种特殊的函数,生成器函数使用function*语法声明,并且在函数体内可以使用yield关键字来暂停函数的执行并返回一个值,或者使用yield*来委托另一个生成器或可迭代对象。

基础概念

当调用生成器函数时,它不会立即执行其函数体中的代码,而是返回一个迭代器对象。这个迭代器对象遵循迭代器协议,即它有一个next()方法。调用next()方法会恢复生成器函数的执行,直到遇到下一个yield表达式或函数结束。每次调用next()时,它都会返回一个对象,该对象包含value(yield表达式的值或函数的最终返回值)和done(一个布尔值,表示生成器是否已经返回其最终值并结束)。

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

// 掉用生成器函数,返回一个迭代器
const t = test();

// 调用迭代器的next方法,返回一个对象
console.log(t.next()); // { value: 1, done: false }
console.log(t.next()); // { value: 2, done: false }
console.log(t.next()); // { value: 3, done: false }
console.log(t.next()); // { value: undefined, done: true }

当第一次调用next()的时候,test函数会执行到到第一个yield的位置,并将后面的1作为value返回。由于后面还有值可以拿到,所有没有结束done的值为false

当第二次调用next()的时候,test函数会执行到第二个yield的位置,并将后面的2作为value返回,done为false

...

同理第四次调用,已经没有yield了,所以value为undefined,done为true

用生成器实现斐波那契数列

这是在某一次面试中遇到的,斐波那契数列就是一个当前值等于前两个值相加的数列。

function* fibonacci() {
  let [prev, curr] = [0, 1];
  while (true) {
    [prev, curr] = [curr, prev + curr];
    yield curr;
  }
}
const gen = fibonacci();
console.log(gen.next()); // { value: 1, done: false }
console.log(gen.next()); // { value: 2, done: false }
console.log(gen.next()); // { value: 3, done: false }
console.log(gen.next()); // { value: 5, done: false }
console.log(gen.next()); // { value: 8, done: false }
console.log(gen.next()); // { value: 13, done: false }

生成器传参

next函数调用的时候是可以传递参数的,这个参数会被当前yield关键字前面的变量接收,可以想象yield这个关键字是一个墙,当第一次调用next()的时候,函数卡在墙的位置,并将墙右边的值作为value返回;第二次调用next()的时候,这个墙变成了next参数的值,如果左边有变量接收,就会赋值给这个变量。所以第一次next()调用的参数没有意义。

function* test() {
  const a = yield 1;
  console.log(a); // next的参数
}

const t = test();

console.log(t.next());// 第一次的next参数没有意义
console.log(t.next("next的参数"));

由于每次调用next才会执行生成器函数,所以这个生成器函数是可以中断的,有人就会想,既然可以中断,那么结合promise是不是就可以有一些花样了。

function func(x) {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve(x + 1);
    }, 1000);
  });
}

function* generator(x) {
  const r1 = yield func(x);
  console.log(r1);// 1s后打印1
  
  const r2 = yield func(r1);
  console.log(r2);// 2s后打印2
  
  const r3 = yield func(r2);
  console.log(r3);// 3s后打印3
}


function asyncGenerator(generator, ...params) {
  const gen = generator(...params);

  function next(args) {
    const { value, done } = gen.next(args);

    if (done) return;
    value.then((x) => next(x));
  }
  next();
}
asyncGenerator(generator, 0);

上面代码中实现了async await的等待效果,事实上,async,await的原理也是这样,下面的代码。

function delay() {
  return new Promise((resolve) => setTimeout(resolve, 1000));
}
async function test() {
  await delay();
  console.log("hello");
}

经过babel编译成低版本浏览器兼容(chrome 51),正是下面的代码,可以看出,借助了生成器。

image.png

function asyncGeneratorStep(n, t, e, r, o, a, c) {
  try {
    var i = n[a](c),
      u = i.value;
  } catch (n) {
    return void e(n);
  }
  i.done ? t(u) : Promise.resolve(u).then(r, o);
}
function _asyncToGenerator(n) {
  return function () {
    var t = this,
      e = arguments;
    return new Promise(function (r, o) {
      var a = n.apply(t, e);
      function _next(n) {
        asyncGeneratorStep(a, r, o, _next, _throw, "next", n);
      }
      function _throw(n) {
        asyncGeneratorStep(a, r, o, _next, _throw, "throw", n);
      }
      _next(void 0);
    });
  };
}
function delay() {
  return new Promise((resolve) => setTimeout(resolve, 1000));
}
function test() {
  return _test.apply(this, arguments);
}
function _test() {
  _test = _asyncToGenerator(function* () {
    yield delay();
    console.log("hello");
  });
  return _test.apply(this, arguments);
}

异步迭代器/生成器

异步迭代器是 JavaScript 中的一个高级特性,它允许你以异步的方式遍历数据,比如异步地从服务器加载大量数据块,而不需要一次性加载所有数据到内存中。异步迭代器结合了迭代器的概念(允许你逐个访问集合中的元素)和异步函数(能够执行耗时操作而不阻塞程序的其他部分)的优势。它的特点是异步迭代器对象上有Symbol.asyncIterator这个属性,

异步生成器(async generator)函数是创建异步迭代器的一种主要方式。异步生成器函数类似于普通的生成器函数,但使用 async function* 语法,并且 yield 关键字可以配合 Promise 使用,以产生异步值。

下面是二者结合的示例:

const obj = {
  name: "web",
  age: 2,
  [Symbol.asyncIterator]: async function* () {
    const keys = Object.keys(this);
    for (let key of keys) {
      // 产出一个Promise,该Promise在1秒后解决为对象的属性值
      yield new Promise((resolve) =>
        setTimeout(() => resolve(this[key]), 1000)
      );
    }
  },
};

// 使用 for await...of 循环遍历异步迭代器
async function iterateObject() {
  for await (const item of obj) {
    console.log(item); // 这里会等待每个Promise解决后再打印
  }
}

// 调用函数来启动迭代
iterateObject();

既然用了async,上面代码内部自然也是利用await做一些操作的。yield关键字会将右边的值作为本次迭代的结果返回。想要遍历这个异步迭代器可以用for await ... of这个语法。

ahooks中有一个hooks,useAsyncEffect,这个hook的参数就是一个异步生成器函数

文档地址:ahooks.js.org/hooks/use-a…

源码地址:github.com/alibaba/hoo…

总结

迭代器和生成器虽然在日常业务代码中的直接应用可能不如其他编程概念常见,但它们在特定场景下展现出了极大的灵活性和效率,特别是对于复杂流程的控制。初次接触并深入理解这些概念时,可能会感到有些挑战,尤其是在使用像redux-saga这样的库时,该库大量依赖于生成器函数来管理异步操作和副作用。

redux-saga作为Redux的中间件,通过生成器函数提供了一种优雅的方式来处理应用中的异步逻辑,如API调用、数据加载等。如果不对生成器的运行原理有一定的了解,那么掌握redux-saga的高级特性和其背后提供的抽象层次就会变得相当困难。

然而,在需要处理复杂控制流、状态管理或异步任务时,迭代器和生成器成为了强大的工具。它们允许开发者以更直观和可维护的方式编写代码,特别是在需要暂停、恢复或分步执行代码块的场景下。因此,虽然它们可能不是每日编程的必备知识,但在处理特定类型的业务逻辑时,掌握迭代器和生成器的使用将极大地提升开发效率和代码质量。