JS 中的 Iterator, Generator, async

wopen發表於2019-03-03

Iterator

它是一種介面,為各種不同的資料結構提供統一的訪問機制。任何資料結構只要部署 Iterator 介面,就可以完成遍歷操作。

Iterator 的遍歷過程:

  1. 建立一個指標物件,指向當前資料結構的起始位置。
  2. 不斷呼叫指標物件的next方法

每一次呼叫next方法,都會返回資料結構的當前成員的資訊。(返回一個包含value和done兩個屬性的物件。其中,value屬性是當前成員的值,done屬性是一個布林值,表示遍歷是否結束。)

ES6 規定,一個資料結構只要具有Symbol.iterator屬性,就可以認為是“可遍歷的”。Symbol.iterator屬性本身是一個函式,就是當前資料結構預設的遍歷器生成函式。(Symbol

Symbol.iterator,它是一個表示式,返回Symbol物件的iterator屬性,這是一個預定義好的、型別為Symbol的特殊值。

const obj = {
  [Symbol.iterator] : function () {
    return {
      next: function () {
        return {
          value: 1,
          done: true
        };
      }
    };
  }
}

class LinkedList {
    [Symbol.iterator] () {
        return {
            next () {
                return { value: 1, done: true }
            }
        }
    }
}
複製程式碼

原生資料結構部署了遍歷器介面有:Array Map Set String TypedArray 函式的 arguments 物件 NodeList 物件

for...of

只要部署了遍歷器介面就可以使用for...of遍歷。

class RangeIterator {
  constructor(start, stop) {
    this.value = start;
    this.stop = stop;
  }

  [Symbol.iterator]() { return this; }

  next() {
    var value = this.value;
    if (value < this.stop) {
      this.value++;
      return {done: false, value: value};
    }
    return {done: true, value: undefined};
  }
}

function range(start, stop) {
  return new RangeIterator(start, stop);
}

for (var value of range(0, 3)) {
  console.log(value); // 0, 1, 2
}

let iterable = {
  0: 'a',
  1: 'b',
  2: 'c',
  length: 3,
  [Symbol.iterator]: Array.prototype[Symbol.iterator]
};
for (let item of iterable) {
  console.log(item); // 'a', 'b', 'c'
}
複製程式碼

以下場景會呼叫 Iterator 介面

let [first, ...rest] = [1,2,3] // 解構賦值

[...'hi'] // 擴充套件運算子

let generator = function* () {
  yield* [1,2,3] // yield*
};

// for...of
// Array.from()
// Map(), Set(), WeakMap(), WeakSet()(比如new Map([['a',1],['b',2]]))
// Promise.all() Promise.race()
複製程式碼

遍歷器返回物件除了next方法還有returnthrow方法,它們是可選的。

return方法的使用場合是,如果for...of迴圈提前退出(通常是因為出錯,或者有break語句),就會呼叫return方法。return方法必須返回一個物件。

Generator

Generator 函式是 ES6 提供的一種非同步程式設計解決方案,執行 Generator 函式會返回一個遍歷器物件。

Generator 函式使用function*定義(有沒有空格都行),內部可以使用yieldyield*表示式。

function* g() {
    yield 1
    yield 2
    return 3
    yield 4
}

let a = g()
// 呼叫 Generator 函式後,該函式並不執行,返回的也不是函式執行結果,而是一個指向內部狀態的指標物件,遍歷器物件

console.log(a)

/*
__proto__: Generator
    __proto__: Generator
    constructor: GeneratorFunction {prototype: Generator, constructor: ƒ, Symbol(Symbol.toStringTag): "GeneratorFunction"}
    next: ƒ next()
    return: ƒ return()
    throw: ƒ throw()
    Symbol(Symbol.toStringTag): "Generator"
    __proto__: Object
[[GeneratorLocation]]: VM89:1
[[GeneratorStatus]]: "suspended"
[[GeneratorFunction]]: ƒ* g()
[[GeneratorReceiver]]: Window
[[Scopes]]: Scopes[2]
*/

// 必須呼叫遍歷器物件的next方法,使得指標移向下一個狀態。也就是說,每次呼叫next方法,內部指標就從函式頭部或上一次停下來的地方開始執行,直到遇到下一個yield表示式(或return語句)為止。

a.next() // { value: 1, done: false }
a.next() // { value: 2, done: false }
a.next() // { value: 3, done: true }
a.next() // { value: undefined, done: true }
複製程式碼

Generator 函式是分段執行的,yield表示式是暫停執行的標記,而next方法可以恢復執行,當碰到yield就返回yield後面的值和donefalse,如果遇到return就返回return後的值和donetrue的物件,如果沒有碰到yieldreturn則返回值為undefineddonetrue的物件。

yield表示式後面的表示式,只有當呼叫next方法、內部指標指向該語句時才會執行,因此等於為 JavaScript 提供了手動的“惰性求值”(Lazy Evaluation)的語法功能。

yield表示式如果用在另一個表示式之中,必須放在圓括號裡面,表示式用作函式引數或放在賦值表示式的右邊,可以不加括號。

function* demo() {
  console.log('Hello' + yield); // SyntaxError
  console.log('Hello' + yield 123); // SyntaxError

  console.log('Hello' + (yield)); // OK
  console.log('Hello' + (yield 123)); // OK
}

function* demo() {
  foo(yield 'a', yield 'b'); // OK
  let input = yield; // OK
}
複製程式碼

任意一個物件的Symbol.iterator方法,等於該物件的遍歷器生成函式,呼叫該函式會返回該物件的一個遍歷器物件,可以把 Generator 賦值給物件的Symbol.iterator屬性,從而使得該物件具有 Iterator 介面。

var myIterable = {};
myIterable[Symbol.iterator] = function* () {
  yield 1;
  yield 2;
  yield 3;
};

[...myIterable] // [1, 2, 3]
複製程式碼

yield可以有返回值,它通過next方法傳入。

function* g() {
    let v1 = yield
    console.log(v1)
    let v2 = yield 2
    console.log(v2)
}

let a = g()
a.next() // { value: undefined, done: false }
a.next([1,2,3]) // { value: 2, done: false }
a.next() // { value: undefined, done: true }

// [1,2,3]
// undefined
複製程式碼

上面的例子,第一個next方法執行到第一個yield暫停,執行第二次next方法時,我們用[1,2,3]做為引數,這時第一個yield就返回[1,2,3],所以第一次列印[1,2,3]第二次列印undefined

所以對第一個next方法傳遞引數是沒有用的,第二個next的引數才作為第一個yield的返回值。

for...of迴圈可以自動遍歷 Generator函式執行時生成的Iterator物件,且此時不再需要呼叫next方法。

function* foo() {
  yield 1;
  yield 2;
  yield 3;
  yield 4;
  yield 5;
  return 6;
}

for (let v of foo()) {
  console.log(v);
}
// 1 2 3 4 5
複製程式碼

只要donetruefor...of就會終止迴圈,所以return的返回值沒有列印。

Generator 函式原型上有throw方法,可以用來在函式體外丟擲錯誤,然後在 Generator 函式體內捕獲。

var g = function* () {
  try {
    yield;
  } catch (e) {
    console.log('內部捕獲', e);
  }
};

var i = g();
i.next();

try {
  i.throw('a'); // throw 方法可以接受一個引數,該引數會被catch語句接收
  i.throw('b'); // throw 方法會附帶執行一次 next 方法
} catch (e) {
  console.log('外部捕獲', e);
}
// 內部捕獲 a
// 外部捕獲 b
複製程式碼

如果內部沒有捕獲,那麼錯誤就會跑到外面來,被外部捕獲。如果內外都沒有捕獲錯誤,那麼程式將報錯,直接中斷執行。

在使用throw之前,必須執行一次next方法,否則丟擲的錯誤不會被內部捕獲,而是直接在外部丟擲,導致程式出錯。

函式體內的錯誤可以被外部捕獲。

function* g() {
    yield 1
    yield 2
    throw new Error('err')
    yield 3
}

let a = g()

try {
    console.log(a.next()) // { value: 1, done: tfalse }
    console.log(a.next()) // { value: 2, done: false }
    console.log(a.next()) // 報錯
} catch(e) {}

console.log(a.next()) // { value: undefined, done: true }
複製程式碼

只要內部錯誤沒有捕獲,跑到外面來,那麼迭代器就會自動停止。

Generator 函式返回的遍歷器物件,還有一個return方法,可以返回給定的值,並且終結遍歷 Generator 函式。

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

var g = gen();

g.next()        // { value: 1, done: false }
g.return('foo') // { value: "foo", done: true }
g.next()        // { value: undefined, done: true }

// ---------------
function* numbers () {
  yield 1;
  try {
    yield 2;
    yield 3;
  } finally {
    yield 4;
    yield 5;
  }
  yield 6;
}
var g = numbers();
g.next() // { value: 1, done: false }
g.next() // { value: 2, done: false }
g.return(7) // { value: 4, done: false }
g.next() // { value: 5, done: false }
g.next() // { value: 7, done: true }

// 如果 Generator 函式內部有try...finally程式碼塊,且正在執行try程式碼塊,那麼return方法會推遲到finally程式碼塊執行完再執行
複製程式碼

yield* 表示式,用來在一個 Generator 函式內部,呼叫另一個 Generator 函式。

function* bar() {
  yield 'x';
  yield* foo();
  yield 'y';
}

// 等同於
function* bar() {
  yield 'x';
  yield 'a';
  yield 'b';
  yield 'y';
}

// 等同於
function* bar() {
  yield 'x';
  for (let v of foo()) {
    yield v;
  }
  yield 'y';
}

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

function* g() {
    yield 1
    yield 2
    return 3
}

function* e() {
    let returnValue = yield* g()
    console.log(returnValue) // 3
    // 如果另一個函式帶有 return 則需要自己獲取
    
    yield* ['a', 'b', 'c'] // 還可以是 陣列,字串這些原生帶有迭代器的物件
}
複製程式碼

如果yield表示式後面跟的是一個遍歷器物件,需要在yield表示式後面加上星號,表明它返回的是一個遍歷器物件。

function* iterTree(tree) {
  if (Array.isArray(tree)) {
    for(let i=0; i < tree.length; i++) {
      yield* iterTree(tree[i]);
    }
  } else {
    yield tree;
  }
}

const tree = [ 'a', ['b', 'c'], ['d', 'e'] ];
[...iterTree(tree)]
複製程式碼

yield* 可以輕鬆的抹平陣列。

Generator 函式不能和new命令一起用。

Async

ES2017 標準引入了 async 函式,使得非同步操作變得更加方便。它可以非常清晰的將非同步操作寫成同步操作。

let p = async function getData() {
    let data = await db.getData()
    console.log(data)
}

console.log(p)
/*
Promise {<resolved>: undefined}
    __proto__: Promise
    [[PromiseStatus]]: "resolved"
    [[PromiseValue]]: undefined
*/

let b = async function () {} // 函式表示式
let c = async () => {} // 箭頭函式
複製程式碼

async 函式 以async開頭,其內部可以使用await等待非同步操作完成。async函式會返回一個Promise物件,所以可以使用then方法新增回撥函式。

函式執行的時候,一旦遇到await就會先返回,等到非同步操作完成,再接著執行函式體內後面的語句。

async函式內部return語句返回的值,會成為then方法回撥函式的引數,async函式內部丟擲錯誤,會導致返回的 Promise 物件變為reject狀態。丟擲的錯誤物件會被catch方法回撥函式接收到。

async function a() {
    throw new Error('err')
}

a() // Uncaught (in promise) Error 報錯,但不會終止程式

console.log(123) // 正常執行
複製程式碼

async函式返回的Promise物件,必須等到內部所有await命令後面的 Promise 物件執行完,才會發生狀態改變,除非遇到return語句或者丟擲錯誤。

  1. await命令後面一般是一個 Promise 物件,返回該物件的結果。如果不是 Promise 物件,就直接返回對應的值。
  2. await命令後面是一個thenable物件(即定義then方法的物件),那麼await會將其等同於 Promise 物件。
  3. 任何一個await語句後面的 Promise 物件變為reject狀態,那麼整個async函式都會中斷執行。
async function main() {
  try {
    const val1 = await firstStep();
    const val2 = await secondStep(val1);
    const val3 = await thirdStep(val1, val2);

    console.log('Final: ', val3);
  }
  catch (err) {
    console.error(err);
  }
} // 我們可以將多個 await 命令放入 try-catch 中
複製程式碼

async函式其實它就是 Generator 函式的語法糖。

async function fn(args) {
  // ...
}

// 等同於

function fn(args) {
  return spawn(function* () {
    // ...
  });
}

function spawn(genF) {
  return new Promise(function(resolve, reject) { // 返回一個 Promise 物件
    const gen = genF();
    function step(nextF) {
      let next;
      try {
        next = nextF();
      } catch(e) {
        return reject(e); // 執行 fn 中的程式碼,如果出錯直接 reject 返回的 Promise
      }
      if(next.done) { // 如果 fn 執行完成則 resolve fn return 的值
        return resolve(next.value);
      }
      Promise.resolve(next.value).then(function(v) { 
        // 將 yield 返回的值變成 Promise 並執行它的 then 方法
        step(function() { return gen.next(v); });
        // 當 yield 後面的 Promise 執行成功完成時則繼續執行 fn 函式
        // 並將它產生的值傳入 fn 函式
      }, function(e) {
        step(function() { return gen.throw(e); });
        // 如果出現錯誤則將錯誤傳入 fn 內部。
        // 如果內部沒有捕獲則被本函式上面的 try-catch 捕獲
      });
    }
    step(function() { return gen.next(undefined); });
  });
}
複製程式碼

非同步遍歷器

非同步遍歷器和遍歷器的區別在於,非同步遍歷器返回的是一個 Promise 物件,但是它的值的格式和遍歷器一樣。非同步遍歷器介面,部署在Symbol.asyncIterator屬性上面。

非同步遍歷器它的next不用等到上一個 Promise resolve 了才能呼叫。這種情況下,next方法會累積起來,自動按照每一步的順序執行下去。

const asyncIterable = createAsyncIterable(['a', 'b']);
const asyncIterator = asyncIterable[Symbol.asyncIterator]();
const [{value: v1}, {value: v2}] = await Promise.all([
  asyncIterator.next(), asyncIterator.next()
]);

console.log(v1, v2); // a b
複製程式碼

for await...of

for await...of迴圈,是用於遍歷非同步的 Iterator 介面。

async function f() {
  for await (const x of createAsyncIterable(['a', 'b'])) {
    console.log(x);
  }
}
// a
// b
複製程式碼

如果next方法返回的 Promise 物件被rejectfor await...of就會報錯,要用try...catch捕捉。

它也可以用於同步遍歷器。

非同步 Generator 函式

就像 Generator 函式返回一個同步遍歷器物件一樣,非同步 Generator 函式的作用,是返回一個非同步遍歷器物件。

async function* gen() {
  yield 'hello';
}
const genObj = gen();
genObj.next().then(x => console.log(x));
// { value: 'hello', done: false }

// 同步 Generator 函式
function* map(iterable, func) {
  const iter = iterable[Symbol.iterator]();
  while (true) {
    const {value, done} = iter.next();
    if (done) break;
    yield func(value);
  }
}

// 非同步 Generator 函式
async function* map(iterable, func) {
  const iter = iterable[Symbol.asyncIterator]();
  while (true) {
    const {value, done} = await iter.next();
    if (done) break;
    yield func(value);
  }
}
複製程式碼

Generator 函式處理同步操作和非同步操作時,能夠使用同一套介面。非同步 Generator 函式內部,能夠同時使用awaityield命令。

如果非同步 Generator 函式丟擲錯誤,會導致 Promise 物件的狀態變為reject,然後丟擲的錯誤被catch方法捕獲。

yield*

yield*語句也可以跟一個非同步遍歷器。

async function* gen1() {
  yield 'a';
  yield 'b';
  return 2;
}

async function* gen2() {
  // result 最終會等於 2
  const result = yield* gen1();
}

for await (const x of gen2()) {
  console.log(x);
}
// a
// b
複製程式碼

相關文章