可以迭代大部分數(shù)據(jù)類型的 for…of 為什么不能遍歷普通對象?

for…of 及其使用
我們知道,ES6 中引入 for...of 循環(huán),很多時候用以替代 for...in 和 forEach() ,并支持新的迭代協(xié)議。for...of 允許你遍歷 Array(數(shù)組), String(字符串), Map(映射), Set(集合),TypedArray(類型化數(shù)組)、arguments、NodeList對象、Generator等可迭代的數(shù)據(jù)結構等。for...of語句在可迭代對象上創(chuàng)建一個迭代循環(huán),調(diào)用自定義迭代鉤子,并為每個不同屬性的值執(zhí)行語句。
for...of的語法:
for (variable of iterable) {
// statement
}
// variable:每個迭代的屬性值被分配給該變量。
// iterable:一個具有可枚舉屬性并且可以迭代的對象。
常用用法
{
// 迭代字符串
const iterable = 'ES6';
for (const value of iterable) {
console.log(value);
}
// Output:
// "E"
// "S"
// "6"
}
{
// 迭代數(shù)組
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(參數(shù)對象)
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可以迭代數(shù)組、Map等數(shù)據(jù)結構,順著這個思路,我們可以結合對象的Object.values()、Object.keys()、Object.entries()方法以及解構賦值的知識來用for...of遍歷普通對象。
Object.values()、Object.keys()、Object.entries()用法及返回值
const obj = {
foo: 'value1',
bar: 'value2'
}
// 打印由value組成的數(shù)組
console.log(Object.values(obj)) // ["value1", "value2"]
// 打印由key組成的數(shù)組
console.log(Object.keys(obj)) // ["foo", "bar"]
// 打印由[key, value]組成的二維數(shù)組
// copy(Object.entries(obj))可以把輸出結果直接拷貝到剪貼板,然后黏貼
console.log(Object.entries(obj)) // [["foo","value1"],["bar","value2"]]
因為 for...of可以迭代數(shù)組和Map,所以我們得到以下遍歷普通對象的方法
const obj = {
foo: 'value1',
bar: 'value2'
}
// 方法一:使用for of迭代Object.entries(obj)形成的二維數(shù)組,利用解構賦值得到value
for(const [, value] of Object.entries(obj)){
console.log(value) // value1, value2
}
// 方法二:Map
// 普通對象轉(zhuǎn)Map
// Map 可以接受一個數(shù)組作為參數(shù)。該數(shù)組的成員是一個個表示鍵值對的數(shù)組
console.log(new Map(Object.entries(obj)))
// 遍歷普通對象生成的Map
for(const [, value] of new Map(Object.entries(obj))){
console.log(value) // value1, value2
}
// 方法三:繼續(xù)使用for in
for(const key in obj){
console.log(obj[key]) // value1, value2
}
{
// 方法四:將【類數(shù)組(array-like)對象】轉(zhuǎn)換為數(shù)組
// 該對象需具有一個 length 屬性,且其元素必須可以被索引。
const obj = {
length: 3, // length是必須的,否則什么也不會打印
0: 'foo',
1: 'bar',
2: 'baz',
a: 12 // 非數(shù)字屬性是不會打印的
};
const array = Array.from(obj); // ["foo", "bar", "baz"]
for (const value of array) {
console.log(value);
}
// Output: foo bar baz
}
{
// 方法五:給【類數(shù)組】部署數(shù)組的[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的循環(huán)可由break,throw,continue或return終止,在這些情況下,迭代器關閉。
const obj = {
foo: 'value1',
bar: 'value2',
baz: 'value3'
}
for(const [, value] of Object.entries(obj)){
if (value === 'value2') break // 不會再執(zhí)行下次迭代
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 // 仍然會繼續(xù)執(zhí)行下一次循環(huán),打印2
console.log(item) // 2
})
For…of與For…in對比for...in不僅枚舉數(shù)組聲明,它還從構造函數(shù)的原型中查找繼承的非枚舉屬性;for...of不考慮構造函數(shù)原型上的不可枚舉屬性(或者說for...of語句遍歷可迭代對象定義要迭代的數(shù)據(jù)。);for...of更多用于特定的集合(如數(shù)組等對象),但不是所有對象都可被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 迭代
前面我們有提到一個詞叫“可迭代”數(shù)據(jù)結構,當用for of迭代普通對象時,也會報一個“not iterable”的錯誤。實際上,任何具有 Symbol.iterator 屬性的元素都是可迭代的。我們可以簡單查看幾個可被for of迭代的對象,看看和普通對象有何不同:



可以看到,這些可被for of迭代的對象,都實現(xiàn)了一個Symbol(Symbol.iterator)方法,而普通對象沒有這個方法。
簡單來說,for of 語句創(chuàng)建一個循環(huán)來迭代可迭代的對象,可迭代的對象內(nèi)部實現(xiàn)了Symbol.iterator方法,而普通對象沒有實現(xiàn)這一方法,所以普通對象是不可迭代的。
Iterator(遍歷器)
關于Iterator(遍歷器)的概念,可以參照阮一峰大大的《ECMAScript 6 入門》——Iterator(遍歷器)的概念:

簡單來說,ES6 為了統(tǒng)一集合類型數(shù)據(jù)結構的處理,增加了 iterator 接口,供 for...of 使用,簡化了不同結構數(shù)據(jù)的處理。而 iterator 的遍歷過程,則是類似 Generator 的方式,迭代時不斷調(diào)用next方法,返回一個包含value(值)和done屬性(標識是否遍歷結束)的對象。
如何實現(xiàn)Symbol.iterator方法,使普通對象可被 for of 迭代
依據(jù)上文的指引,我們先看看數(shù)組的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}
我們可以嘗試給普通對象實現(xiàn)一個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實現(xiàn)了Symbol.iterator接口后,我們甚至還可以像下面這樣把對象轉(zhuǎn)換成數(shù)組:
console.log([...obj]); // ["value1", "value2"]
console.log([...{}]); // console.log is not iterable (cannot read property Symbol(Symbol.iterator))
我們給obj對象實現(xiàn)了一個Symbol.iterator接口,在此,有一點需要說明的是,不用擔心[Symbol.iterator]屬性會被Object.keys()獲取到導致遍歷結果出錯,因為Symbol.iterator這樣的Symbol屬性,需要通過Object.getOwnPropertySymbols(obj)才能獲取,Object.getOwnPropertySymbols() 方法返回一個給定對象自身的所有 Symbol 屬性的數(shù)組。
有一些場合會默認調(diào)用 Iterator 接口(即Symbol.iterator方法:
擴展運算符 ...:這提供了一種簡便機制,可以將任何部署了 Iterator 接口的數(shù)據(jù)結構,轉(zhuǎn)為數(shù)組。也就是說,只要某個數(shù)據(jù)結構部署了 Iterator 接口,就可以對它使用擴展運算符,將其轉(zhuǎn)為數(shù)組(毫不意外的,代碼[...{}]會報錯,而[...'123']會輸出數(shù)組['1','2','3'])。數(shù)組和可迭代對象的解構賦值(解構是ES6提供的語法糖,其實內(nèi)在是針對 可迭代對象的Iterator接口,通過遍歷器按順序獲取對應的值進行賦值。而普通對象解構賦值的內(nèi)部機制,是先找到同名屬性,然后再賦給對應的變量。);yield*:_yield*后面跟的是一個可遍歷的結構,它會調(diào)用該結構的遍歷器接口;由于數(shù)組的遍歷會調(diào)用遍歷器接口,所以任何接受數(shù)組作為參數(shù)的場合,其實都調(diào)用; 字符串是一個類似數(shù)組的對象,也原生具有Iterator接口,所以也可被 for of迭代。
迭代器模式
迭代器模式提供了一種方法順序訪問一個聚合對象中的各個元素,而又無需暴露該對象的內(nèi)部實現(xiàn),這樣既可以做到不暴露集合的內(nèi)部結構,又可讓外部代碼透明地訪問集合內(nèi)部的數(shù)據(jù)。迭代器模式為遍歷不同的集合結構提供了一個統(tǒng)一的接口,從而支持同樣的算法在不同的集合結構上進行操作。
不難發(fā)現(xiàn),Symbol.iterator實現(xiàn)的就是一種迭代器模式。集合對象內(nèi)部實現(xiàn)了Symbol.iterator接口,供外部調(diào)用,而我們無需過多的關注集合對象內(nèi)部的結構,需要處理集合對象內(nèi)部的數(shù)據(jù)時,我們通過for of調(diào)用Symbol.iterator接口即可。
比如針對前文普通對象的Symbol.iterator接口實現(xiàn)一節(jié)的代碼,如果我們對obj里面的數(shù)據(jù)結構進行了如下調(diào)整,那么,我們只需對應的修改供外部迭代使用的Symbol.iterator接口,即可不影響外部迭代調(diào)用:
const obj = {
// 數(shù)據(jù)結構調(diào)整
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 };
}
}
};
}
}
// 外部調(diào)用
for (const value of obj) {
console.log(value); // value1 value2
}
實際使用時,我們可以把上面的Symbol.iterator提出來進行單獨封裝,這樣就可以對一類數(shù)據(jù)結構進行迭代操作了。當然,下面的代碼只是最簡單的示例,你可以在此基礎上探究更多實用的技巧。
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
一點補充
在寫這篇文章時,有個問題給我?guī)砹死_:原生object對象默認沒有部署Iterator接口,即object不是一個可迭代對象。對象的擴展運算符...等同于使用Object.assign()方法,這個比較好理解。那么,原生object對象的解構賦值又是怎樣一種機制呢?
let aClone = { ...a };
// 等同于
let aClone = Object.assign({}, a);
有一種說法是:ES6提供了Map數(shù)據(jù)結構,實際上原生object對象被解構時,會被當作Map進行解構。關于這點,大家有什么不同的觀點嗎?歡迎評論區(qū)一起探討。
同時,ECMAScript后面又引入了異步迭代器for await...of 語句,該語句創(chuàng)建一個循環(huán),該循環(huán)遍歷異步可迭代對象以及同步可迭代對象,詳情可查看MDN:for-await...of。
來源:獨釣寒江雪
https://segmentfault.com/a/1190000038393650
