Iterator, Generator, async/await

Iterator, Generator, async/await


javascript iterator generator 异步编程

Iterator

迭代器。它为 js 中各种不同的数据结构(Array、Set、Map)提供统一的访问机制。任何数据结构只要部署了 Iterator 接口,就可以完成遍历操作。 因此 iterator 也是一种对象,不过相比于普通对象来说,它有着专为迭代而设计的接口。

作用

  • 为各种数据结构,提供一个统一的、简便的访问接口;
  • 使得数据结构的成员能够按某种次序排列;
  • ES6 创造了一种新的遍历命令 for…of 循环,Iterator 接口主要供 for…of 消费

结构

有 next 方法,该方法返回一个包含 value 和 done 两个属性的对象(我们假设叫 result)。value 是迭代的值,后者是表明迭代是否完成的标志。true 表示迭代完成,false 表示没有。iterator 内部有指向迭代位置的指针,每次调用 next,自动移动指针并返回相应的 result。

原生具备 iterator 接口的数据结构如下:

  • Array
  • Map
  • Set
  • String
  • TypedArray
  • 函数里的 arguments 对象
  • NodeList 对象

这些数据结构都有一个 Symbol.iterator 属性,可以直接通过这个属性来直接创建一个迭代器。 也就是说,Symbol.iterator 属性只是一个用来创建迭代器的接口,而不是一个迭代器,因为它不含遍历的部分。 使用 Symbol.iterator 接口生成 iterator 迭代器来遍历数组的过程为:

let arr = ["a", "b", "c"];

let iter = arr[Symbol.iterator]();

iter.next(); // { value: 'a', done: false }
iter.next(); // { value: 'b', done: false }
iter.next(); // { value: 'c', done: false }
iter.next(); // { value: undefined, done: true }

for … of 的循环内部实现机制其实就是 iterator,它首先调用被遍历集合对象的 Symbol.iterator 方法,该方法返回一个迭代器对象,迭代器对象是可以拥有.next()方法的任何对象,然后,在 for … of 的每次循环中,都将调用该迭代器对象上的 .next 方法。然后使用 for i of 打印出来的 i 就是调用迭代器对象.next 方法后得到的对象上的 value 属性。


对于原生不具备 iterator 接口的数据结构,比如 Object,我们可以采用自定义的方式来创建一个遍历器。

let obj = { name: "niko", age: 26 };
function createIterator(object) {
  let keys = Object.keys(object);
  let index = 0;
  return {
    next: function () {
      let done = index >= keys.length;
      let value = !done ? object[keys[index++]] : undefined;
      return {
        value,
        done,
      };
    },
  };
}

let iter = createIterator(obj);
iter.next(); // {value: 'niko', done: false}
iter.next(); // {value: 26, done: false}
iter.next(); // {value: undefined, done: true}

Generator

Generator 是 ES6 提出的一种异步编程的方案。因为手动创建一个 iterator 十分麻烦,因此 ES6 推出了 generator,用于更方便的创建 iterator。也就是说,Generator 就是一个返回值为 iterator 对象的函数。

function* createIterator() {
  yield 1;
  yield 2;
  yield 3;
}
// generator函数可以像正常函数一样被调用,不同的是会返回一个 iterator
let iterator = createIterator();
console.log(iterator.next().value); // 1
console.log(iterator.next().value); // 2
console.log(iterator.next().value); // 3

Generator 函数有两个特征:

  • function 关键字与函数名之间有一个星号
  • 函数体内部使用 yield 语句,定义不同的内部状态

Generator 函数的调用方法与普通函数一样,也是在函数名后面加上一对圆括号。不同的是,调用 Generator 函数后,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象,也就是迭代器对象(Iterator Object)。

在普通函数中,我们想要一个函数最终的执行结果,一般都是 return 出来,或者以 return 作为结束函数的标准。运行函数时也不能被打断,期间也不能从外部再传入值到函数体内。 但在 generator 中,就打破了这几点,所以 generator 和普通的函数完全不同。 当以 function*的方式声明了一个 Generator 生成器时,内部是可以有许多状态的,以 yield 进行断点间隔。期间我们执行调用这个生成的 Generator,他会返回一个迭代器对象,用这个对象上的方法,实现获得一个 yield 后面输出的结果。

