js- 迭代器,生成器/async-awiait

109 阅读12分钟

迭代器

什么是迭代器

迭代器,顾名思义,就是遍访一个数据结构,例如数组,map,set 等结构,能够访问这些数据结构中的每一个元素,并且做一些事情。

迭代器并不是js 特有的结构,在java ,Python等语言中都有类似的概念。

从迭代器的定义中我们可以看出来,迭代器其实是一个工具,一个帮助我们对某种数据对象进行遍历的对象。

在JavaScript 中,迭代器就是一个具体的对象,我们规定只要一个对象符合迭代器协议(iterator protocol),这个对象就是一个迭代器。

迭代器协议是一个协议,具体如下

  • 迭代器协议要求对象必须有一种产生一系列值(无论是有限还是无限个)的标准方式
  • 迭代器协议要求一个迭代器对象中必须有一个next 方法。
  • 迭代器中的next 方法还必须满足如下的要求
    • 一个无参数或者一个的函数,返回一个应当拥有一下两个属性的对象
    • done(boolean)
      • 如果迭代器可以产生序列中的下一个值,则为false
      • 如果迭代器中已经将序列迭代完毕,则为true,这种情况下,value是可选的,如果它依然存在,即为迭代结束之后的默认返回值
    • value
      • 迭代器返回的任何JavaScript值。done 为true时 可省略 迭代器的例子:
let nameArr = ["abc", "cba", "nba"];
function createIterator() {
  let index = 0;
  return {
    next: function () {
      if (index < nameArr.length) {
        return { done: false, value: nameArr[index++] };
      } else {
        return { done: true, value: undefined };
      }
    },
  };
}
const iteatorObj = createIterator();
console.log(iteatorObj.next());//{ done: false, value: 'abc' }
console.log(iteatorObj.next());//{ done: false, value: 'cba' }
console.log(iteatorObj.next());//{ done: false, value: 'nba' }
console.log(iteatorObj.next());//{ done: true, value: undefined }

可以看到我们定义了一个函数, 这个函数可以返回一个针对nameArr数组的迭代器。通过调用这个迭代器的next 方法,我们就可以遍历nameArr 数组中的每个元素。

所以说迭代器就是一个符合迭代器协议的对象,任何对象只要内部有一个符合条件的next方法,并且通过next 方法的调用能够访问遍历一个数据结构的所有元素,我们都可以称这个对象是一个迭代器。

什么是可迭代对象

可迭代对象是与迭代器不同的另一个概念。

当一个对象实现了iterable protocol协议时,它就是一个可迭代对象。

iterable protocol协议要求一个对象必须实现@@iterator 方法,在代码中我们使用 Symbol.iterator 访问该属性。 @@iterator 方法的返回值 必须是一个迭代器。

由此我们可以更清楚的描述可迭代对象。

可迭代对象是一个实现了 @@iterator 方法,并且@@iterator 方法必须返回一个迭代器的对象。由此也可以看出可迭代对象其实是对迭代器和迭代器要遍历的数据的结构进行封装后的一个对象。

可迭代对象举例:

const iterableObj = {
  namesArr: ["abc", "cba", "nba"],
  [Symbol.iterator]: function () {
    let index = 0;
    return {
      next: () => {
        if (index < this.namesArr.length) {
          return { done: false, value: [this.namesArr[index++]] };
        } else {
          return { done: true, value: undefined };
        }
      },
    };
  },
};

for (const item of iterableObj) {
  console.log(item);
}

可以看到 iterableObj 就是我们创建的可迭代对象。这个对象有自己的可遍历数据结构,也实现了@@iterator 方法。我们可以通过 of 操作符 来调用 可迭代对象的@@iterator 方法,每次调用这个方法,都会产生一个针对这个可迭代对象的迭代器。

原生中的可迭代对象

事实上我们平时创建的很多原生对象已经实现了可迭代协议,通过Symbol.iterator 属性的访问,可以获取到一个迭代器对象。 例如String、Array、Map、Set、arguments对象、NodeList集合都是实现了可迭代协议的原生对象。

const names = ["abc", "cba", "nba"];
const iterator = names[Symbol.iterator]();
console.log(iterator.next());//{ value: 'abc', done: false }
console.log(iterator.next());//{ value: 'cba', done: false }
console.log(iterator.next());//{ value: 'nba', done: false }
console.log(iterator.next());//{ value: undefined, done: true }

可迭代对象的应用场景

  • JavaScript中语法:for ...of、展开语法(spread syntax)、yield、解构赋值(Destructuring_assignment);*
  • 创建一些对象时:new Map([Iterable])、new WeakMap([iterable])、new Set([iterable])、new WeakSet([iterable]);
  • 一些方法的调用:Promise.all(iterable)、Promise.race(iterable)、Array.from(iterable);
