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

迭代器模式

文章內(nèi)容分兩部分:

  1. 前半部分為 “迭代器模式” 概念痒芝;
  2. 后半部分為 ES6 中 Iterator (迭代器)

上半部分開始...

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

簡單理解(白話理解):統(tǒng)一 “集合” 型數(shù)據(jù)結(jié)構(gòu)的遍歷接口,實現(xiàn)可循環(huán)遍歷獲取集合中各數(shù)據(jù)項(不關(guān)心數(shù)據(jù)項中的數(shù)據(jù)結(jié)構(gòu))骡和。

生活小栗子:清單 TodoList相赁。每日清單有學(xué)習(xí)類、生活類慰于、工作類钮科、運(yùn)動類等項目,清單列表只管羅列婆赠,不管類別绵脯。

模式特點

  1. 為遍歷不同數(shù)據(jù)結(jié)構(gòu)的 “集合” 提供統(tǒng)一的接口;
  2. 能遍歷訪問 “集合” 數(shù)據(jù)中的項,不關(guān)心項的數(shù)據(jù)結(jié)構(gòu)

模式實現(xiàn)

// 統(tǒng)一遍歷接口實現(xiàn)
var each = function(arr, callBack) {
  for (let i = 0, len = arr.length; i < len; i++) {
    // 將值蛆挫,索引返回給回調(diào)函數(shù)callBack處理
    if (callBack(i, arr[i]) === false) {
      break;  // 中止迭代器赃承,跳出循環(huán)
    }
  }
}

// 外部調(diào)用
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]

“迭代器模式的核心,就是實現(xiàn)統(tǒng)一遍歷接口璃吧¢沟迹”

模式細(xì)分

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

內(nèi)部迭代器

內(nèi)部迭代器: 內(nèi)部定義迭代規(guī)則,控制整個迭代過程畜挨,外部只需一次初始調(diào)用

// jQuery 的 $.each(跟上文each函數(shù)實現(xiàn)原理類似)
$.each(['Angular', 'React', 'Vue'], function(index, value) {
    console.log([index, value]);
});

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

優(yōu)點:調(diào)用方式簡單,外部僅需一次調(diào)用
缺點:迭代規(guī)則預(yù)先設(shè)置噩凹,欠缺靈活性巴元。無法實現(xiàn)復(fù)雜遍歷需求(如: 同時迭代比對兩個數(shù)組)

外部迭代器

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

借助 ES6 新增的 Generator 函數(shù)中的 yield* 表達(dá)式來實現(xiàn)外部迭代器。

// ES6 的 yield 實現(xiàn)外部迭代器
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']

優(yōu)點:靈活性更佳驮宴,適用面廣逮刨,能應(yīng)對更加復(fù)雜的迭代需求
缺點:需顯示調(diào)用迭代進(jìn)行(手動控制迭代過程),外部調(diào)用方式較復(fù)雜

適用場景

不同數(shù)據(jù)結(jié)構(gòu)類型的 “數(shù)據(jù)集合”堵泽,需要對外提供統(tǒng)一的遍歷接口修己,而又不暴露或修改內(nèi)部結(jié)構(gòu)時,可應(yīng)用迭代器模式實現(xiàn)迎罗。


下半部分開始...

ES6 的 Iterator 迭代器

“迭代器等同于遍歷器睬愤。在某些文章中,可能會出現(xiàn)遍歷器的字眼纹安,其實兩者的意思一致尤辱。”

JavaScript 中 原有表示 “集合” 的數(shù)據(jù)結(jié)構(gòu)主要是 “數(shù)組(Array)” 和 “對象(Object)”厢岂,ES6又新增了 MapSet光督,共四種數(shù)據(jù)集合,瀏覽器端還有 NodeList 類數(shù)組結(jié)構(gòu)塔粒。為 “集合” 型數(shù)據(jù)尋求統(tǒng)一的遍歷接口结借,正是 ES6 的 Iterator 誕生的背景。

