前端面試複習2:迭代器,生成器與非同步迭代器

全菜工程師發表於2019-03-20

最近在刷面試題,這次學習下 迭代器,生成器與非同步迭代器(頭大)。

話不多說開始擼:

原文地址:我的部落格

Iterator 迭代器

iterator 是一個特殊的物件,它有用一個 next 方法,next 方法返回一個物件。

這個物件包含兩個屬性:

  1. value: any,表示成員的值
  2. done: boolean,表示迭代器是否結束

iterator.next() // 返回 {value: '', done: false}

迭代器內部會儲存一個指標,指向迭代器的成員位置,每呼叫一次 next 方法,指標就會移動到下一個成員

直到指標指向迭代器最後一個成員後面的位置

這時,done 的值為 truevalue 的值一般為 undefined,需要根據 iterator 的實際實現來決定。

簡單實現一個迭代器類

成員屬性介紹:

  • private point: number = 0 指標 初始為0
  • private params: any[] | Object 傳入可迭代資料
  • private keys: any[] 可迭代資料的 key
  • private length: number 可迭代資料的 key 的長度
class MyIterator {
  private point: number = 0;
  private params: any[] | Object;
  private keys: any[];
  private length: number;

  constructor(params: any[] | Object) {
    this.params = params;
    this.keys = Object.keys(params);
    this.length = this.keys.length;
  }

  public next(): { done: boolean; value: any; } {
    const done = this.point >= this.length;
    const value = done? undefined : this.params[this.keys[this.point]];
    if (!done) this.point++;
    return {
      done,
      value,
    }
  }
}

const iterator = new MyIterator([1,2,3]);

console.log(1, iterator.next()); // 1 { done: false, value: 1 }
console.log(2, iterator.next()); // 2 { done: false, value: 2 }
console.log(3, iterator.next()); // 3 { done: false, value: 3 }
console.log(4, iterator.next()); // 4 { done: true, value: undefined }
console.log(5, iterator.next()); // 5 { done: true, value: undefined }
複製程式碼

iterator介面

首先介紹下 for...infor...of 區別

  1. 推薦在迴圈物件屬性的時候,使用 for...in會遍歷到原型鏈),在遍歷陣列的時候的時候使用 for...ofObject.getOwnPropertyNames() 只會拿到自身可列舉非可列舉的屬性)
  2. for...in 迴圈出的是 keyfor...of 迴圈出的是 value
  3. for...of 是ES6新引入的特性
  4. for...of 不能迴圈沒有 [Symbol.iterator] 屬性的物件,需要通過和 Object.keys() 搭配使用

擁有了 iterator 介面的資料結構,也就是具有 Symbol.iterator 方法的資料結構,就可以被 for...of 遍歷。

Symbol.iterator 方法類似於上面實現的 MyIterator

  1. 陣列天生部署了迭代器介面
const array = [1, 2, 3];
typeof array[Symbol.iterator] // 'function'

for (const val of array) {
    console.log(val);
}
// 1
// 2
// 3
複製程式碼
  1. 物件沒有迭代器介面
const obj = {a: 'a1', b: 'b1', c: 'c1'};
typeof obj[Symbol.iterator] // 'undefined'

for (const val of obj) {
    console.log(val);
}
// VM974:4 Uncaught TypeError: obj is not iterable

obj[Symbol.iterator] = function() {
    const keys = Object.keys(this);
    const len = keys.length;
    let pointer = 0;
    return {
        next() {
            const done = pointer >= len;
            const value = !done ? self[this[pointer++]] : undefined;
            return {
                value,
                done
            };
        }
    }
}
for (const val of obj) {
    console.log(val);
}
// 'a'
// 'b'
// 'c'
複製程式碼
  1. 當迭代器遍的狀態已經完成時候,不會再更改狀態

Generator 生成器

Generator 是一個特殊的函式,函式體內部使用 yield 表示式,定義不同的內部狀態。

當執行 Generator 函式時,不會直接執行函式體,而是會返回一個 迭代器物件(iterator)

  1. Generator 函式內部可以使用 yield 表示式,定義內部狀態
  2. function 關鍵字與函式名之間有一個 *
function* generator() {
  yield 1;
  yield 2;
  return 3;
}
const myIterator = generator();
// 當呼叫iterator的next方法時,函式體開始執行,
console.log(myIterator.next()); // {value: 1, done: false}
console.log(myIterator.next()); // {value: 2, done: false}
console.log(myIterator.next()); // {value: 3, done: true}

for (const val of myIterator) {
    console.log(val); // 不會觸發
}
複製程式碼