yield 和 return 的异同:

  • 都能返回紧跟在语句后面的那个表达式的值(yield 语句本身没有返回值,只是把后面的表达式的赋给 Generator 对象调用 next() 方法返回的对象的 value 属性)
  • yield 相比于 return 来说,更像是一个断点。遇到 yield,函数暂停执行,下一次再从该位置继续向后执行,而 return 语句不具备位置记忆的功能。
  • 一个函数里面,只能执行一个 return 语句,但是可以执行多次 yield 表达式。
  • 如果 return 语句后面还有 yield 表达式,那么后面的 yield 完全不生效
function* gen() {
  console.log("staring");
  yield "a";
  yield "b";
  yield "c";
  return "d";
  yield "ending";
}
const iter = gen();
console.log(iter.next()); // staring  {value: a,done:false}
console.log(iter.next()); // {value: b,done:false}
console.log(iter.next()); // {value: b,done:false}
console.log(iter.next()); // {value: d,done:true}
console.log(iter.next()); // {value: undefined,done:true}

注意点

  • yield 语句只能在 Generator 函数中,不能在其他函数中。
(function  foo() {
  yield 1;
}())
// Uncaught SyntaxError: Unexpected number
  • yield 表达式如果用在另一个表达式之中,必须放在圆括号里面。
function* gen() {
  console.log('hello' + yield 123); // 这句会报语法错误
  console.log('hello' + (yield 123));
}
  • 如果表达式用作函数参数或者放在赋值表达式的右边,可以不加括号。
function* gen() {
  let res = yield 123;
}
  • yield 不能跨函数。并且 yield 需要和*配套使用,别处使用无效
  • 箭头函数不能用做 generator
  • for…of 循环可以自动遍历 Generator 函数生成的遍历器对象,且此时不再需要调用 next()方法
function* gen() {
  yield 1;
  yield 2;
  yield 3;
  yield 4;
  return 5;
}
for (let item of foo()) {
  console.log(item);
}
// 1
// 2
// 3
// 4

一旦 next()方法的返回对象的 done 属性为 true,for…of 循环就会终止,且不包含该返回对象。所以上述的 return 语句返回值不包括在 for…of 循环中。

利用 Generator 函数和 for…of 循环实现斐波那契数列

function* fibonacci() {
  let [prev, curr] = [0, 1];
  for (;;) {
    [prev, curr] = [curr, prev + curr];
    yield curr;
  }
}
for (let n of fibonacci()) {
  if (n > 10) break;
  console.log(n);
}
// 1
// 2
// 3
// 5
// 8

除了 for…of 循环,扩展符(…)、解构赋值和 Array.from 方法内部调用的都是遍历器接口。所以,它们都可以将 Generator 函数返回的遍历器对象作为参数。

function* numbers() {
  yield 1;
  yield 2;
  yield 3;
  return 4;
}
// 扩展运算符
console.log([...numbers()]); // [1,2,3]
// 解构赋值
let [x, y, z] = numbers();
console.log([x, y, z]); // [1,2,3]

// Array.from
console.log(Array.from(numbers())); // [1,2,3]

使用 yield*语句,可以实现在一个 Generator 函数里面执行另一个 Generator 函数

function* foo() {
  yield "a";
  yield "b";
}
function* bar() {
  yield "x";
  yield* foo();
  yield "y";
}
for (let v of bar()) {
  console.log(v);
}
// x
// a
// b
// y

yield*后面的 Generator 函数(没有 return 语句时)可以看做是 for…of 的一种简写形式。

当有 return 函数时,需要用 var value = yield* generator()的形式获取 return 语句的值。

function* foo() {
  yield 2;
  yield 3;
  return "foo";
}

function* bar() {
  yield 1;
  var v = yield* foo();
  console.log("v:" + v);
  yield 4;
}

let iter = bar();
console.log(iter.next().value); // 1
console.log(iter.next().value); // 2
console.log(iter.next().value); // 3
console.log(iter.next().value);
// v: foo
// 4
console.log(iter.next()); // {value:undefined, done: true}

