亚洲在线久爱草,狠狠天天香蕉网,天天搞日日干久草,伊人亚洲日本欧美

ES6+ Generator 基礎

1. 前言

前面我們花了三節深入地學習了 ES6 的異步解決方案 Promise,本節學習的生成器也是為了解決異步而生的,但是它的出發思路和 Promise 截然不同。

上節我們學習了 ES6 中 迭代 的相關內容,并實現了一個迭代器。我們知道實現一個迭代器,我們需要手動添加對象的 Symbol.iterator 屬性,并需要實現 next 方法。那么有沒有什么可以幫助我們自動實現迭代器呢?ES6 給出了生成器的方法來滿足我們的需求。我們不需要在對象上添加 Symbol.iterator 屬性,使用生成器函數就可以實現迭代器的功能。本節我們將學習生成器的相關概念和基礎用法。

生成器是一個靈活的結構,能使得一個函數塊內部暫停和恢復代碼執行的能力。在實際應用中,使用生成器可以自定義迭代器和協程。

2. 生成器對象和生成器函數

有些概念是我們必須要理解的,前面在學習迭代器的時候,我們學習了迭代協議和迭代器協議,實現一個迭代器需要滿足這兩個協議才算是一個真正的迭代器。而本節的生成器和生成器函數也是如此,我們也需要知道生成器對象和生成器函數概念和它們直接的關系。

Generator 就是我們說的生成器,它包含兩個概念 生成器對象和生成器函數。首先,要理解的是生成器對象和迭代器的關系,生成器對象是遵守迭代協議和迭代器協議實現的 Iterable 接口,可以理解生成器對象其實也是一個迭代器;然后,我們需要理解什么是生成器函數,生成器函數是由 function * 來定義的,并且返回結果是一個 Generator 對象。

生成器是一個特殊的函數,在調用后會返回一個生成器對象,這個生成器對象是遵守可迭代協議和迭代器協議實現的 Iterable 接口。生成器可以使用 yield 關鍵字來暫停執行的生成器函數:

function* generator() {
  yield 'a';
  yield 'b';
}

var gen = generator();	// Object [Generator] {}

2.1 Generator.prototype.next()

生成器的 next () 方法和迭代器返回的結果是一樣的,返回了一個包含屬性 donevalue 的對象,該方法也可以通過接受一個參數用以向生成器傳值。

使用 yield 返回的值會被迭代器的 next () 方法捕獲:

var gen = generator();

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

從上面代碼的執行結果可以看出,生成器函數在執行后會返回一個生成器對象,這個生成器對象滿足迭代協議和迭代器協議,所以我們可以去手動調用它的 next () 方法去獲取每一步的返回值。從這里可以看出,生成器其實就是迭代器的一個應用,并且這個應用會在異步中大放異彩。

2.2 Generator.prototype.return()

return() 方法返回給定的值并結束生成器。

var gen = generator();

gen.next();        // { value: 'a', done: false }
gen.return("imooc"); // { value: "imooc", done: true }
gen.next();        // { value: undefined, done: true }

另外,如果對已經完成狀態的生成器調用 return(value) 則生成器會一直保持在完成狀態,如果出入參數,value 會設置成傳入的參數,done 的值不變:

var gen = generator();

gen.next(); // { value: 1, done: false }
gen.next(); // { value: 2, done: false }
gen.next(); // { value: undefined, done: true }
gen.return(); // { value: undefined, done: true }
gen.return(1); // { value: 1, done: true }

2.2 Generator.prototype.throw()

throw() 方法用來向生成器拋出異常,并恢復生成器的執行,返回帶有 donevalue 兩個屬性的對象。

function* generator() {
  while(true) {
    try {
       yield 'imooc'
    } catch(e) {
      console.log("Error caught!");
    }
  }
}
var gen = generator();
gen.next(); // { value: "imooc", done: false }
gen.throw(new Error("error")); // "Error caught!"