for...of 之後,迭代器的狀態會關閉。

實現箇中序遍歷(網上看到的,很屌)

function* traverseTree(node) {
    if (node == null) return;
    yield* traverseTree(node.left);
    yield node.value;
    yield* traverseTree(node.right);
}
複製程式碼

gennerator巢狀

生成器函式中使用生成器函式 需要使用 *

當我們想在 generator b 中巢狀 generator a 時,怎麼巢狀呢?

yield *a(); ==> yield 1,實際上就是把 yield 1 放在這個位置

所以在生成器函式中使用生成器函式 需要使用 *

function* a(){
    yield 1;
}
function* b(){
    yield* a();
    yield 2;
}
let it = b();
console.log(it.next()); // { value: 1, done: false }
複製程式碼

實現非同步迭代

// ajax 是一個返回 Promise 的函式
function ajaxName() {
  return Promise.resolve('測試');
}
function ajaxSex() {
  return Promise.resolve('男');
}
function ajaxSchool() {
  return Promise.resolve('遼寧大學');
}

function * fetchInfo () {
  const name = yield ajaxName();
  const sex = yield ajaxSex();
  const school = yield ajaxSchool();
}

const info = fetchInfo();

// 加入的控制程式碼
info.next().value.then(name => {
  console.log(name); // 測試
  return info.next().value;
}).then(sex => {
  console.log(sex); // 男
  return info.next().value;
}).then(school => {
  console.log(school); // 遼寧大學
});
複製程式碼

async/await 非同步迭代器

  • async/await 其實相當於簡化了 Generator

更改下上面的非同步迭代器:

// 相同
async function fetchInfo () {
  const name = await ajaxName();
  console.log(name); // 測試
  const sex = await ajaxSex();
  console.log(sex); // 男
  const school = await ajaxSchool();
  console.log(school); // 遼寧大學
}
複製程式碼
  • await 只能用在 async 關鍵詞的函式中

  • async 函式返回一個 Promise

  • async/await 相當於封裝了 Promise

await 到底在等啥?

MDN文件

還是這個例子:

async function fetchInfo () {
  const name = await ajaxName();
  console.log(name); // 測試
  const sex = await ajaxSex();
  console.log(sex); // 男
  const school = await ajaxSchool();
  console.log(school); // 遼寧大學
}
複製程式碼
  1. await 表示式會暫停當前 async function 的執行,等待 Promise 處理完成。
  2. Promise 正常處理(fulfilled),其**建構函式第一個引數 resolve 函式的引數(resolve(value))**作為 await 表示式的值,繼續執行 async function
  3. Promise 處理異常(rejected),await 表示式會把 Promise 的異常原因丟擲。
  4. 另外,如果 await 操作符後的表示式的值不是一個 Promise,則返回該值本身。a = await 10 a就是10

上面的非同步函式相當於:

function fetchInfo() {
  let name, sex, school;

  return ajaxName().then(name => {
    name = name;
    console.log(name); // 測試
    return ajaxSex();
  }).then(sex => {
    sex = sex;
    console.log(sex); // 男
    return ajaxSchool();
  }).then(school => {
    school = school;
    console.log(school); // 遼寧大學
  });
}
複製程式碼

async/await 併發

我們的程式碼在執行到 await 的時候會等待結果返回才執行下一行,這樣如果我們有很多需要非同步執行的操作就會變成一個序列的流程,可能會導致非常慢。

比如如下程式碼,我們需要遍歷獲取 redis 中儲存的100個使用者的資訊:

const ids = [1,2,3,4];
const users=[];
for (let i=0;i<ids.length;i++) {
  users.push(await db.get(ids));
}
複製程式碼

由於每次資料庫讀取操作都要消耗時間,這個介面將會變得非常慢。

如果我們把它變成一個並行的操作,將會極大提升效率

const ids = [1,2,3,4];
const users=[];
const p = ids.map(async (id) => await db.get(id)); // any[]
const users = await Promise.all(p); // any[]
複製程式碼

總結

  1. Iterator 是一個可迭代介面,任何實現了此介面的資料結構都可以被 for...of 迴圈遍歷
  2. Generator 是一個可以暫停和繼續執行的函式體,可以完全實現 Iterator 的功能,並且由於可以儲存上下文,非常適合實現簡單的狀態機。另外通過一些流程控制程式碼的配合,可以比較容易進行非同步操作。
  3. Async/Await 就是 Generator 進行非同步操作並封裝了 Promise的語法糖。返回值為 Promise 的函式。

相關文章