理解 JavaScript 中的迴圈

子曰五溪發表於2019-04-12

理解 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 為我們節省了許多複雜性和邏輯,並有助於使我們的程式碼看起來很清晰易讀,如果你還沒有嘗試過,我認為現在正是時候。

相關文章