for…of
及其使用
我們知道,ES6 中引入 for...of
迴圈,很多時候用以替代 for...in
和 forEach()
,並支援新的迭代協議。for...of
允許你遍歷 Array(陣列), String(字串), Map(對映), Set(集合),TypedArray(型別化陣列)、arguments、NodeList物件、Generator等可迭代的資料結構等。for...of
語句在可迭代物件上建立一個迭代迴圈,呼叫自定義迭代鉤子,併為每個不同屬性的值執行語句。
for...of
的語法:
for (variable of iterable) {
// statement
}
// variable:每個迭代的屬性值被分配給該變數。
// iterable:一個具有可列舉屬性並且可以迭代的物件。
常用用法
{
// 迭代字串
const iterable = 'ES6';
for (const value of iterable) {
console.log(value);
}
// Output:
// "E"
// "S"
// "6"
}
{
// 迭代陣列
const iterable = ['a', 'b'];
for (const value of iterable) {
console.log(value);
}
// Output:
// a
// b
}
{
// 迭代Set(集合)
const iterable = new Set([1, 2, 2, 1]);
for (const value of iterable) {
console.log(value);
}
// Output:
// 1
// 2
}
{
// 迭代Map
const iterable = new Map([["a", 1], ["b", 2], ["c", 3]]);
for (const entry of iterable) {
console.log(entry);
}
// Output:
// ["a", 1]
// ["b", 2]
// ["c", 3]
for (const [key, value] of iterable) {
console.log(value);
}
// Output:
// 1
// 2
// 3
}
{
// 迭代Arguments Object(引數物件)
function args() {
for (const arg of arguments) {
console.log(arg);
}
}
args('a', 'b');
// Output:
// a
// b
}
{
// 迭代生成器
function* foo(){
yield 1;
yield 2;
yield 3;
};
for (let o of foo()) {
console.log(o);
}
// Output:
// 1
// 2
// 3
}
Uncaught TypeError: obj is not iterable
// 普通物件
const obj = {
foo: 'value1',
bar: 'value2'
}
for(const item of obj){
console.log(item)
}
// Uncaught TypeError: obj is not iterable
可以看出,for of
可以迭代大部分物件甚至字串,卻不能遍歷普通物件。
如何用for...of
迭代普通物件
通過前面的基本用法,我們知道,for...of
可以迭代陣列、Map等資料結構,順著這個思路,我們可以結合物件的Object.values()
、Object.keys()
、Object.entries()
方法以及解構賦值的知識來用for...of
遍歷普通物件。
Object.values()
、Object.keys()
、Object.entries()
用法及返回值
const obj = {
foo: 'value1',
bar: 'value2'
}
// 列印由value組成的陣列
console.log(Object.values(obj)) // ["value1", "value2"]
// 列印由key組成的陣列
console.log(Object.keys(obj)) // ["foo", "bar"]
// 列印由[key, value]組成的二維陣列
// copy(Object.entries(obj))可以把輸出結果直接拷貝到剪貼簿,然後黏貼
console.log(Object.entries(obj)) // [["foo","value1"],["bar","value2"]]
- 因為
for...of
可以迭代陣列和Map,所以我們得到以下遍歷普通物件的方法
const obj = {
foo: 'value1',
bar: 'value2'
}
// 方法一:使用for of迭代Object.entries(obj)形成的二維陣列,利用解構賦值得到value
for(const [, value] of Object.entries(obj)){
console.log(value) // value1, value2
}
// 方法二:Map
// 普通物件轉Map
// Map 可以接受一個陣列作為引數。該陣列的成員是一個個表示鍵值對的陣列
console.log(new Map(Object.entries(obj)))
// 遍歷普通物件生成的Map
for(const [, value] of new Map(Object.entries(obj))){
console.log(value) // value1, value2
}
// 方法三:繼續使用for in
for(const key in obj){
console.log(obj[key]) // value1, value2
}
{
// 方法四:將【類陣列(array-like)物件】轉換為陣列
// 該物件需具有一個 length 屬性,且其元素必須可以被索引。
const obj = {
length: 3, // length是必須的,否則什麼也不會列印
0: 'foo',
1: 'bar',
2: 'baz',
a: 12 // 非數字屬性是不會列印的
};
const array = Array.from(obj); // ["foo", "bar", "baz"]
for (const value of array) {
console.log(value);
}
// Output: foo bar baz
}
{
// 方法五:給【類陣列】部署陣列的[Symbol.iterator]方法【對普通字串屬性物件無效】
const 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'
}
}
注意事項
- 有別於不可終止遍歷的
forEach
,for...of
的迴圈可由break
,throw
,continue
或return
終止,在這些情況下,迭代器關閉。
const obj = {
foo: 'value1',
bar: 'value2',
baz: 'value3'
}
for(const [, value] of Object.entries(obj)){
if (value === 'value2') break // 不會再執行下次迭代
console.log(value) // value1
};
[1,2].forEach(item => {
if(item == 1) break // Uncaught SyntaxError: Illegal break statement
console.log(item)
});
[1,2].forEach(item => {
if(item == 1) continue // Uncaught SyntaxError: Illegal continue statement: no surrounding iteration statement
console.log(item)
});
[1,2].forEach(item => {
if(item == 1) return // 仍然會繼續執行下一次迴圈,列印2
console.log(item) // 2
})
For…of
與For…in
對比for...in
不僅列舉陣列宣告,它還從建構函式的原型中查詢繼承的非列舉屬性;for...of
不考慮建構函式原型上的不可列舉屬性(或者說for...of
語句遍歷可迭代物件定義要迭代的資料。);for...of
更多用於特定的集合(如陣列等物件),但不是所有物件都可被for...of
迭代。
Array.prototype.newArr = () => {}; Array.prototype.anotherNewArr = () => {}; const array = ['foo', 'bar', 'baz']; for (const value in array) { console.log(value); // 0 1 2 newArr anotherNewArr } for (const value of array) { console.log(value); // 'foo', 'bar', 'baz' }
普通物件為何不能被 for of
迭代
前面我們有提到一個詞叫“可迭代”資料結構,當用for of
迭代普通物件時,也會報一個“not iterable”的錯誤。實際上,任何具有 Symbol.iterator
屬性的元素都是可迭代的。我們可以簡單檢視幾個可被for of
迭代的物件,看看和普通物件有何不同:
可以看到,這些可被for of
迭代的物件,都實現了一個Symbol(Symbol.iterator)
方法,而普通物件沒有這個方法。
簡單來說,for of
語句建立一個迴圈來迭代可迭代的物件,可迭代的物件內部實現了Symbol.iterator
方法,而普通物件沒有實現這一方法,所以普通物件是不可迭代的。
Iterator(遍歷器)
關於Iterator(遍歷器)的概念,可以參照阮一峰大大的《ECMAScript 6 入門》——Iterator(遍歷器)的概念:
簡單來說,ES6 為了統一集合型別資料結構的處理,增加了 iterator 介面,供 for...of
使用,簡化了不同結構資料的處理。而 iterator 的遍歷過程,則是類似 Generator 的方式,迭代時不斷呼叫next方法,返回一個包含value(值)和done屬性(標識是否遍歷結束)的物件。
如何實現Symbol.iterator
方法,使普通物件可被 for of
迭代
依據上文的指引,我們先看看陣列的Symbol.iterator
介面:
const arr = [1,2,3];
const iterator = arr[Symbol.iterator]();
console.log(iterator.next()); // {value: 1, done: false}
console.log(iterator.next()); // {value: 2, done: false}
console.log(iterator.next()); // {value: 3, done: false}
console.log(iterator.next()); // {value: undefined, done: true}
我們可以嘗試給普通物件實現一個Symbol.iterator
介面:
// 普通物件
const obj = {
foo: 'value1',
bar: 'value2',
[Symbol.iterator]() {
// 這裡Object.keys不會獲取到Symbol.iterator屬性,原因見下文
const keys = Object.keys(obj);
let index = 0;
return {
next: () => {
if (index < keys.length) {
// 迭代結果 未結束
return {
value: this[keys[index++]],
done: false
};
} else {
// 迭代結果 結束
return { value: undefined, done: true };
}
}
};
}
}
for (const value of obj) {
console.log(value); // value1 value2
};
上面給obj實現了Symbol.iterator
介面後,我們甚至還可以像下面這樣把物件轉換成陣列:
console.log([...obj]); // ["value1", "value2"]
console.log([...{}]); // console.log is not iterable (cannot read property Symbol(Symbol.iterator))
我們給obj物件實現了一個Symbol.iterator
介面,在此,有一點需要說明的是,不用擔心[Symbol.iterator]
屬性會被Object.keys()
獲取到導致遍歷結果出錯,因為Symbol.iterator
這樣的Symbol
屬性,需要通過Object.getOwnPropertySymbols(obj)
才能獲取,Object.getOwnPropertySymbols()
方法返回一個給定物件自身的所有 Symbol 屬性的陣列。
有一些場合會預設呼叫 Iterator 介面(即Symbol.iterator方法:
- 擴充套件運算子
...
:這提供了一種簡便機制,可以將任何部署了 Iterator 介面的資料結構,轉為陣列。也就是說,只要某個資料結構部署了 Iterator 介面,就可以對它使用擴充套件運算子,將其轉為陣列(毫不意外的,程式碼[...{}]
會報錯,而[...'123']
會輸出陣列['1','2','3']
)。 - 陣列和可迭代物件的解構賦值(解構是ES6提供的語法糖,其實內在是針對
可迭代物件
的Iterator介面
,通過遍歷器
按順序獲取對應的值進行賦值。而普通物件解構賦值的內部機制,是先找到同名屬性,然後再賦給對應的變數。); yield*
:_yield*
後面跟的是一個可遍歷的結構,它會呼叫該結構的遍歷器介面;- 由於陣列的遍歷會呼叫遍歷器介面,所以任何接受陣列作為引數的場合,其實都呼叫;
- 字串是一個類似陣列的物件,也原生具有Iterator介面,所以也可被
for of
迭代。
迭代器模式
迭代器模式提供了一種方法順序訪問一個聚合物件中的各個元素,而又無需暴露該物件的內部實現,這樣既可以做到不暴露集合的內部結構,又可讓外部程式碼透明地訪問集合內部的資料。迭代器模式為遍歷不同的集合結構提供了一個統一的介面,從而支援同樣的演算法在不同的集合結構上進行操作。
不難發現,Symbol.iterator
實現的就是一種迭代器模式。集合物件內部實現了Symbol.iterator
介面,供外部呼叫,而我們無需過多的關注集合物件內部的結構,需要處理集合物件內部的資料時,我們通過for of
呼叫Symbol.iterator
介面即可。
比如針對前文普通物件的Symbol.iterator
介面實現一節的程式碼,如果我們對obj裡面的資料結構進行了如下調整,那麼,我們只需對應的修改供外部迭代使用的Symbol.iterator
介面,即可不影響外部迭代呼叫:
const obj = {
// 資料結構調整
data: ['value1', 'value2'],
[Symbol.iterator]() {
let index = 0;
return {
next: () => {
if (index < this.data.length) {
// 迭代結果 未結束
return {
value: this.data[index++],
done: false
};
} else {
// 迭代結果 結束
return { value: undefined, done: true };
}
}
};
}
}
// 外部呼叫
for (const value of obj) {
console.log(value); // value1 value2
}
實際使用時,我們可以把上面的Symbol.iterator
提出來進行單獨封裝,這樣就可以對一類資料結構進行迭代操作了。當然,下面的程式碼只是最簡單的示例,你可以在此基礎上探究更多實用的技巧。
const obj1 = {
data: ['value1', 'value2']
}
const obj2 = {
data: [1, 2]
}
// 遍歷方法
consoleEachData = (obj) => {
obj[Symbol.iterator] = () => {
let index = 0;
return {
next: () => {
if (index < obj.data.length) {
return {
value: obj.data[index++],
done: false
};
} else {
return { value: undefined, done: true };
}
}
};
}
for (const value of obj) {
console.log(value);
}
}
consoleEachData(obj1); // value1 value2
consoleEachData(obj2); // 1 2
一點補充
在寫這篇文章時,有個問題給我帶來了困擾:原生object物件預設沒有部署Iterator介面,即object不是一個可迭代物件。物件的擴充套件運算子...
等同於使用Object.assign()
方法,這個比較好理解。那麼,原生object物件的解構賦值又是怎樣一種機制呢?
let aClone = { ...a };
// 等同於
let aClone = Object.assign({}, a);
有一種說法是:ES6提供了Map資料結構,實際上原生object物件被解構時,會被當作Map進行解構。關於這點,大家有什麼不同的觀點嗎?歡迎評論區一起探討。