Iterator(遍歷器) 和 for…of 循環
遍歷器(Iterator)就是這樣一種機制。它是一種接口,爲各類不一樣的數據結構提供統一的訪問機制
任何數據結構只要部署 Iterator 接口,就能夠完成遍歷操做(即依次處理該數據結構的全部成員)
html
1、迭代器和 for…of 淺談
1.1 傳統 for 循環
先來看一段標準的 for 循環的代碼:前端
var arr = [1,2,3]; for (let i = 0; i < arr.length; i++) { console.log(arr[i]); } // 1 2 3
注意,咱們拿到了裏面的元素,但卻多作了不少事:git
- 咱們聲明瞭 i 標索引;
- 肯定了邊界,一旦多層嵌套;
function unique(array) { var res = []; for (var i = 0, arrayLen = array.length; i < arrayLen; i++) { for (var j = 0, resLen = res.length; j < resLen; j++) { if (array[i] === res[j]) { break; } } if (j === resLen) { // 把首次出現的加入到新數組中 res.push(array[i]); } } return res; }
爲了消除這種複雜度以及減小循環中的錯誤(好比錯誤使用其餘循環中的變量),ES6 提供了迭代器和 for of 循環共同解決這個問題。es6
1.2 terator(迭代器)
迭代器的描述:github
- 是爲各類數據結構,提供一個統一的、簡便的訪問接口,是用於遍歷數據結構元素的指針
- 二是使得數據結構的成員可以按某種次序排列;
- 三是
ES6
創造的一種遍歷命令 for…of 循環,Iterator 接口主要供 for…of 消費。
迭代的過程以下:web
- 經過 Symbol.iterator 建立一個迭代器,指向當前數據結構的起始位置
- 隨後經過 next 方法進行向下迭代指向下一個位置:
- next 方法會返回當前位置的對象,對象包含了
value
和done
兩個屬性; - value 是當前屬性的值;
- done 用於判斷是否遍歷結束,done 爲 true 時則遍歷結束;
- next 方法會返回當前位置的對象,對象包含了
迭代的內部邏輯應該是:數組
var it = makeIterator(["a", "b"]); it.next(); // { value: "a", done: false } it.next(); // { value: "b", done: false } it.next(); // { value: undefined, done: true } function makeIterator(array) { let index = 0; const iterator = { }; iterator.next = function() { if (index < array.length) return { value: array[index++], done: false }; return { value: undefined, done: true }; }; return iterator; }
1.3 什麼是 for…of?
注意這裏咱們僅說起了 forof 與迭代器的關係。數據結構
for…of 的描述:函數
- for…of 語句在可迭代對象上建立一個迭代循環,調用自定義迭代鉤子,併爲每一個不一樣屬性的值執行語句——MDN
- 一個數據結構只要部署了
Symbol.iterator
屬性,就被視爲具備iterator
接口,就能夠用for...of
循環遍歷它的成員。
看到這裏你會發現for...of
和迭代器老是在一塊兒, for...of
循環內部調用的是數據結構的Symbol.iterator
方法。學習
舉個例子:
const obj = { value: 1, }; for (value of obj) { console.log(value); } // TypeError: iterator is not iterable
咱們直接 for of 遍歷一個對象,會報錯,然而若是咱們給該對象添加 Symbol.iterator 屬性:
const obj = { value: 1, }; obj[Symbol.iterator] = function() { return createIterator([1, 2, 3]); }; for (value of obj) { console.log(value); } // 1 // 2 // 3
由此,咱們也能夠發現 for...of
遍歷的實際上是對象的 Symbol.iterator
屬性。
JavaScript 原有的 for…in 循環,只能得到對象的鍵名,不能直接獲取鍵值。ES6 提供 for…of 循環,容許遍歷得到鍵值。
var arr = ["a", "b", "c", "d"]; for (let a in arr) { console.log(a); // 0 1 2 3 } for (let a of arr) { console.log(a); // a b c d }
上面代碼代表:
- for…in 循環讀取鍵名
- for…of 循環讀取鍵值
for…of 循環調用遍歷器接口,數組的遍歷器接口只返回具備數字索引的屬性。這一點跟 for…in 循環也不同。
2、默認的 Iterator 接口
Iterator
接口的目的,就是爲全部數據結構,提供了一種統一的訪問機制。當使用 for…of 循環遍歷某種數據結構時,該循環會自動去尋找 Iterator 接口。
原生具有 Iterator 接口的數據結構以下。
- Array
- Map
- Set
- String
- TypedArray
- 函數的 arguments 對象
- NodeList 對象
拿數組舉例:
const item = [1, 2, 3][Symbol.iterator](); item.next(); item.next(); item.next(); // {value: 1, done: false} // {value: 2, done: false} // {value: 3, done: false} // {value: undefined, done: true}
對於原生部署Iterator
接口的數據結構,不用本身寫遍歷器生成函數,for...of
循環會自動遍歷它們。除此以外,都須要本身在 Symbol.iterator 屬性上面部署。
本質上,遍歷器是一種線性處理,對於任何非線性的數據結構,部署遍歷器接口,就等於部署一種線性轉換。
對象(Object)之因此沒有默認部署 Iterator 接口,也是由於對象無法統一進行線性轉換
一個對象若是要具有可被 for…of 循環調用的 Iterator 接口,就必須在 Symbol.iterator 的屬性上部署遍歷器生成方法(原型鏈上的對象具備該方法也可)。
class newiterator { constructor(start, stop) { this.value = start; this.stop = stop; } // Iterator接口 返回自己 [Symbol.iterator]() { return this; } next() { if (this.value < this.stop) { return { value: this.value++, done: false }; } return { value: undefined, done: true }; } } const iterator = new newiterator(0, 3); for (let key of iterator) { console.log(key); } // 0 1 2
上面代碼是一個類部署 Iterator 接口的寫法。Symbol.iterator 屬性對應一個函數,執行後返回當前對象的遍歷器對象。
對於相似數組的對象(存在數值鍵名和 length 屬性),部署 Iterator 接口,有一個簡便方法,就是 Symbol.iterator 方法直接引用數組的 Iterator 接口。
NodeList.prototype[Symbol.iterator] = Array.prototype[Symbol.iterator]; // 或者 NodeList.prototype[Symbol.iterator] = [][Symbol.iterator]; [...document.querySelectorAll("div")]; // 能夠執行了
NodeList 對象是相似數組的對象,原本就具備遍歷接口,能夠直接遍歷。上面代碼中,咱們將它的遍歷接口改爲數組的 Symbol.iterator 屬性,能夠看到沒有任何影響。
注意,普通對象部署數組的 Symbol.iterator 方法,並沒有效果。
let iterable = { a: "a", b: "b", c: "c", length: 3, [Symbol.iterator]: Array.prototype[Symbol.iterator], }; for (let item of iterable) { console.log(item); // undefined, undefined, undefined }
若是 Symbol.iterator 方法對應的不是遍歷器生成函數(即會返回一個遍歷器對象),解釋引擎將會報錯。
3、模擬實現的 for…of
其實模擬實現 for of
也比較簡單,就是利用它與 Symbol.iterator 的關係。
function forOf(obj, cb) { let iterable, result; if (typeof obj[Symbol.iterator] !== "function") throw new TypeError(result + " is not iterable"); if (typeof cb !== "function") throw new TypeError("cb must be callable"); iterable = obj[Symbol.iterator](); result = iterable.next(); while (!result.done) { cb(result.value); result = iterable.next(); } }
4、使用 Iterator 接口的場景
有一些場合會默認調用 Iterator 接口(即 Symbol.iterator 方法),除了 for…of 循環,還有幾個別的場合。
4.1 解構賦值
對數組和 Set 結構進行解構賦值時,會默認調用 Symbol.iterator 方法。
let set = new Set() .add("a") .add("b") .add("c"); let [x, y] = set; // x='a'; y='b' let [first, ...rest] = set; // first='a'; rest=['b','c'];
4.2 擴展運算符
擴展運算符(…)也會調用默認的 Iterator 接口。
// 例一 var str = "hello"; [...str]; // ['h','e','l','l','o'] // 例二 let arr = ["b", "c"]; ["a", ...arr, "d"]; // ['a', 'b', 'c', 'd']
上面代碼的擴展運算符內部就調用 Iterator 接口。
實際上,這提供了一種簡便機制,能夠將任何部署了 Iterator 接口的數據結構,轉爲數組。也就是說,只要某個數據結構部署了 Iterator 接口,就能夠對它使用擴展運算符,將其轉爲數組。
4.3 yield*
yield*
後面跟的是一個可遍歷的結構,它會調用該結構的遍歷器接口。
let generator = function*() { yield 1; yield* [2, 3, 4]; yield 5; }; var iterator = generator(); iterator.next(); // { value: 1, done: false } iterator.next(); // { value: 2, done: false } iterator.next(); // { value: 3, done: false } iterator.next(); // { value: 4, done: false } iterator.next(); // { value: 5, done: false } iterator.next(); // { value: undefined, done: true }
4.4 其餘場合
因爲數組的遍歷會調用遍歷器接口,因此任何接受數組做爲參數的場合,其實都調用了遍歷器接口。下面是一些例子。
- Array.from()
- Map(), Set(), WeakMap(), WeakSet()(好比 new Map([[‘a’,1],[‘b’,2]]))
- Promise.all()
- Promise.race()
5、Iterator 接口與 Generator 函數
Symbol.iterator()
方法的最簡單實現,仍是使用 ES6 新提出的 Generator 函數。
let myIterable = { [Symbol.iterator]: function* () { yield 1; yield 2; yield 3; } }; [...myIterable] // [1, 2, 3] // 或者採用下面的簡潔寫法 let obj = { [Symbol.iterator]() { yield 'hello'; yield 'world'; } }; for (let x of obj) { console.log(x); } // "hello" // "world"
上面代碼中,Symbol.iterator()方法幾乎不用部署任何代碼,只要用 yield 命令給出每一步的返回值便可。
6、遍歷器對象的 return(),throw()
遍歷器對象除了具備 next()方法,還能夠具備 return()方法和 throw()方法。若是你本身寫遍歷器對象生成函數,那麼 next()方法是必須部署的,return()方法和 throw()方法是否部署是可選的。
return()方法的使用場合是,若是 for…of 循環提早退出(一般是由於出錯,或者有 break 語句),就會調用 return()方法。若是一個對象在完成遍歷前,須要清理或釋放資源,就能夠部署 return()方法。
function readLinesSync(file) { return { [Symbol.iterator]() { return { next() { return { done: false }; }, return() { file.close(); return { done: true }; }, }; }, }; }
上面代碼中,函數 readLinesSync 接受一個文件對象做爲參數,返回一個遍歷器對象,其中除了 next()方法,還部署了 return()方法。下面的兩種狀況,都會觸發執行 return()方法。
// 狀況一 for (let line of readLinesSync(fileName)) { console.log(line); break; } // 狀況二 for (let line of readLinesSync(fileName)) { console.log(line); throw new Error(); }
上面代碼中:
- 狀況一輸出文件的第一行之後,就會執行 return()方法,關閉這個文件;
- 狀況二會在執行 return()方法關閉文件以後,再拋出錯誤。
參考
寫在最後
JavaScript 系列:
關於我
- 花名:餘光(沉迷 JS,虛心學習中)
- WX:j565017805
其餘沉澱
這是文章所在 GitHub 倉庫的傳送門,您點的 star
,就是對我最大的鼓勵 ~