const names = ["abc", "cba", "nba"];

// 1,for... of 的应用
for (const item of names) {
  console.log(item);
}
// abc
// cba
// nba
//2.展开运算符
console.log([...names]); //[ 'abc', 'cba', 'nba' ]

//3.解构
const [name1, name2] = names;
console.log(name1, name2); //abc cba

//4. 创建其他对象
const setObj = new Set(names); //Set(3) { 'abc', 'cba', 'nba' }
console.log(setObj);

//5,方法调用
Promise.all(names).then((res) => {
  console.log(res); //[ 'abc', 'cba', 'nba' ]
});

自定义可迭代对象

只有符合了可迭代协议的对象才是可迭代对象,对象才是可遍历的,也能应用到各种可迭代对象能够被应用的场景。 我们自己创建的类的实例都是不能遍历的,但是我们可以通过实现这个类的@@iterator 协议,使其满足可迭代协议,这样我们就可以像遍历数组那样,遍历我们自己的对象了。这种做法被称为自定义可迭代对象。

举例

class Person {
  constructor(name, age, frieneds) {
    this.name = name;
    this.age = age;
    this.frieneds = frieneds;
  }

  [Symbol.iterator]() {
    let index = 0;

    return {
      next: () => {
        if (index < this.frieneds.length) {
          return { done: false, value: this.frieneds[index++] };
        } else {
          return { done: true, value: undefined };
        }
      },
      return: () => {
        console.log("迭代器提前退出了"); //当遍历过程中遇到 break、continue、return、throw 等操作时,会执行迭代器中的 return 函数
        return { done: true };
      },
    };
  }
}

const person = new Person("王一", 19, ["kebi", "lemon", "xiaohan"]);
console.log(person.frieneds);

for (const friened of person) {
  console.log(friened);
}

for (const friened of person) {
  console.log(friened);
  if (friened === "lemon") break;
}

通过实现@@iterator 方法,我们就能够随意定义符合我们自己需求的可迭代对象,另外,当可迭代对象在遍历过程中,如果遇到break、continue、return、throw 等关键字,会退出遍历,如果我们希望可以监听到这种退出的情况,可以在迭代器中添加 return 方法的实现,来进行监听。具体做法如上例。

生成器

生成器是什么

生成器是ES6中新增的一种函数控制、使用的方案,它可以让我们更加灵活的控制函数什么时候继续执行、暂停执 行等。

生成器是一种特殊的迭代器。

生成器也是一个对象。由生成器函数的调用返回。

生成器函数也是一个函数,但是和普通的函数有一些区别

  • 首先,生成器函数需要在function的后面加一个符号:*
  • 其次,生成器函数可以通过yield关键字来控制函数的执行流程
  • 最后,生成器函数的返回值是一个Generator(生成器):
    • MDN:Instead, they return a special type of iterator, called a Generator.(生成器是一种特殊的迭代器)

生成器对函数流程的控制方法

通过在生成器函数中的yield 关键字进行代码流程的分段,通过 生成器对象的next()方法进行分段代码的执行(next()的返回值默认是{done:,value}的对象,其value值默认是undefined)。 在生成器函数中,我们也可以通过return 关键字终止生成器函数的执行。

举例

function* createGenerator() {
  let code1 = "分段代码1";
  console.log(code1);

  yield;

  let code2 = "分段代码2";
  console.log(code2);

  yield;
  let code3 = "分段代码3";
  console.log(code3);

  return;
  console.log("return 后代码");
}

const generator = createGenerator(); // 通过生成器函数调用 生成 生成器对象
const returnValue = generator.next(); // 分段代码1
console.log(returnValue); //{ value: undefined, done: false }

通过代码打印,我们可以看到,生成器函数生成生成器对象后,其使用方法与迭代器一致,可以通过next()方法,获取迭代器的下一个元素。而next()方法的返回值也是迭代器中的一个元素,所以我们说生成器是一种特殊的迭代器,因为是迭代器,所以可以通过next()方法遍历迭代器中的元素,说特殊,是因为生成器的对象的生成方式与一般的迭代器有区别,它是由生成器函数生成的,而不是通过之前迭代器的方式生成的。

另外,我们也可以清晰的看到,通过yield 关键字,我们可以将生成器函数中的代码进行分段,从而达到控制函数执行流程的目的。我们可以把这些分段了的代码,看成是迭代器中的 一个一个的元素,这样看来,生成器就是一个迭代器,只不过迭代器迭代的是一般元素,生成器迭代的是代码元素。

生成器的传参和返回值

生成器的返回值

在调用next()方法后,next()方法的返回值默认是{done:value:undefined}. 那如我们我想知道next 方法执行的 这段代码的返回值是什么,要怎么获取呢?

