ES6之Iterator、Generator

angelayun發表於2017-07-16

Iterator的作用

一是為各種資料結構,提供一個統一的、簡便的訪問介面;(統一)
二是使得資料結構的成員能夠按某種次序排列;(按序
三是ES6創造了一種新的遍歷命令for…of迴圈,Iterator介面主要供for…of消費。
舉個例子:遍歷器生成函式,作用就是返回一個遍歷器物件,next方法返回一個物件,表示當前資料成員的資訊。這個物件具有value和done兩個屬性,value屬性返回當前位置的成員,done屬性是一個布林值,表示遍歷是否結束

function makeIterator(array) {
  var nextIndex = 0;
  return {
    next: function() {
      return nextIndex < array.length ?
        {value: array[nextIndex++], done: false} :
        {value: undefined, done: true};
    }
  };
}

對於遍歷器物件來說,done: false和value: undefined屬性都是可以省略的,所以上述程式碼可以簡寫為:

function makeIterator(array) {
  var nextIndex = 0;
  return {
    next: function() {
      return nextIndex < array.length ?
        {value: array[nextIndex++]} :
        {done: true};
    }
  };
}

Iterator 只是把介面規格加到資料結構之上,所以,遍歷器與它所遍歷的那個資料結構,實際上是分開的,完全可以寫出沒有對應資料結構的遍歷器物件,或者說用遍歷器物件模擬出資料結構

Iterator介面

預設的 Iterator 介面部署在資料結構的Symbol.iterator屬性,Symbol.iterator屬性本身是一個函式,就是當前資料結構預設的遍歷器生成函式。執行這個函式,就會返回一個遍歷器
原生具備 Iterator 介面的資料結構如下:
1)Array
2)Map
3)Set
4)String
5)TypedArray
6)函式的 arguments 物件
對於部署了Iterator介面的資料結構除了可以使用for of迴圈之外,可以用while判斷物件的done屬性進行迴圈遍歷
怎麼樣使用原生的遍歷器呢?

let arr = [`a`, `b`, `c`];
let iter = arr[Symbol.iterator]();

iter.next()

//第二個例子:
var someString = "hi";
typeof someString[Symbol.iterator]
// "function"

var iterator = someString[Symbol.iterator]();

iterator.next()  // { value: "h", done: false }

對於類似陣列的物件應該怎樣呼叫陣列的Symbol.iterator方法?

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 set = new Set().add(`a`).add(`b`).add(`c`);
let [first, ...rest] = set;
// first=`a`; rest=[`b`,`c`];
//對陣列和 Set 結構進行解構賦值時,會預設呼叫Symbol.iterator方法

擴充套件運算子

var str = `hello`;
[...str] //  [`h`,`e`,`l`,`l`,`o`]
//擴充套件運算子(...)也會呼叫預設的 Iterator 介面

yield*

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

var iterator = generator();

iterator.next()

遍歷器的return和throw方法

遍歷器物件生成函式,next方法是必須部署的,return方法和throw方法是否部署是可選的。return方法必須返回一個物件

function readLinesSync(file) {
  return {
    next() {
      if (file.isAtEndOfFile()) {
        file.close();
        return { done: true };
      }
    },
    return() {
      file.close();
      return { done: true };
    },
  };
}
for (let line of readLinesSync(fileName)) {
  console.log(line);
  break;//我們讓檔案的遍歷提前返回,這樣就會觸發執行return方法
}

for of 迴圈優點

forEach沒辦法跳出迴圈,也就是說在forEach當中break命令或return命令都不能奏效
for…in迴圈不僅遍歷數字鍵名,還會遍歷手動新增的其他鍵,甚至包括原型鏈上的鍵,而且for in 是沒有順序的(也就是說for…in迴圈主要是為遍歷物件而設計的,不適用於遍歷陣列)

小訣竅:
1、並不是所有類似陣列的物件都具有 Iterator 介面,一個簡便的解決方法,就是使用Array.from方法將其轉為陣列

let arrayLike = { length: 2, 0: `a`, 1: `b` };

// 報錯
for (let x of arrayLike) {
  console.log(x);
}

// 正確
for (let x of Array.from(arrayLike)) {
  console.log(x);
}

2、因為普通物件並沒有部署Iterator介面,所以是無法使用for of迴圈的,用以下兩種方案解決:

for (var key of Object.keys(someObject)) {
  console.log(key + `: ` + someObject[key]);
}

//第二種方式:
function* entries(obj) {
  for (let key of Object.keys(obj)) {
    yield [key, obj[key]];
  }
}
for (let [key, value] of entries(obj)) {
  console.log(key, `->`, value);
}
//第三種方式:
function* objectEntries(obj) {
  let propKeys = Reflect.ownKeys(obj);
  for (let propKey of propKeys) {
    yield [propKey, obj[propKey]];
  }
}
for (let [key, value] of objectEntries(obj)) {
  console.log(`${key}: ${value}`);
}
//或者也可以這樣
let jane = { first: `Jane`, last: `Doe` };

jane[Symbol.iterator] = objectEntries;//這句話是重點將 Generator 函式加到物件的Symbol.iterator屬性上面

for (let [key, value] of jane) {
  console.log(`${key}: ${value}`);
}

Generator簡介