yield*遍历数据

function* gen() {
  yield* ["a", "b", "c"]; // 这里不加*的话就会直接返回整个数组
}
let iter = gen();
console.log(iter.next().value); // a
console.log(iter.next().value); // b
console.log(iter.next().value); // c

next 中的传参

function step(value) {
  return value + 1;
}
function* task(value) {
  var value2 = yield step(value);
  console.log(value2);
  var value3 = yield step(value2);
  console.log(value3);
}
let gen = task(1);
gen.next(); // {value: 2, done: false}
gen.next(); // undefined {value: NaN, done: false}
gen.next(); // undefined {value: undefined, done: true}

通过上述例子可以看出,value2 并不会直接被 yield 语句赋值,这里的赋值实际上是通过 next 传参完成的, next 里的参数相当于上一句 yield 表达式的值。

function step(value) {
  return value + 1;
}
function* task(value) {
  var value2 = yield step(value);
  console.log(value2);
  var value3 = 2 * (yield step(value2)); // next里的值是3, 所以value3是2*3=6
  console.log(value3);
}
let gen = task(1);
gen.next(); // {value: 2, done: false}
gen.next(2); // 2 {value: 3, done: false}
gen.next(3); // 6 {value: undefined, done: true}

当然这样写是不行的,可以这么写:

function step(value) {
  return value + 1;
}
function* task(value) {
  var value2 = yield step(value);
  var value3 = yield step(value2);
  var value4 = yield step(value3);
  console.log(value4);
}
go(task(1));
function go(task) {
  var taskObj = task.next(task.value);
  console.log(taskObj);
  // 如果Generator函数未结束,就继续调用
  if (!taskObj.done) {
    task.value = taskObj.value;
    go(task);
  }
}
// {value: 2, done: false}
// {value: 3, done: false}
// {value: 4, done: false}
// 4
// {value: undefined, done: true}

async/await

async/await 实际上是由 generator + yield 控制流程 + promise 实现回调的一个语法糖。

先看一下使用 generator 怎么把一个异步操作”同步化”。

function* gen(){
  var url = 'https://xiejunyi.com/xxx';
  var result = yield fetch(url);
  console.log(result);
}

let g = gen();
let result = g.next();
result.value.then(data => return data.json)
            .then(data => g.next(data))

大概就是到 then 里面再去执行 next, 同时把返回值传进去。 但是这样写很麻烦,不好管理,所以就有了 async/await。两者在函数定义时的写法也差不多,就是把 * 换成 async, yield 换成 await。

首先看下这个例子:

async function test() {
  return "niko";
}
const result = test();
console.log(result); // Promise {<fulfilled>: 'niko'}

可以看到输出的是一个 Promise 对象! 所以,async 函数返回的是一个 Promise 对象,如果直接 return 一个直接量,async 会把这个直接量通过 Promise.resolve()封装成 Promise 对象。

注意点

一般来说,都认为 await 是在等待一个 async 函数完成,确切的说等待的是一个表示式,这个表达式的计算结果是 Promise 对象或者是其他值(没有限定是什么)

即 await 后面不仅可以接 Promise,还可以接普通函数或者直接量。

同时,我们可以把 async 理解为一个运算符,用于组成表达式,表达式的结果取决于它等到的东西

  • 等到非 Promise 对象: 表达式结果为它等到的东西
  • 等到 Promise 对象: await 就会阻塞后面的代码,等待 Promise 对象 resolve,取得 resolve 的值,作为表达式的结果

async 函数的优点

1.内置执行器

Generator 函数的执行必须靠执行器,所以才有了 co 函数库,而 async 函数自带执行器。也就是说,async 函数的执行,与普通函数一模一样,只要一行。

2.语义化更好

async 和 await,比起星号和 yield,语义更清楚了。async 是“异步”的简写,而 await 可以认为是 async wait 的简写。所以应该很好理解 async 用于申明一个 function 是异步的,而 await 用于等待一个异步方法执行完成。

3.更广的适用性

yield 命令后面只能是 Thunk 函数或 Promise 对象,而 async 函数的 await 命令后面,可以跟 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时等同于同步操作)。

© 2025 Niko Xie