Iterator(遍歷器) 和 for…of 回圈
遍歷器(Iterator)就是這樣一種機制,它是一種介面,為各種不同的資料結構提供統一的訪問機制
任何資料結構只要部署 Iterator 介面,就可以完成遍歷操作(即依次處理該資料結構的所有成員)
一、迭代器和 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
注意,我們拿到了里面的元素,但卻多做了很多事:
- 我們宣告了 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 回圈共同解決這個問題,
1.2 terator(迭代器)
迭代器的描述:
- 是為各種資料結構,提供一個統一的、簡便的訪問介面,是用于遍歷資料結構元素的指標
- 二是使得資料結構的成員能夠按某種次序排列;
- 三是
ES6創造的一種遍歷命令 for…of 回圈,Iterator 介面主要供 for…of 消費,
迭代的程序如下:
- 通過 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 回圈也不一樣,
二、默認的 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 方法對應的不是遍歷器生成函式(即會回傳一個遍歷器物件),解釋引擎將會報錯,
三、模擬實作的 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();
}
}
四、使用 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()
五、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 命令給出每一步的回傳值即可,
六、遍歷器物件的 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()方法關閉檔案之后,再拋出錯誤,
參考
- ES6 系列之迭代器與 for of
- 廖雪峰:迭代器函式
寫在最后
JavaScript 系列:
- 《JavaScript 內功進階系列》(已完結)
- 《JavaScript 專項系列》(持續更新)
- 《ES6 基礎系列》(持續更新)
關于我
- 花名:余光(沉迷 JS,虛心學習中)
- WX:j565017805
其他沉淀
- Js 版 LeetCode 題解
- 前端進階筆記
- 我的 CSDN 博客
這是文章所在 GitHub 倉庫的傳送門,您點的 star,就是對我最大的鼓勵 ~
轉載請註明出處,本文鏈接:https://www.uj5u.com/qianduan/225374.html
標籤:其他
上一篇:Bootstrap4+MySQL前后端綜合實訓-Day04-AM【新聞管理手機端頁面+資料庫操作(PowerDesigner 圖形化資料庫設計軟體、SQLyog軟體)】