3. Generator 案例

3.1 類數組轉化

將一個類數組轉化為一個真正的數組方式有很多,ES6 提供了 Array.from() 可以將類數組轉化為數組 。另外在一些函數中可以使用 [...argument] 的方式轉化類數組。

function fn() {
  const arg = [...arguments];
  console.log(arg);
}
fn(1, 2, 3);	// [1, 2, 3]

當然我們知道類數組的定義,所以我們自己定義一個類數組,看能不能使用展開運算符將類數組轉化為數組:

const likeArr = {
  0: 1,
  1: 2,
  length: 2,
}
console.log([...likeArr]);	// Uncaught TypeError: likeArr is not iterable

上面代碼中我們定義了一個類數組,但是使用展開運算符報錯了,提示我們 likeArr 不是一個迭代器。因為在函數中類數組是內部幫我們實現了迭代器的功能,而我們自己定義的類數組是不具有迭代器功能的,那我們來自己實現一個:

likeArr[Symbol.iterator] = function() {
  let index = 0;
  return {
    next: () => {
      return { value: this[index], done: index++ === this.length}
    }
  }
}
console.log([...likeArr]);	// [1, 2]

上面的代碼我們在 likeArr 對象上定義了 Symbol.iterator 它具有迭代功能。上面代碼中我們需要手動地去實現 next () 方法,這比較麻煩,那能不能簡化一下呢?我們的生成器函數就出場了:

likeArr[Symbol.iterator] = function* () {
  let index = 0;
  while (index !== this.length) {
    yield this[index++];
  }
}
console.log([...likeArr]);	// [1, 2]

上面的代碼使用了生成器函數,并且沒有去手動實現 next () 方法,從這里我們也能很清楚地知道迭代器和生成器的關系。而且使用生成器函數更加簡單方便。

3.2 單步獲取質數

還有一個案例是面試中經常會考到的:

題目:實現一個函數,每次調用返回下一個質數,要求不使用全局變量,且函數本身不接受任何參數

從題目的要求可以知道,這個函數每次調用都會返回一個質數,也就是說每次調用后都會返回一個函數。

首先我們定義一個判斷一個數是否為質數的方法:

function isPrime(num) {
  for (let i = 2; i <= Math.sqrt(num); i++) {
    if (num % i === 0) {
      return false
    }
  }
  return true
}

傳統的方式是使用閉包方法來解決:

function primeHandler() {
  let prime = 1
  return () => {
    while (true) {
      prime++
      if (isPrime(prime)) {
        return prime
      }
    }
  }
}

const getPrime = primeHandler()
console.log(getPrime());	// 2
console.log(getPrime());	// 3
console.log(getPrime());	// 5

既然是單步執行的,那么我們就可以使用迭代器方式實現:

var prime = {}
prime[Symbol.iterator] = function() {
  let prime = 1;
  return {
    next() {
      while(true) {
        prime++
        if (isPrime(prime)) {
          return prime;
        }
      }
    }
  }
}
var getPrime = prime[Symbol.iterator]().next;
console.log(getPrime());	// 2
console.log(getPrime());	// 3

上一個實例我們知道實現迭代器的方式是很麻煩的,可以使用生成器函數去替代迭代器的功能,所以上面的代碼可以使用生成器函數改造如下:

function* primeGenerator () {
  let prime = 1
  while (true) {
    prime++
    if (isPrime(prime)) {
      yield prime
    }
  }
}

var getPrime = primeGenerator().next().value
console.log(getPrime());	// 2
console.log(getPrime());	// 3

4. 小結

本節我們主要學習了生成器的概念和用法,需要生成器對象是由生成器函數返回的結果,生成器對象是遵守迭代協議和迭代器協議實現的 Iterable 接口。生成器其實就是對迭代器的應用。另外,通過兩個案例更加深刻地理解了生成器的應用場景,對比了生成器和迭代器的不同。