这里我们可以通过在生成器函数中在yield 后跟上返回值,来改变next()方法的返回值,通过这种方式,next()方法的返回值对象中的value 值就会保存,我们就可以通过next()方法的返回值,拿到本段代码执行后的返回值。

function* foo() {
  console.log("函数开始执行~");

  const value1 = 100;
  console.log("第一段代码:", value1);
  yield value1;

  const value2 = 200;
  console.log("第二段代码:", value2);
  yield value2;

  const value3 = 300;
  console.log("第三段代码:", value3);
  yield value3;

  console.log("函数执行结束~");
  return "123";
}

const generator = foo();
console.log(generator.next());//{ value: 100, done: false }
console.log(generator.next().value);//200


生成器的参数传递

如果生成器函数在执行的过程中需要函数外的参数时,我们怎么把变量值传递给生成器函数呢?

我们可以在两个时机对生成器函数进行参数的传递。

  • 在调用生成器函数生成 生成器的时候进行参数传递
  • 通过生成器的next()方法进行参数传递。
    • 我们在调用next函数的时候,可以给它传递参数,那么这个参数会作为上一个yield语句的返回值。
    • 如果yeild 语句的返回值和 next()函数的传递参数同时存在,会使用next()中传递的值。
function* foo(num) {
  console.log("函数开始执行~")

  const value1 = 100 * num
  console.log("第一段代码:", value1)
  const n = yield value1

  const value2 = 200 * n
  console.log("第二段代码:", value2)
  const count = yield value2

  const value3 = 300 * count
  console.log("第三段代码:", value3)
  yield value3

  console.log("函数执行结束~")
  return "123"
}

//在调用生成器函数的时候传递参数
const generator = foo(5); // 第一段代码传递参数 必须通过,生成器函数传递
console.log(generator.next()); //
console.log(generator.next(20)); // 第二段代码传递参数可以通过next()方法传递参数

生成器提前结束-return 函数

一个可以给生成器函数传递参数的方法是通过return函数. return传值后这个生成器函数就会结束,本次本应该调用的代码段也不会执行了。之后调用next也不会继续生成值了;

function* foo(num) {
  console.log("函数开始执行~")

  const value1 = 100 * num
  console.log("第一段代码:", value1)
  const n = yield value1

  const value2 = 200 * n
  console.log("第二段代码:", value2)
  const count = yield value2

  const value3 = 300 * count
  console.log("第三段代码:", value3)
  yield value3

  console.log("函数执行结束~")
  return "123"
}

const generator = foo(10)

console.log(generator.next())

// 第二段代码的执行, 使用了return
// 那么就意味着相当于在第一段代码的后面加上return, 就会提前终端生成器函数代码继续执行
console.log(generator.return(15))
console.log(generator.next())

生成器抛出异常-throw 函数

除了给生成器函数内部传递参数之外,也可以给生成器函数内部抛出异常:

抛出异常后我们可以在生成器函数中捕获异常;

但是在catch语句中不能继续yield新的值了,但是可以在catch语句外使用yield继续中断函数的执行;

function* foo() {
  console.log("代码开始执行~");

  const value1 = 100;
  try {
    yield value1;//第一次执行至此
    
  } catch (error) {
    console.log("捕获到异常情况:", error);
  }
  yield "abc";

  console.log("第二段代码继续执行");
  const value2 = 200;
  yield value2;

  console.log("代码执行结束~");
}

const generator = foo();

const result = generator.next();//代码开始执行~
console.log(generator.throw("error message"));//生成器的throw函数()会再次使生成器函数执行,并捕获 异常,我们在catch 语句中 不能通过yield设置返回值,而是需要在try/catch 外层 通过yield 返回值。
//捕获到异常情况: error message
//{ value: 'abc', done: false }
generator.next();//第二段代码继续执行
generator.next();//代码执行结束~

生成器替代迭代器

既然生成器就是迭代器,那么在一些使用迭代器的地方,我们同样可以使用生成器去替代迭代器。 1.使用生成器去遍历一个数组

const names = ["abc", "cba", "nba"];
// function* namesGenator(arr) {
//   for (const item of arr) {
//     yield item;
//   }
// }
function* namesGenator(arr) {
  yield* arr;
}
const namesIterator = namesGenator(names);
console.log(namesIterator.next()); //{ value: 'abc', done: false }
console.log(namesIterator.next()); //{ value: 'cba', done: false }
console.log(namesIterator.next()); //{ value: 'cba', done: false }

2.在可迭代对象中使用生成器替代迭代器

class Person {
  constructor(name, age, friends) {
    this.name = name;
    this.age = age;
    this.friends = friends;
  }
  *[Symbol.iterator]() {
    yield* this.friends;
  }
}

const person = new Person("wangyi", 18, ["james", "kebi", "xiaohan"]);
for (const friend of person) {
  console.log(friend);//"james" "kebi" "xiaohan"
}

