for…of
及其使用
??我們知道力试,ES6 中引入 for...of
循環(huán)贮预,很多時(shí)候用以替代 for...in
和 forEach()
贝室,并支持新的迭代協(xié)議。for...of
允許你遍歷 Array(數(shù)組), String(字符串), Map(映射), Set(集合),TypedArray(類型化數(shù)組)仿吞、arguments档玻、NodeList對(duì)象、Generator等可迭代的數(shù)據(jù)結(jié)構(gòu)等茫藏。for...of
語(yǔ)句在可迭代對(duì)象上創(chuàng)建一個(gè)迭代循環(huán)误趴,調(diào)用自定義迭代鉤子,并為每個(gè)不同屬性的值執(zhí)行語(yǔ)句务傲。
for...of
的語(yǔ)法:
for (variable of iterable) {
// statement
}
// variable:每個(gè)迭代的屬性值被分配給該變量凉当。
// iterable:一個(gè)具有可枚舉屬性并且可以迭代的對(duì)象。
常用用法
{
// 迭代字符串
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ù)對(duì)象)
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
// 普通對(duì)象
const obj = {
foo: 'value1',
bar: 'value2'
}
for(const item of obj){
console.log(item)
}
// Uncaught TypeError: obj is not iterable
??可以看出售葡,for of
可以迭代大部分對(duì)象甚至字符串看杭,卻不能遍歷普通對(duì)象。
如何用for...of
迭代普通對(duì)象
??通過(guò)前面的基本用法挟伙,我們知道楼雹,for...of
可以迭代數(shù)組、Map等數(shù)據(jù)結(jié)構(gòu)尖阔,順著這個(gè)思路贮缅,我們可以結(jié)合對(duì)象的Object.values()
、Object.keys()
介却、Object.entries()
方法以及解構(gòu)賦值的知識(shí)來(lái)用for...of
遍歷普通對(duì)象谴供。
-
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))可以把輸出結(jié)果直接拷貝到剪貼板桂肌,然后黏貼
console.log(Object.entries(obj)) // [["foo","value1"],["bar","value2"]]
- 因?yàn)?code>for...of可以迭代數(shù)組和Map数焊,所以我們得到以下遍歷普通對(duì)象的方法
const obj = {
foo: 'value1',
bar: 'value2'
}
// 方法一:使用for of迭代Object.entries(obj)形成的二維數(shù)組,利用解構(gòu)賦值得到value
for(const [, value] of Object.entries(obj)){
console.log(value) // value1, value2
}
// 方法二:Map
// 普通對(duì)象轉(zhuǎn)Map
// Map 可以接受一個(gè)數(shù)組作為參數(shù)崎场。該數(shù)組的成員是一個(gè)個(gè)表示鍵值對(duì)的數(shù)組
console.log(new Map(Object.entries(obj)))
// 遍歷普通對(duì)象生成的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)對(duì)象】轉(zhuǎn)換為數(shù)組
// 該對(duì)象需具有一個(gè) length 屬性佩耳,且其元素必須可以被索引。
const obj = {
length: 3, // length是必須的谭跨,否則什么也不會(huì)打印
0: 'foo',
1: 'bar',
2: 'baz',
a: 12 // 非數(shù)字屬性是不會(huì)打印的
};
const array = Array.from(obj); // ["foo", "bar", "baz"]
for (const value of array) {
console.log(value);
}
// Output: foo bar baz
}
{
// 方法五:給【類數(shù)組】部署數(shù)組的[Symbol.iterator]方法【對(duì)普通字符串屬性對(duì)象無(wú)效】
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'
}
}
注意事項(xiàng)
- 有別于不可終止遍歷的
forEach
干厚,for...of
的循環(huán)可由break
,throw
饺蚊,continue
或return
終止,在這些情況下悬嗓,迭代器關(guān)閉污呼。
const obj = {
foo: 'value1',
bar: 'value2',
baz: 'value3'
}
for(const [, value] of Object.entries(obj)){
if (value === 'value2') break // 不會(huì)再執(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 // 仍然會(huì)繼續(xù)執(zhí)行下一次循環(huán),打印2
console.log(item) // 2
})
-
For…of
與For…in
對(duì)比-
for...in
不僅枚舉數(shù)組聲明包竹,它還從構(gòu)造函數(shù)的原型中查找繼承的非枚舉屬性燕酷; -
for...of
不考慮構(gòu)造函數(shù)原型上的不可枚舉屬性(或者說(shuō)for...of
語(yǔ)句遍歷可迭代對(duì)象定義要迭代的數(shù)據(jù)。)周瞎; -
for...of
更多用于特定的集合(如數(shù)組等對(duì)象)苗缩,但不是所有對(duì)象都可被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' }
-
普通對(duì)象為何不能被 for of
迭代
??前面我們有提到一個(gè)詞叫“可迭代”數(shù)據(jù)結(jié)構(gòu)慰照,當(dāng)用for of
迭代普通對(duì)象時(shí),也會(huì)報(bào)一個(gè)“not iterable”的錯(cuò)誤。實(shí)際上,任何具有 Symbol.iterator
屬性的元素都是可迭代的齐鲤。我們可以簡(jiǎn)單查看幾個(gè)可被for of
迭代的對(duì)象捧灰,看看和普通對(duì)象有何不同:
[圖片上傳失敗...(image-869405-1607263974085)]
[圖片上傳失敗...(image-86da24-1607263974085)]
[圖片上傳失敗...(image-9dd87f-1607263974085)]
??可以看到煌寇,這些可被for of
迭代的對(duì)象,都實(shí)現(xiàn)了一個(gè)Symbol(Symbol.iterator)
方法,而普通對(duì)象沒有這個(gè)方法更振。
??簡(jiǎn)單來(lái)說(shuō)乎芳,for of
語(yǔ)句創(chuàng)建一個(gè)循環(huán)來(lái)迭代可迭代的對(duì)象吭净,可迭代的對(duì)象內(nèi)部實(shí)現(xiàn)了Symbol.iterator
方法,而普通對(duì)象沒有實(shí)現(xiàn)這一方法肴甸,所以普通對(duì)象是不可迭代的寂殉。
Iterator(遍歷器)
??關(guān)于Iterator(遍歷器)的概念,可以參照阮一峰大大的《ECMAScript 6 入門》——Iterator(遍歷器)的概念:
[圖片上傳失敗...(image-a60031-1607263974085)]
??簡(jiǎn)單來(lái)說(shuō)原在,ES6 為了統(tǒng)一集合類型數(shù)據(jù)結(jié)構(gòu)的處理友扰,增加了 iterator 接口彤叉,供 for...of
使用,簡(jiǎn)化了不同結(jié)構(gòu)數(shù)據(jù)的處理村怪。而 iterator 的遍歷過(guò)程秽浇,則是類似 Generator 的方式,迭代時(shí)不斷調(diào)用next方法甚负,返回一個(gè)包含value(值)和done屬性(標(biāo)識(shí)是否遍歷結(jié)束)的對(duì)象柬焕。
如何實(shí)現(xiàn)Symbol.iterator
方法,使普通對(duì)象可被 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}
??我們可以嘗試給普通對(duì)象實(shí)現(xiàn)一個(gè)Symbol.iterator
接口:
// 普通對(duì)象
const obj = {
foo: 'value1',
bar: 'value2',
[Symbol.iterator]() {
// 這里Object.keys不會(huì)獲取到Symbol.iterator屬性斑举,原因見下文
const keys = Object.keys(obj);
let index = 0;
return {
next: () => {
if (index < keys.length) {
// 迭代結(jié)果 未結(jié)束
return {
value: this[keys[index++]],
done: false
};
} else {
// 迭代結(jié)果 結(jié)束
return { value: undefined, done: true };
}
}
};
}
}
for (const value of obj) {
console.log(value); // value1 value2
};
??上面給obj實(shí)現(xiàn)了Symbol.iterator
接口,甚至我們還可以像下面這樣:
console.log([...obj]); // ["value1", "value2"]
console.log([...{}]); // console.log is not iterable (cannot read property Symbol(Symbol.iterator))
??我們給obj對(duì)象實(shí)現(xiàn)了一個(gè)Symbol.iterator
接口病涨,在此富玷,有一點(diǎn)需要說(shuō)明的是,不用擔(dān)心[Symbol.iterator]
屬性會(huì)被Object.keys()
獲取到導(dǎo)致遍歷結(jié)果出錯(cuò)没宾,因?yàn)?code>Symbol.iterator這樣的Symbol
屬性凌彬,需要通過(guò)Object.getOwnPropertySymbols(obj)
才能獲取沸柔,Object.getOwnPropertySymbols()
方法返回一個(gè)給定對(duì)象自身的所有 Symbol 屬性的數(shù)組循衰。
??有一些場(chǎng)合會(huì)默認(rèn)調(diào)用 Iterator 接口(即Symbol.iterator方法:
- 擴(kuò)展運(yùn)算符
...
:這提供了一種簡(jiǎn)便機(jī)制,可以將任何部署了 Iterator 接口的數(shù)據(jù)結(jié)構(gòu)褐澎,轉(zhuǎn)為數(shù)組会钝。也就是說(shuō),只要某個(gè)數(shù)據(jù)結(jié)構(gòu)部署了 Iterator 接口工三,就可以對(duì)它使用擴(kuò)展運(yùn)算符迁酸,將其轉(zhuǎn)為數(shù)組(毫不意外的,代碼[...{}]
會(huì)報(bào)錯(cuò)俭正,而[...'123']
會(huì)輸出數(shù)組['1','2','3']
)奸鬓。 - 數(shù)組和可迭代對(duì)象的解構(gòu)賦值(解構(gòu)是ES6提供的語(yǔ)法糖,其實(shí)內(nèi)在是針對(duì)
可迭代對(duì)象
的Iterator接口
掸读,通過(guò)遍歷器
按順序獲取對(duì)應(yīng)的值進(jìn)行賦值串远。而普通對(duì)象解構(gòu)賦值的內(nèi)部機(jī)制,是先找到同名屬性儿惫,然后再賦給對(duì)應(yīng)的變量澡罚。); -
yield*
:_yield*
后面跟的是一個(gè)可遍歷的結(jié)構(gòu)肾请,它會(huì)調(diào)用該結(jié)構(gòu)的遍歷器接口留搔; - 由于數(shù)組的遍歷會(huì)調(diào)用遍歷器接口,所以任何接受數(shù)組作為參數(shù)的場(chǎng)合铛铁,其實(shí)都調(diào)用隔显;
- 字符串是一個(gè)類似數(shù)組的對(duì)象却妨,也原生具有Iterator接口,所以也可被
for of
迭代荣月。
迭代器模式
??迭代器模式提供了一種方法順序訪問(wèn)一個(gè)聚合對(duì)象中的各個(gè)元素管呵,而又無(wú)需暴露該對(duì)象的內(nèi)部實(shí)現(xiàn),這樣既可以做到不暴露集合的內(nèi)部結(jié)構(gòu)哺窄,又可讓外部代碼透明地訪問(wèn)集合內(nèi)部的數(shù)據(jù)捐下。迭代器模式為遍歷不同的集合結(jié)構(gòu)提供了一個(gè)統(tǒng)一的接口,從而支持同樣的算法在不同的集合結(jié)構(gòu)上進(jìn)行操作萌业。
??不難發(fā)現(xiàn)坷襟,Symbol.iterator
實(shí)現(xiàn)的就是一種迭代器模式。集合對(duì)象內(nèi)部實(shí)現(xiàn)了Symbol.iterator
接口生年,供外部調(diào)用婴程,而我們無(wú)需過(guò)多的關(guān)注集合對(duì)象內(nèi)部的結(jié)構(gòu),需要處理集合對(duì)象內(nèi)部的數(shù)據(jù)時(shí)抱婉,我們通過(guò)for of
調(diào)用Symbol.iterator
接口即可档叔。
??比如針對(duì)前文普通對(duì)象的Symbol.iterator
接口實(shí)現(xiàn)一節(jié)的代碼,如果我們對(duì)obj里面的數(shù)據(jù)結(jié)構(gòu)進(jìn)行了如下調(diào)整蒸绩,那么衙四,我們只需對(duì)應(yīng)的修改供外部迭代使用的Symbol.iterator
接口,即可不影響外部迭代調(diào)用:
const obj = {
// 數(shù)據(jù)結(jié)構(gòu)調(diào)整
data: ['value1', 'value2'],
[Symbol.iterator]() {
let index = 0;
return {
next: () => {
if (index < this.data.length) {
// 迭代結(jié)果 未結(jié)束
return {
value: this.data[index++],
done: false
};
} else {
// 迭代結(jié)果 結(jié)束
return { value: undefined, done: true };
}
}
};
}
}
// 外部調(diào)用
for (const value of obj) {
console.log(value); // value1 value2
}
??實(shí)際使用時(shí)患亿,我們可以把上面的Symbol.iterator
提出來(lái)進(jìn)行單獨(dú)封裝传蹈,這樣就可以對(duì)一類數(shù)據(jù)結(jié)構(gòu)進(jìn)行迭代操作了。當(dāng)然步藕,下面的代碼只是最簡(jiǎn)單的示例惦界,你可以在此基礎(chǔ)上探究更多實(shí)用的技巧。
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
一點(diǎn)補(bǔ)充
??在寫這篇文章時(shí)咙冗,有個(gè)問(wèn)題給我?guī)?lái)了困擾:原生object對(duì)象默認(rèn)沒有部署Iterator接口沾歪,即object不是一個(gè)可迭代對(duì)象,那么雾消,原生object對(duì)象的解構(gòu)賦值是怎么樣一種機(jī)制呢灾搏?
??有一種說(shuō)法是:ES6提供了Map數(shù)據(jù)結(jié)構(gòu),實(shí)際上原生object對(duì)象被解構(gòu)時(shí)仪或,會(huì)被當(dāng)作Map進(jìn)行解構(gòu)确镊。關(guān)于這點(diǎn),大家有什么不同的觀點(diǎn)嗎范删?歡迎評(píng)論區(qū)一起探討蕾域。