ES6 中迭代器 Iterator 作為一個接口卒茬,作用就是為各種不同數(shù)據(jù)結(jié)構(gòu)提供統(tǒng)一的訪問機(jī)制船老。任何數(shù)據(jù)結(jié)構(gòu)只要部署了 Iterator 接口,就可以完成遍歷操作扬虚。

Iterator 作用:

  1. 為各種數(shù)據(jù)結(jié)構(gòu)努隙,提供一個統(tǒng)一的、簡便的訪問接口辜昵;
  2. 使得數(shù)據(jù)結(jié)構(gòu)的成員能夠按某種次序排列荸镊;
  3. 為新的遍歷語法 for...of 實現(xiàn)循環(huán)遍歷

Iterator只是一種接口,與遍歷的數(shù)據(jù)結(jié)構(gòu)是分開的。 重溫迭代器模式特點:我只要統(tǒng)一遍歷數(shù)據(jù)項的接口躬存,不關(guān)心其數(shù)據(jù)結(jié)構(gòu)张惹。

ES6 默認(rèn)的 Iterator 接口部署在數(shù)據(jù)結(jié)構(gòu)的 Symbol.iterator 屬性上,該屬性本身是一個函數(shù)岭洲,代表當(dāng)前數(shù)據(jù)結(jié)構(gòu)默認(rèn)的遍歷器生成函數(shù)宛逗。執(zhí)行該函數(shù) [Symbol.iterator](),會返回一個遍歷器對象盾剩。只要數(shù)據(jù)結(jié)構(gòu)擁有 Symbol.iterator 屬性雷激,那么它就是 “可遍歷的” 。

遍歷器對象的特征:

  1. 擁有 next 屬性方法告私;
  2. 執(zhí)行 next()屎暇,會返回一個包含 valuedone 屬性的對象
    • value: 當(dāng)前數(shù)據(jù)結(jié)構(gòu)成員的值
    • done: 布爾值,表示遍歷是否結(jié)束

原生具備 Iterator 接口的數(shù)據(jù)結(jié)構(gòu):

  1. Array
  2. Map
  3. Set
  4. String
  5. TypedArray
  6. 函數(shù)的 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 接口的數(shù)據(jù)結(jié)構(gòu)驻粟,無需手動執(zhí)行遍歷器生成函數(shù)根悼,可使用 for...of 自動循環(huán)遍歷。

for...of 運(yùn)行原理:

  1. 首先調(diào)用遍歷對象 [Symobo.iterator]() 方法蜀撑,拿到遍歷器對象;
  2. 每次循環(huán)挤巡,調(diào)用遍歷器對象 next() 方法,得到 {value: ..., done: ... } 對象
// for...of 自動遍歷擁有 Iterator 接口的數(shù)據(jù)結(jié)構(gòu)
let arr = ['a', 'b', 'c'];
for (let item of arr) {
  console.log(item);
}

// 輸出:a  b  c

類數(shù)組對象:存在數(shù)值鍵名和 length 屬性的對象

類數(shù)組對象部署 Iterator 方法:

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

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

// for...of 遍歷類數(shù)組對象
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)沒有默認(rèn) Iterator 接口酷麦,因為對象屬性遍歷順序不確定矿卑,需開發(fā)者手動指定。

注意:

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

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

  1. 使用 Objet.keys 將對象鍵名生成一個數(shù)組徘意,然后遍歷該數(shù)組;
  2. Generator 函數(shù)重新包裝對象
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 應(yīng)用場景

  1. 解構(gòu)賦值
  2. 擴(kuò)展運(yùn)算符
  3. yield*
  4. 任何以數(shù)組為參數(shù)的遍歷的場景:
  • for...of
  • Array.from()
  • Map()/Set()/WeakMap()/WeakSet()
  • Promise.all()/Promise.race()

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

for 循環(huán) :需定義索引變量轩褐,指定循環(huán)終結(jié)條件椎咧。

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

forEach: 無法中途跳出循環(huán),break/return把介。

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