通过生成器替代迭代器,可以去除迭代器繁复的写法,代码也更清晰。

生成器与迭代器的用途

1.通过给对象添加 iterator protocol协议的方法,我们可以将某类自定义的对象变成可迭代对象。

2.在async/await 出现之前,可以通过Promise + 生成器 的组合,可以用于解决回调地狱问题.实际上,async/await 就是这种组合的语法糖

// 需求: 
// 1> url: why -> res: why
// 2> url: res + "aaa" -> res: whyaaa
// 3> url: res + "bbb" => res: whyaaabbb
function requestData(url) {
  // 异步请求的代码会被放入到executor中
  return new Promise((resolve, reject) => {
    // 模拟网络请求
    setTimeout(() => {
      // 拿到请求的结果
      resolve(url)
    }, 2000);
  })
}
function* getData() {
  const res1 = yield requestData("why")
  const res2 = yield requestData(res1 + "aaa")
  const res3 = yield requestData(res2 + "bbb")
  const res4 = yield requestData(res3 + "ccc")
  console.log(res4)
}

const generator = getData()

generator.next().value.then(res => {
  generator.next(res).value.then(res => {
    generator.next(res).value.then(res => {
      generator.next(res)
    })
  })
})

async/await

async/await 是Es8

异步函数的写法

async function foo1() {
}

const foo2 = async () => {
}

class Foo {
  async bar() {
  }
}

异步函数 的执行流程

1.异步函数的内部代码执行过程和普通的函数是一致的,默认情况下也是会被同步执行。

async function foo() {
  console.log("foo function start~")

  console.log("内部的代码执行1")
  console.log("内部的代码执行2")
  console.log("内部的代码执行3")

  console.log("foo function end~")
}


console.log("script start")
foo()
console.log("script end")

2.当异步函数中有await 关键字的使用时,才会与一般函数的执行流程有区别。

异步函数 的返回值

异步函数有返回值时,和普通函数会有区别:

  1. 异步函数也可以有返回值,但是异步函数的返回值会被包裹到Promise.resolve中;
  2. 如果我们的异步函数的返回值是Promise,Promise.resolve的状态会由Promise决定;
  3. 如果我们的异步函数的返回值是一个对象并且实现了thenable,那么会由对象的then方法来决定;
  4. 异步函数的返回值一定是一个Promise
async function foo() {
  console.log("foo function start~")

  console.log("中间代码~")

  console.log("foo function end~")

  // 1.返回一个值

  // 2.返回thenable
  // return {
  //   then: function(resolve, reject) {
  //     resolve("hahahah")
  //   }
  // }

  // 3.返回Promise
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve("hehehehe")
    }, 2000)
  })
}

// 异步函数的返回值一定是一个Promise
const promise = foo()
promise.then(res => {
  console.log("promise then function exec:", res)
})

异步函数的异常处理

如果我们在async中抛出了异常,那么程序它并不会像普通函数一样报错,而是会作为Promise的reject来传递;

async function foo() {
  console.log("foo function start~")

  console.log("中间代码~")

  // 异步函数中的异常, 会被作为异步函数返回的Promise的reject值的
  throw new Error("error message")

  console.log("foo function end~")
}

// 异步函数的返回值一定是一个Promise
foo().catch(err => {
  console.log("coderwhy err:", err)
})

console.log("后续还有代码~~~~~")

await 关键字

async函数另外一个特殊之处就是可以在它内部使用await关键字,而普通函数中是不可以的

await 关键字的特点

通常使用await是后面会跟上一个表达式,这个表达式会返回一个Promise;

那么await会等到Promise的状态变成fulfilled状态,之后继续执行异步函数;

如果await后面是一个普通的值,那么会直接返回这个值;

如果await后面是一个thenable的对象,那么会根据对象的then方法调用来决定后续的值;

如果await后面的表达式,返回的Promise是reject的状态,那么会将这个reject结果直接作为函数的Promise的 reject值;

// 1.await跟上上表达式
function requestData() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      // resolve(222)
      reject(1111);
    }, 2000);
  });
}

async function foo() {
  const res1 = await requestData()
  console.log("后面的代码1", res1)
  console.log("后面的代码2")
  console.log("后面的代码3")

  const res2 = await requestData()
  console.log("res2后面的代码", res2)
}

// 2.跟上其他的值
async function foo() {
  // const res1 = await 123
  // const res1 = await {
  //   then: function(resolve, reject) {
  //     resolve("abc")
  //   }
  // }
  const res1 = await new Promise((resolve) => {
    resolve("why");
  });
  console.log("res1:", res1);
}

// 3.reject值
async function foo() {
  const res1 = await requestData();
  console.log("res1:", res1);
}

foo().catch((err) => {
  console.log("err:", err);
});