- type: FrontEnd
- title: Understanding the For…of Loop In JavaScript
- link: blog.bitsrc.io/understandi…
- author: kurtwanger40
理解 JavaScript 中的迴圈
在JavaScript
的世界中,我們可以使用很多種迴圈表示式:
while
表示式do...while
表示式for
表示式for...in
表示式for...of
表示式
所有這些表示式都有一個基本的功能:它們會重複一件事情直到一個具體的條件出現。
在這篇文章中,我們將深入 for...of
表示式,去了解它是如何工作的,以及在我們的應用中可以使用它來優化程式碼的地方。
for…of
for...of
是一種 for
表示式,用來迭代 iterables(iterable objects)
直到終止條件出現。
下面是一個基礎的例子:
let arr = [2,4,6,8,10]
for(let a of arr) {
log(a)
}
// It logs:
// 2
// 4
// 6
// 8
// 10
複製程式碼
使用比for
迴圈更好的程式碼,我們遍歷了arr
陣列。
let myname = "Nnamdi Chidume"
for (let a of myname) {
log(a)
}
// It logs:
// N
// n
// a
// m
// d
// i
//
// C
// h
// i
// d
// u
// m
// e
複製程式碼
你知道如果我們使用for
迴圈,我們將必須使用數學和邏輯去判斷何時我們將會達到myname
的末尾並且停止迴圈。但是正如你所見的,使用for...of
迴圈之後,我們將會避免這些煩人的事情。
for...of
有以下通用的使用方法:
for ( variable of iterable) {
//...
}
複製程式碼
variable
- 儲存每次迭代過程中的迭代物件的屬性值
iterable
- 我們進行迭代的物件
Iterables(可迭代的物件) and Iterator(迭代器)
在for...of
迴圈的定義中,我們說它是“迭代 iterables(iterable objects)”。這個定義告訴我們for...of
迴圈只能用於可迭代物件。
那麼, 什麼是可迭代的物件(iterables
)?
簡單來說的話,可迭代物件(Iterables)是可以用於迭代的物件。在ES6
中增加了幾個特性。這些特性包含了新的協議,其中就有迭代器(Iterator)協議和可迭代(Iterable)協議。
參考MDN
的描述,“可迭代協議允許JS
物件定義或者修改它們的迭代行為,比如哪些值可以被for...of
迴圈到。”同時“為了變成可迭代的,物件必須實現@@iterator
方法,這意味著這個物件(或者其原型鏈上的物件)必須包含一個可以使用Symbol.iterator
常量訪問的@@iterator
的屬性”
說人話就是,對於可以使用for...of
迴圈的物件來說,它必須是可迭代的,換句話就是它必須有@@iterator
屬性。這就是可迭代協議。
所以當物件有@@iterator
屬性的時候就可以被for...of
迴圈迭代,@@iterator
方法被for...of
呼叫,並且返回一個迭代器。
同時,迭代器協議定義了一種物件中的值如何返回的方式。一個迭代器物件必須實現next
方法,next 方法需要遵循以下準則:
- 必須返回一個包含 done, value 屬性的物件
- done 是一個布林值,用來表示迴圈是否結束
- value 是當前迴圈的值
舉個例子:
const createIterator = function () {
var array = ['Nnamdi','Chidume']
return {
next: function() {
if(this.index == 0) {
this.index++
return { value: array[this.index], done: false }
}
if(this.index == 1) {
return { value: array[this.index], done: true }
}
},
index: 0
}
}
const iterator = createIterator()
log(iterator.next()) // Nnamdi
log(iterator.next()) // Chidume
複製程式碼
基本上,@@iterator
方法回返回一個迭代器,for...of
迴圈正是使用這個迭代器去迴圈操作目標物件從而得到值。因此,如果一個物件沒有@@iterator
方法同時這個返回值是一個迭代器,for...of
迴圈將不會生效。
const nonIterable = //...
for( let a of nonIterable) {
// ...
}
for( let a of nonIterable) {
^
TypeError: nonIterable is not iterable
複製程式碼
內建的可迭代物件有有以下這些:
- String
- Map
- TypedArray
- Array
- Set
- Generator
注意,Object 不是可迭代的。如果我們嘗試使用for...of
去迭代物件的屬性:
let obj {
firstname: "Nnamdi",
surname: "Chidume"
}
for(const a of obj) {
log(a)
}
複製程式碼
將會丟擲一個異常:
for(const a of obj) {
^
TypeError: obj is not iterable
複製程式碼
我們可以用下面的方式檢查一個物件是否可迭代:
const str = new String('Chidume');
log(typeof str[Symbol.iterator]);
function
複製程式碼
看到了吧,列印的結果是function
, 這意味著@@iterator
屬性存在,並且是函式型別。如果我們在 Object 上面進行嘗試:
const obj = {
surname: "Chidume"
}
log(typeof obj[Symbol.iterator]);
undefined
複製程式碼
哇!undefined
表示不存在。
for…of: Array
陣列是可迭代物件。
log(typeof new Array("Nnamdi", "Chidume")[Symbol.iterator]);
// function
複製程式碼
這是我們可以對它使用for...of
迴圈的原因。
const arr = ["Chidume", "Nnamdi", "loves", "JS"]
for(const a of arr) {
log(a)
}
// It logs:
// Chidume
// Nnamdi
// loves
// JS
const arr = new Array("Chidume", "Nnamdi", "loves", "JS")
for(const a of arr) {
log(a)
}
// It logs:
// Chidume
// Nnamdi
// loves
// JS
複製程式碼
for…of: String
字串也是可迭代的。
const myname = "Chidume Nnamdi"
for(const a of myname) {
log(a)
}
// It logs:
// C
// h
// i
// d
// u
// m
// e
//
// N
// n
// a
// m
// d
// i
const str = new String("The Young")
for(const a of str) {
log(a)
}
// 列印結果是:
// T
// h
// e
//
// Y
// o
// u
// n
// g
複製程式碼
for…of: Map型別
const map = new Map([["surname", "Chidume"],["firstname","Nnamdi"]])
for(const a of map) {
log(a)
}
// 列印結果是:
// ["surname", "Chidume"]
// ["firstname","Nnamdi"]
for(const [key, value] of map) {
log(`key: ${key}, value: ${value}`)
}
// 列印結果是:
// key: surname, value: Chidume
// key: firstname, value: Nnamdi
複製程式碼
for…of: Set型別
const set = new Set(["Chidume","Nnamdi"])
for(const a of set) {
log(a)
}
// 列印結果是:
// Chidume
// Nnamdi
複製程式碼
for…of: TypedArray型別
const typedarray = new Uint8Array([0xe8, 0xb4, 0xf8, 0xaa]);
for (const a of typedarray) {
log(a);
}
// 列印結果是:
// 232
// 180
// 248
// 170
複製程式碼
for…of: arguments物件
arguments物件是可遍歷的嗎?我們先來看:
// testFunc.js
function testFunc(arg) {
log(typeof arguments[Symbol.iterator])
}
testFunc()
$ node testFunc
function
複製程式碼
答案出來了。如果進一步探討,arguments其實是IArguments型別的物件,而且實現了IArguments介面的class都有一個@@iterator屬性,使得arguments物件可遍歷。
// testFunc.js
function testFunc(arg) {
log(typeof arguments[Symbol.iterator])
for(const a of arguments) {
log(a)
}
}
testFunc("Chidume")
// It:
// Chidume
複製程式碼
for…of: 自定義可遍歷物件(Iterables)
正如上一節那樣,我們可以建立一個自定義的可以通過for..of
遍歷的可遍歷物件。
var obj = {}
obj[Symbol.iterator] = function() {
var array = ["Chidume", "Nnamdi"]
return {
next: function() {
let value = null
if (this.index == 0) {
value = array[this.index]
this.index++
return { value, done: false }
}
if (this.index == 1) {
value = array[this.index]
this.index++
return { value, done: false }
}
if (this.index == 2) {
return { done: true }
}
},
index: 0
}
};
複製程式碼
這裡建立了一個可遍歷的obj
物件,通過[Symbol.iterator]
賦予它一個@@iterator
屬性,然後建立一個返回遍歷器的方法。
//...
return {
next: function() {...}
}
//...
複製程式碼
記住,遍歷器一定要有一個next()
方法。
在next方法裡面,我們實現了在for...of遍歷過程中會返回的值,這個過程很清晰。
Let’s test this our obj
against a for..of to see what will happen:
// customIterableTest.js
//...
for (let a of obj) {
log(a)
}
$ node customIterableTest
Chidume
Nnamdi
複製程式碼
耶!成功了!
把Object和普通物件(plain objects)變成可遍歷
簡單物件是不可遍歷的,通過Object
得到的物件也是不可遍歷的。
我們可以通過自定義遍歷器把@@iterator新增到Object.prototype來實現這個目標。
Object.prototype[Symbol.iterator] = function() {
let properties = Object.keys(this)
let count = 0
let isdone = false
let next = () => {
let value = this[properties[count]]
if (count == properties.length) {
isdone = true
}
count++
return { done: isdone, value }
}
return { next }
}
複製程式碼
properties
變數裡面包含了通過呼叫Object.keys()
得到的object的所有屬性。在next方法裡面,我們只需要返回properties裡面的每一個值,並且通過更新作為索引的count變數來獲取下一個值。當count達到properties的長度的時候,就把done設為true,遍歷就結束了。
用Object來測試一下:
let o = new Object()
o.s = "SK"
o.me = 'SKODA'
for (let a of o) {
log(a)
}
SK
SKODA
複製程式碼
成功了!!!
用簡單物件來測試一下:
let dd = {
shit: 900,
opp: 800
}
for (let a of dd) {
log(a)
}
900
800
複製程式碼
也成功了!! :)
所以我們可以把這個新增到polyfill裡面,然後就可以在app裡面使用for...of來遍歷物件了。
在ES6的類(class)中使用for…of
我們可以用for...of來遍歷class的例項中的資料列表。
class Profiles {
constructor(profiles) {
this.profiles = profiles
}
}
const profiles = new Profiles([
{
firstname: "Nnamdi",
surname: "Chidume"
},
{
firstname: "Philip",
surname: "David"
}
])
複製程式碼
Profiles類有一個profile
屬性,包含一個使用者列表。當我們需要在app中用for...of來展示這個列表的時候,如果這樣做:
//...
for(const a of profiles) {
log(a)
}
複製程式碼
顯然是不行的。
for(const a of profiles) {
^
TypeError: profiles is not iterable
複製程式碼
為了把profiles
變成可遍歷,請記住以下規則:
- 這個物件一定要有
@@iterator
屬性。 - 這個
@@iterator
的方法一定要返回一個遍歷器(iterator). - 這個
iterator
一定要實現next()
方法。
我們通過一個熟悉的常量[Symbol.iterator]
來定義這個@@iterator
class Profiles {
constructor(profiles) {
this.profiles = profiles
}
[Symbol.iterator]() {
let props = this.profiles
let propsLen = this.profiles.length
let count = 0
return {
next: function() {
if (count < propsLen) {
return { value: props[count++], done: false }
}
if (count == propsLen) {
return { done: true }
}
}
}
}
}
複製程式碼
然後,如果我們這樣執行:
//...
for(const a of profiles) {
log(a)
}
$ node profile.js
{ firstname: 'Nnamdi', surname: 'Chidume' }
{ firstname: 'Philip', surname: 'David' }
複製程式碼
我們可以顯示 profiles 的屬性
Async Iterator
ECMAScript 2018 引入了一個新的語法,可以迴圈遍歷一個 Promise 陣列,它就是 for-await-of
和新的 Symbol Symbol.asyncIterator
。
iterable 中的 Symbol.asyncIterator
函式需要返回一個返回 Promise 的迭代器。
const f = {
[Symbol.asyncIterator]() {
return new Promise(...)
}
}
複製程式碼
[Symbol.iterator]
和 [Symbol.asyncIterator]
的區別在於前者返回的是 { value, done }
而後者返回的是一個 Promise
,只不過當 Promise resolve 的時候傳入了 { value, done }
。
我們上面的那個 f 例子將如下所示:
const f = {
[Symbol.asyncIterator]() {
return {
next: function() {
if (this.index == 0) {
this.index++
return new Promise(res => res({ value: 900, done: false }))
}
return new Promise(res => res({ value: 1900, done: true }))
},
index: 0
}
}
}
複製程式碼
這個 f
是可非同步迭代的,你看它總是返回一個 Promise ,而只有在迭代時 Promise 的 resolve 才返回真正的值。
要遍歷 f
,我們不能使用 for..of
而是要使用新的 for-await-of
,它看起來會是這樣:
// ...
async function fAsyncLoop(){
for await (const _f of f) {
log(_f)
}
}
fAsyncLoop()
$ node fAsyncLoop.js
900
複製程式碼
我們也可以使用 for-await-of
來迴圈遍歷一個 Promise 陣列:
const arrayOfPromises = [
new Promise(res => res("Nnamdi")),
new Promise(res => res("Chidume"))
]
async function arrayOfPromisesLoop(){
for await (const p of arrayOfPromises) {
log(p)
}
}
arrayOfPromisesLoop()
$ node arrayOfPromisesLoop.js
Nnamdi
Chidume
複製程式碼
Conclusion
在這篇文章中我們深入研究了 for...of
迴圈,我們首先定義理解什麼是 for...of
,然後看看什麼是可迭代的。for...of
為我們節省了許多複雜性和邏輯,並有助於使我們的程式碼看起來很清晰易讀,如果你還沒有嘗試過,我認為現在正是時候。