for...in:

  1. 只能獲取鍵名勤讽,不能獲取鍵值
  2. 以字符串為鍵名(但數(shù)組的鍵名為數(shù)值類型索引)
  3. 任意順序遍歷鍵名(?拗踢?脚牍?)
  4. 會遍歷手動添加的其它鍵(原型鏈上的鍵)
  5. 為遍歷對象設(shè)計,不適用數(shù)組
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 較其它三者優(yōu)點:

  1. for...in 一樣簡潔诸狭,但沒有 for...in 的缺點券膀;
  2. 不同于 forEach, 可使用 break/return/continue 退出循環(huán);
  3. 提供了遍歷所有數(shù)據(jù)的統(tǒng)一接口

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


參考文章

本文首發(fā)Github,期待Star叉庐!
https://github.com/ZengLingYong/blog

作者:以樂之名
本文原創(chuàng)舒帮,有不當(dāng)?shù)牡胤綒g迎指出。轉(zhuǎn)載請指明出處陡叠。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末玩郊,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子匾竿,更是在濱河造成了極大的恐慌瓦宜,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,185評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件岭妖,死亡現(xiàn)場離奇詭異,居然都是意外死亡反璃,警方通過查閱死者的電腦和手機(jī)昵慌,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,652評論 3 393
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來淮蜈,“玉大人斋攀,你說我怎么就攤上這事∥嗵铮” “怎么了淳蔼?”我有些...
    開封第一講書人閱讀 163,524評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長裁眯。 經(jīng)常有香客問我鹉梨,道長,這世上最難降的妖魔是什么穿稳? 我笑而不...
    開封第一講書人閱讀 58,339評論 1 293
  • 正文 為了忘掉前任存皂,我火速辦了婚禮,結(jié)果婚禮上逢艘,老公的妹妹穿的比我還像新娘旦袋。我一直安慰自己,他們只是感情好它改,可當(dāng)我...
    茶點故事閱讀 67,387評論 6 391
  • 文/花漫 我一把揭開白布疤孕。 她就那樣靜靜地躺著,像睡著了一般央拖。 火紅的嫁衣襯著肌膚如雪祭阀。 梳的紋絲不亂的頭發(fā)上鹉戚,一...
    開封第一講書人閱讀 51,287評論 1 301
  • 那天,我揣著相機(jī)與錄音柬讨,去河邊找鬼崩瓤。 笑死,一個胖子當(dāng)著我的面吹牛踩官,可吹牛的內(nèi)容都是我干的却桶。 我是一名探鬼主播,決...
    沈念sama閱讀 40,130評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼蔗牡,長吁一口氣:“原來是場噩夢啊……” “哼颖系!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起辩越,我...
    開封第一講書人閱讀 38,985評論 0 275
  • 序言:老撾萬榮一對情侶失蹤嘁扼,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后黔攒,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體趁啸,經(jīng)...
    沈念sama閱讀 45,420評論 1 313
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,617評論 3 334
  • 正文 我和宋清朗相戀三年督惰,在試婚紗的時候發(fā)現(xiàn)自己被綠了不傅。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,779評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡赏胚,死狀恐怖访娶,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情觉阅,我是刑警寧澤崖疤,帶...
    沈念sama閱讀 35,477評論 5 345
  • 正文 年R本政府宣布,位于F島的核電站典勇,受9級特大地震影響劫哼,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜痴柔,卻給世界環(huán)境...
    茶點故事閱讀 41,088評論 3 328
  • 文/蒙蒙 一沦偎、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧咳蔚,春花似錦豪嚎、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,716評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至糯耍,卻和暖如春扔字,著一層夾襖步出監(jiān)牢的瞬間囊嘉,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,857評論 1 269
  • 我被黑心中介騙來泰國打工革为, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留扭粱,地道東北人。 一個月前我還...
    沈念sama閱讀 47,876評論 2 370
  • 正文 我出身青樓震檩,卻偏偏與公主長得像琢蛤,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子抛虏,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,700評論 2 354