JavaScript 設計模式(五):迭代器模式

迭代器模式

文章內容分兩部分:

  1. 前半部分爲 “迭代器模式” 概念;
  2. 後半部分爲 ES6 中 Iterator (迭代器)

上半部分開始...

迭代器模式:提供一種方法順序訪問一個聚合對象中的各個元素,而又不需要暴露該對象的內部表示。

簡單理解(白話理解):統一 “集合” 型數據結構的遍歷接口,實現可循環遍歷獲取集合中各數據項(不關心數據項中的數據結構)。

生活小栗子:清單 TodoList。每日清單有學習類、生活類、工作類、運動類等項目,清單列表只管羅列,不管類別。

模式特點

  1. 爲遍歷不同數據結構的 “集合” 提供統一的接口;
  2. 能遍歷訪問 “集合” 數據中的項,不關心項的數據結構

模式實現

// 統一遍歷接口實現
var each = function(arr, callBack) {
  for (let i = 0, len = arr.length; i < len; i++) {
    // 將值,索引返回給回調函數callBack處理
    if (callBack(i, arr[i]) === false) {
      break;  // 中止迭代器,跳出循環
    }
  }
}

// 外部調用
each([1, 2, 3, 4, 5], function(index, value) {
    if (value > 3) {
      return false; // 返回false中止each
    }
    console.log([index, value]);
})

// 輸出:[0, 1]  [1, 2]  [2, 3]

“迭代器模式的核心,就是實現統一遍歷接口。”

模式細分

  1. 內部迭代器 (jQuery 的 $.each / for...of)
  2. 外部迭代器 (ES6 的 yield)

內部迭代器

內部迭代器: 內部定義迭代規則,控制整個迭代過程,外部只需一次初始調用
// jQuery 的 $.each(跟上文each函數實現原理類似)
$.each(['Angular', 'React', 'Vue'], function(index, value) {
    console.log([index, value]);
});

// 輸出:[0, Angular]  [1, React]  [2, Vue]

優點:調用方式簡單,外部僅需一次調用
缺點:迭代規則預先設置,欠缺靈活性。無法實現複雜遍歷需求(如: 同時迭代比對兩個數組)

外部迭代器

外部迭代器: 外部顯示(手動)地控制迭代下一個數據項

藉助 ES6 新增的 Generator 函數中的 yield* 表達式來實現外部迭代器。

// ES6 的 yield 實現外部迭代器
function* generatorEach(arr) {
  for (let [index, value] of arr.entries()) {
    yield console.log([index, value]);
  }
}

let each = generatorEach(['Angular', 'React', 'Vue']);
each.next();
each.next();
each.next();

// 輸出:[0, 'Angular']  [1, 'React']  [2, 'Vue']

優點:靈活性更佳,適用面廣,能應對更加複雜的迭代需求
缺點:需顯示調用迭代進行(手動控制迭代過程),外部調用方式較複雜

適用場景

不同數據結構類型的 “數據集合”,需要對外提供統一的遍歷接口,而又不暴露或修改內部結構時,可應用迭代器模式實現。


下半部分開始...

ES6 的 Iterator 迭代器

“迭代器等同於遍歷器。在某些文章中,可能會出現遍歷器的字眼,其實兩者的意思一致。”

JavaScript 中 原有表示 “集合” 的數據結構主要是 “數組(Array)” 和 “對象(Object)”,ES6又新增了 MapSet,共四種數據集合,瀏覽器端還有 NodeList 類數組結構。爲 “集合” 型數據尋求統一的遍歷接口,正是 ES6 的 Iterator 誕生的背景。

ES6 中迭代器 Iterator 作爲一個接口,作用就是爲各種不同數據結構提供統一的訪問機制。任何數據結構只要部署了 Iterator 接口,就可以完成遍歷操作。

Iterator 作用:

  1. 爲各種數據結構,提供一個統一的、簡便的訪問接口;
  2. 使得數據結構的成員能夠按某種次序排列;
  3. 爲新的遍歷語法 for...of 實現循環遍歷

Iterator只是一種接口,與遍歷的數據結構是分開的。 重溫迭代器模式特點:我只要統一遍歷數據項的接口,不關心其數據結構。

ES6 默認的 Iterator 接口部署在數據結構的 Symbol.iterator 屬性上,該屬性本身是一個函數,代表當前數據結構默認的遍歷器生成函數。執行該函數 [Symbol.iterator](),會返回一個遍歷器對象。只要數據結構擁有 Symbol.iterator 屬性,那麼它就是 “可遍歷的” 。

遍歷器對象的特徵:

  1. 擁有 next 屬性方法;
  2. 執行 next(),會返回一個包含 valuedone 屬性的對象

    • value: 當前數據結構成員的值
    • done: 布爾值,表示遍歷是否結束