在我看來Generator就是為了非同步程式設計提供一種解決方案;
Generator 函式是一個普通函式,事實上它是遍歷器生成函式,但有幾個特性:
1、function關鍵字與函式名之間有一個星號(星號緊跟在function關鍵字後面);
2、函式體內部使用yield表示式,定義不同的內部狀態(yield在英語裡的意思就是“產出” 1)yield只能在Generator函式中使用;2)yield表示式如果用在另一個表示式之中,必須放在圓括號裡面;3)yield表示式用作函式引數或放在賦值表示式的右邊,可以不加括號);
3、可以把Generator函式理解為狀態機,呼叫 Generator 函式後,該函式並不執行,返回的也不是函式執行結果,而是一個指向內部狀態的指標物件,每次呼叫next方法,內部指標就從函式頭部或上一次停下來的地方開始執行,直到遇到下一個yield表示式(或return語句)為止;Generator 函式是分段執行的,yield表示式是暫停執行的標記,而next方法可以恢復執行。

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

[...myIterable]

Generator 函式執行後,返回一個遍歷器物件。該物件本身也具有Symbol.iterator屬性,執行後返回自身。

function* gen(){
  // some code
}

var g = gen();

g[Symbol.iterator]() === g

Generator中next方法的引數

通過next方法的引數,就有辦法在 Generator 函式開始執行之後,繼續向函式體內部注入值
由於next方法的參數列示上一個yield表示式的返回值,所以第一次使用next方法時,不能帶有引數。V8 引擎直接忽略第一次使用next方法時的引數,只有從第二次使用next方法開始,引數才是有效的
舉個簡單的例子吧:

function* dataConsumer() {
  console.log(`Started`);
  console.log(`1. ${yield}`);
  console.log(`2. ${yield}`);
  return `result`;
}

let genObj = dataConsumer();
genObj.next();
// Started
genObj.next(`a`)
// 1. a
genObj.next(`b`)
// 2. b

複雜一點的:

function* foo(x) {
  var y = 2 * (yield (x + 1));
  var z = yield (y / 3);
  return (x + y + z);
}

var a = foo(5);
a.next() // Object{value:6, done:false}
a.next() // Object{value:NaN, done:false}
a.next() // Object{value:NaN, done:true}

var b = foo(5);
b.next() // { value:6, done:false }
b.next(12) // { value:8, done:false }
//解釋一下結果y=2*12 所以返回的是24/3
b.next(13) // { value:42, done:true }
//y=24 z=13 所以返回的是5+24+13

說說for of與Generator的關係吧

有一點需要注意的是當next方法中返回done為true則終止迴圈且不包含該返回物件,所以上面程式碼的return語句返回的6,不包括在for…of迴圈之中

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

for (let v of foo()) {
  console.log(v);
}

Generator中的throw方法

Generator 函式返回的遍歷器物件,都有一個throw方法,可以在函式體外丟擲錯誤,然後在 Generator 函式體內捕獲,throw方法可以接受一個引數,該引數會被catch語句接收

var g = function* () {
  try {
    yield;
  } catch (e) {
    console.log(e);
  }
};

var i = g();
i.next();
i.throw(new Error(`出錯了!`));
// Error: 出錯了!(…)

throw方法被捕獲以後,會附帶執行下一條yield表示式。也就是說,會附帶執行一次next方法。

var gen = function* gen(){
  try {
    yield console.log(`a`);
  } catch (e) {
    // ...
  }
  yield console.log(`b`);
  yield console.log(`c`);
}

var g = gen();
g.next() // a
g.throw() // b
g.next() // c

Generator中的throw有什麼優勢呢?多個yield表示式,可以只用一個try…catch程式碼塊來捕獲錯誤,大大方便了對錯誤的處理。

一旦 Generator 執行過程中丟擲錯誤,且沒有被內部捕獲,就不會再執行下去了

function* g() {
  yield 1;
  console.log(`throwing an exception`);
  throw new Error(`generator broke!`);
  yield 2;
  yield 3;
}

如上所示程式碼:因為丟擲了異常generator broke,所以後面的2 3都不會返回

Generator return

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 }

g呼叫return方法後,返回值的value屬性就是return方法的引數foo。並且,Generator函式的遍歷就終止了,返回值的done屬性為true,以後再呼叫next方法,done屬性總是返回true

另外一個特殊情況: Generator 函式內部有try…finally程式碼塊,那麼return方法會推遲到finally程式碼塊執行完再執行。也就是說:呼叫return方法後,就開始執行finally程式碼塊,然後等到finally程式碼塊執行完,再執行return方法

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 }

yield*表示式

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

let delegatedIterator = (function* () {
  yield `Hello!`;
  yield `Bye!`;
}());

let delegatingIterator = (function* () {
  yield `Greetings!`;
  yield* delegatedIterator;
  yield `Ok, bye.`;
}());

for(let value of delegatingIterator) {
  console.log(value);
}
// "Greetings!
// "Hello!"
// "Bye!"
// "Ok, bye."

Generator函式中的this

Generator函式不能跟new命令一起用
怎樣讓Generator 函式返回一個正常的物件例項,既可以用next方法,又可以獲得正常的this?

function* F() {
  this.a = 1;
  yield this.b = 2;
  yield this.c = 3;
}
//這是第一種方式
var obj = {};
var f = F.call(obj);//讓F內部的this物件繫結obj物件

//第二種方式
var f = F.call(F.prototype);

f.next();  // Object {value: 2, done: false}
f.next();  // Object {value: 3, done: false}
f.next();  // Object {value: undefined, done: true}

obj.a // 1
obj.b // 2
obj.c // 3

相關文章