Beace Lee

Beace Blog

Written by Beace Lee who lives and works in China building useful things. You should follow him on Twitter

【译文】理解 JavaScript 中的 for…of 循环

December 23, 2018

原文链接: Understanding the For…of Loop In JavaScript

在 JavaScript 中,我们有很多循环语句。

  • while 语句
  • do...while 语句
  • for 语句
  • for...in 语句
  • for...of 语句

所有这些语句都有一个最基础的共同功能:他们会一直重复,直到到达某个条件。

在这篇文章,我们将研究 for…of 表达式来查看它是如何工作的并且如何在 JavaScript 应用中利用它书写更好的代码。

技巧:使用 Bit 构造一个 js 应用是非常快速的。使用 Bit 可以快速的在不同项目中分享和复用公共组件,可以在团队中分享,就像玩乐高那样简单。你可以免费使用它。

for…of

for…offor 语句的一种循环 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

你应该直到,如果使用循环,我们不得不使用相关的数学知识和逻辑来计算到达循环 myname 的终点并推出程序。但是 for…of 语句帮我们解决了这个头疼的问题。

正如我们看到的,for..of 一般有以下定义:

for ( variable of iterable) {
    //...
}
  • variable 保存了每次迭代的 iterable 中对象的值
  • iterable 是要被迭代的对象

Iterables and Iterator (迭代对象和迭代器)

for…of 循环的定义中,我们说它“循环遍历可迭代对象(iterable objects)”。因此有了上述定义就意味着 for...of 只能被用于可迭代的对象,否则不能使用 for...of 循环。

那么,什么是可迭代对象(Iterables)呢?

简单的说,可迭代对象是可以执行迭代的对象。在 ECMASCRIPT 2015 中加入了一个 coupla。这些新增内容是新协议。协议中包括Iterator协议和Iterable协议。

根据 Mozilla 开发者说,“iterable 协议允许 JavaScript 对象去定义或者自定义迭代行为,例如在 for..of 构造中循环的值。” 并且,“为了可迭代,对象必须实现 @@iterator 方法,意味着对象必须包含(或者在其原型链中包含)一个 @@interator 的属性,这个属性可以通过常量Symbol.iterator获得”

这实际上意味着,如果想要你的对象可以使用 for…of 进行迭代,它必须是可迭代对象(interables),换言之,必须具有 @@iterator 属性。这样才符合可迭代协议(iterable protocol)。

因此,当对对象具有 @@iterator 时,它就可以被 for…of@@interator 方法由 for...of 调用。@@interator 返回一个迭代器(interator)。

现在,可迭代协议定义了一种可以从对象中返回值的流的方式。迭代器必须实现 next 方法。next 方法需要遵守以下几个规则。

  • 必须返回一个带有 done 属性的对象。类似value {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 返回了一个迭代器(interator),for…of 用来循环对象并且获取其值。因此,如果一个对象不包含@@interator 方法并且(或者)返回的不是一个迭代器(interator),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

注意这里少了 ObjectObject不是一个可迭代对象。如果我们尝试使用 for…of 循环去遍历 Object 中的属性:

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,这表明在 String 存在@@iterator 属性。如果我们尝试 Object:

const obj = {
    surname: "Chidume"
}
log(typeof obj[Symbol.iterator]);
undefined

Woo!! 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

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)
}
// It logs:
// 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)
}
// It logs:
// ["surname", "Chidume"]
// ["firstname","Nnamdi"]
for(const [key, value] of map) {
    log(`key: ${key}, value: ${value}`)
}
// It logs:
// key: surname, value: Chidume
// key: firstname, value: Nnamdi

for…of: Set

const set = new Set(["Chidume","Nnamdi"])
for(const a of set) {
    log(a)
}
// It logs:
// Chidume
// Nnamdi

for…of: TypedArray

const typedarray = new Uint8Array([0xe8, 0xb4, 0xf8, 0xaa]);
for (const a of typedarray) {
  log(a);
}
// It logs:
// 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接口的类具有@@iterator属性,该属性使参数可迭代。

// testFunc.js
function testFunc(arg) {
    log(typeof arguments[Symbol.iterator])
    for(const a of arguments) {
        log(a)
    }
}
testFunc("Chidume")
// It:
// Chidume

for…of: Custom 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] 增加了@@interator 属性。然后, function 最终返回一个迭代器(interator)。

//...
return {
    next: function() {...}
}
//...

记住,一个迭代器需要有一个 next 函数。

next 函数中,我定义的值将在迭代执行期间返回给 for…of。看上面的代码,你可以清楚的看到我做了什么。

让我们来测试一下 是用 for...of 遍历 obj 会得到什么:

// customIterableTest.js
//...
for (let a of obj) {
    log(a)
}
$ node customIterableTest
Chidume
Nnamdi

是的,它正确执行了:)!

使对象和普通对象可迭代

普通对象(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() 获取的对象的属性。在 next 函数中,我们简单地返回了从 properties 中获取的每一个值并且更新了 count,从而使用 count作为索引从属性中获取下一个值。 当 count 等于properties 长度时,迭代就会停止。

通过 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 ,在应用中使用 for..of

Using for…of on ES6 classes

我们可以使用 for..of 来遍历类实例中的数据列表。

class Profiles {
    constructor(profiles) {
        this.profiles = profiles
    }
}
const profiles = new Profiles([
    {
        firstname: "Nnamdi",
        surname: "Chidume"
    },
    {
        firstname: "Philip",
        surname: "David"
    }
])

Profiles 具有一个 profile 属性包含一个数组 users 。我们可能需要在应用程序中使用 for...of展示此数据。如果我们这样做:

//...
for(const a of profiles) {
    log(a)
}

显然,它不能正常工作。

for(const a of profiles) {
               ^
TypeError: profiles is not iterable

需要让 profiles 迭代对象具有以下规则

  • 对象必须包含 @@iterator 属性
  • @@iterator 必须返回一个迭代器 interator
  • iterator 迭代器必须有 next 方法的实现

我们通过 [Symbol.iterator] 来定义了 @@interator

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引入了一个新的构造,以便能够遍历Promises数组,这个新构造是 for-await-of 和一个新的 Symbol Symbol.asyncIterator

Symbol.asyncIterator 函数是一个可以返回 promise 迭代器的可迭代对象。

const f = {
    [Symbol.asyncIterator]() {
        return new Promise(...)
    }
}

[Symbol.iterator][Symbol.asyncIterator] 的区别在于前者返回 { value, done }, 后者返回 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 方法在每一个迭代中都返回了 value

要迭代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

总结

在这篇文章中,我们深入挖掘了for... of循环。我们首先定义for..of是什么,然后继续看看是什么使得可迭代。然后,我们查看了JS中完整的可迭代列表,并浏览了它们中的每一个,以了解如何使用它们的循环。

就像我在开始时说的那样,for..of 为我们节省了许多复杂性和逻辑,并有助于使我们的代码看起来更清晰,更易读。如果您还没有尝试过这种令人敬畏的for循环变异,我认为现在是时候这样做了。

如果您对此或我应该添加,更正或删除的任何问题有任何疑问,请随时在下面发表评论,以及任何事情或DM我。谢谢阅读! :)