原生具備 Iterator 接口的數據結構:

  1. Array
  2. Map
  3. Set
  4. String
  5. TypedArray
  6. 函數的 arguments 對象
  7. NodeList 對象
let arr = ['a', 'b', 'c'];
let iterator = arr[Symbol.iterator]();

iterator.next();  // { value: 'a', done: false }
iterator.next();  // { value: 'b', done: false }
iterator.next();  // { value: 'c', done: false }
iterator.next();  // { value: undefined, done: false }

原生部署 Iterator 接口的數據結構,無需手動執行遍歷器生成函數,可使用 for...of 自動循環遍歷。

for...of 運行原理:

  1. 首先調用遍歷對象 [Symobo.iterator]() 方法,拿到遍歷器對象;
  2. 每次循環,調用遍歷器對象 next() 方法,得到 {value: ..., done: ... } 對象
// for...of 自動遍歷擁有 Iterator 接口的數據結構
let arr = ['a', 'b', 'c'];
for (let item of arr) {
  console.log(item);
}

// 輸出:a  b  c
類數組對象:存在數值鍵名和 length 屬性的對象

類數組對象部署 Iterator 方法:

// 方法一:
NodeList.prototype[Symbol.iterator] = Array.prototype[Sybmol.iterator];

// 方法二:
NodeList.prototype[Symbol.iterator] = [][Symbol.iterator];

// for...of 遍歷類數組對象
let arrLike = {
  0: 'a',
  1: 'b',
  2: 'c',
  length: 3,
  [Symbol.iterator]: Array.prototype[Symbol.iterator]
};

for (let item of arrLike) {
  console.log(item);
}

// 輸出:a  b  c

對象(Object)沒有默認 Iterator 接口,因爲對象屬性遍歷順序不確定,需開發者手動指定。

注意:

  1. 普通對象部署數組的 Symbol.iterator 方法,並無效果;
  2. 普通對象若 Symbol.iterator 方法對應的部署遍歷器生成函數(即返回一個遍歷器對象),解釋引擎會報錯。
var obj = {};
obj[Symbol.iterator] = () => 1;
[...obj]; // TypeError: [] is not a function

for...of 遍歷普通對象的解決方法:

  1. 使用 Objet.keys 將對象鍵名生成一個數組,然後遍歷該數組;
  2. Generator 函數重新包裝對象
let person = {
  name: 'Ken',
  sex: 'Male'
}

// Object.keys
for (let key of Object.keys(person)) {
  console.log(`${key}: ${person[key]}`);
}

// Generator 包裝對象
function* entries(obj) {
  for (let key of Object.keys(obj)) {
    yield [key, obj[key]];
  }
}
for (let [key, value] of entries(person)) {
  console.log(`${key}: ${value}`);
}

// 輸出:
// name: Ken 
// sex: Male

ES6 的 Iterator 應用場景

  1. 解構賦值
  2. 擴展運算符
  3. yield*
  4. 任何以數組爲參數的遍歷的場景:

    • for...of
    • Array.from()
    • Map()/Set()/WeakMap()/WeakSet()
    • Promise.all()/Promise.race()

for...of 對比 for / for...in / forEach

for 循環 :需定義索引變量,指定循環終結條件。

for (let i = 0, len = arr.length; i < len; i++) {
  console.log(arr[i]);
}

forEach: 無法中途跳出循環,break/return

forEach(arr, function(item, index) {
  console.log(item, index);
})

for...in:

  1. 只能獲取鍵名,不能獲取鍵值
  2. 以字符串爲鍵名(但數組的鍵名爲數值類型索引)
  3. 任意順序遍歷鍵名(???)
  4. 會遍歷手動添加的其它鍵(原型鏈上的鍵)
  5. 爲遍歷對象設計,不適用數組
let triangle = {a: 1, b: 2, c: 3};

function ColoredTriangle() {
  this.color = 'red';
}

ColoredTriangle.prototype = triangle;

let obj = new ColoredTriangle();

for (let prop in obj) {
  // 需手動判斷是否屬於自身屬性,而不是原型鏈屬性
  if (obj.hasOwnProperty(prop)) {
    console.log(`obj.${prop} = ${obj[prop]}`);
  } 
}

// 輸出:obj.color = red

for...of 較其它三者優點:

  1. for...in 一樣簡潔,但沒有 for...in 的缺點;
  2. 不同於 forEach, 可使用 break/return/continue 退出循環;
  3. 提供了遍歷所有數據的統一接口

缺點:遍歷普通對象時,不能直接使用。


參考文章

本文首發Github,期待Star!
https://github.com/ZengLingYong/blog

作者:以樂之名
本文原創,有不當的地方歡迎指出。轉載請指明出處。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章