理解 JavaScript 中的循環

理解 JavaScript 中的循環

JavaScript的世界中,咱們可使用不少種循環表達式:javascript

  • while 表達式
  • do...while 表達式
  • for 表達式
  • for...in 表達式
  • for...of 表達式

全部這些表達式都有一個基本的功能:它們會重複一件事情直到一個具體的條件出現。java

在這篇文章中,咱們將深刻 for...of 表達式,去了解它是如何工做的,以及在咱們的應用中可使用它來優化代碼的地方。node

for…of

for...of 是一種 for 表達式,用來迭代 iterables(iterable objects)直到終止條件出現。數組

下面是一個基礎的例子:app

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循環以後,咱們將會避免這些煩人的事情。async

for...of有如下通用的使用方法:函數

for ( variable of iterable) {
    //...
}
複製代碼

variable - 保存每次迭代過程當中的迭代對象的屬性值 iterable - 咱們進行迭代的對象oop

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 爲咱們節省了許多複雜性和邏輯,並有助於使咱們的代碼看起來很清晰易讀,若是你尚未嘗試過,我認爲如今正是時候。

相關文章
相關標籤/搜索