for...of与Iterator

129 阅读5分钟

for...of与Iterator

for...of循环的由来

循环遍历是最常见的操作需求,但在es6前循环遍历数组只有三种方法:(1)for循环(2)forEach方法(3)for...in循环

  1. for循环
const arr = [1, 2, 3]
for (let i = 0; i < arr.length; i++) { // 最传统方法,编写的代码冗长
    console.log(arr[i]) // => 1 2 3
}
  1. forEach方法
arr.forEach((item, index, arr), thisArg) { // es5新增,无法结合break\continue\return语句使用
    console.log(item) // => 1 2 3
}
  1. for...in循环 是一种适用于遍历对象自身及其原型链上的所有可枚举属性的方法。
// 1.循环遍历的结果是索引,且顺序无法保证
// 2.会遍历出自身及祖先的所有可枚举属性
// 3.索引值为字符串,容易写出错误代码
arr.emu = 'emu'
for (let key in arr) { 
    console.log(key) // => '0' '1' '2' 'emu'
    console.log(arr[key]) // => 1 2 3 'emu'
}

为了解决上述方法存在的缺陷,且能满足向后兼容的需求,js编程界急需一种简洁,且能结合return break continue语句使用的方法,于是es6提出了全新的for...of循环。

const arr = [1, 2, 3]
for (let item of arr) {
    console.log(item) // => 1 2 3
}

for...of适用场景

(1)Array数组 (2)Set集合 (3)Map集合 (4)Nodelist类数组 (5)arguments类数组 (6)String字符串 (7)有iterator接口的对象 对于没有iterator接口的类数组或对象,可以采用转换的方法。

// 包含索引和length属性的类数组可以转化为数组
for (let 🍉 of Array.from(nodelist)) {
    console.log(🍉) 
}
let arrayLike = {
    0: 1,
    2: 2,
    2: 3,
    length: 3,
    [Symbol.iterator]: Array.prototype[Symbol.iterator]
}
nodeList[Symbol.iterator] = [][Symbol.iterator]

for(let 🍟 of Object.keys(obj)) {
    console.log(🍟)
}

for...of循环的原理

正如上面提到的,for...of循环适用的场景非常广,不同类型的数据结构集合均能使用。那么,它是运用了怎样的原理使不同的数据结构能够使用同一的方法呢?

for (let item of arr) {
    console.log(item)
}

执行上述语句时,实际上会执行以下几步:

  1. 调用arrSymbol.iterator方法,并返回一个迭代器对象$iterator
  2. 调用$iterator迭代器的next()方法,并返回{value: 1, done: false}结果对象
  3. 将结果对象的value值赋值给item变量,并判断done值是否为true
  4. done值为false则继续调用next()方法,否则结束循环
  5. 遇到异常或break语句,则调用迭代器的return()方法 那么,现在我们就知道了,for...of循环实际上是调用数据结构的[Symbol.iterator]()方法接口。所以问题的关键就在于如何将不同的数据结构和[Symnol.iterator]()方法关联以转化成同一数据结构。这就不得不提到iterator迭代器。

迭代器是什么

迭代器是es6新提供的一个特殊对象,它包含一个next方法,用于迭代一个对象的属性。迭代器本身就是一个指针对象,当第一次调用next方法指向数据结构的第一个成员,第二次调用则指向数据结构的第二个成员,以此类推,直到结束条件done: true则停止迭代。迭代器适用于线性结构的数据,对于非线性结构的数据结构(如Object)则需专门的转换。下面我们来为一个普通对象创建iterator方法。

// 最简单的迭代器(对象本身就是迭代器)
let foo = {
    [Symbol.iterator] () {
        return this
    },
    next () {
        return {done: true, value: 0}
    }
}

// 通过`this`建立迭代器与原数据结构的关联
let obj = {
    data: ['apple', 'pear', 'bananer'],
    [Symbol.iterator] () {
        let index = 0
        const self = this
        return {
            next () {
                if (index < self.data.length) {
                    return {value: data[index ++], done: false}
                } else {
                    return {done: true}
                }
            }
        }
    }
}

// 通过遍历器实现指针结构
let Obj = function (value) {
    this.value = value
    this.next = null
}
Obj.prototype[Symbol.iterator] = function () {
    let current = this
    return {
        next () {
            if (current) {
                let curValue = current.value
                current = current.next
                return {value: curValue, done: false}
            } else {
                return {done: true}
            }
        }
    }
}
let one = new Obj('one')
let two = new Obj('two')
let three = new Obj('three')
one.next = two
two.next = three
for (let val of one) {
    console.log(val)
}

迭代器的使用场合

除了for..of循环,迭代器作为一个接口还可以应用于很多方法建立方法与原数据结构的联系。

  1. while循环
let $iterator = [1, 2, 3][Symbol.iterator]()
let $result = $iterator.next()
while(!$result.done) {
    console.log($result.value)
    $result = $iterator.next() // 需要手动不断调用迭代器的`next`方法
}
  1. 解构 Set/Map集合在解构赋值时,会默认调用iterator接口。
let newSet = new Set().add('a').add('b').add('c')
let [a, b, c] = newSet
let newMap = new Map().set(a, 1).set(b, 2).set(c, 3)
let [d, e, f] = newMap
console.log(a, b, c, d, e, f) // => a b c [ 'a', 1 ] [ 'b', 2 ] [ 'c', 3 ]
  1. 扩展运算符 只要有iterator接口的数据结构,就可以调用扩展运算符将其转化为数组。
let obj = {
    data: [1, 2, 4],
    index: 0,
    [Symbol.iterator] () {return this},
    next () {
        if (this.index < this.data.length) {
            return {value: this.data[this.index++], done: false}
        } else {
            return {done: true}
        }
    }
}
const result = [...obj]
console.log(result) // => [1, 2, 4]
  1. yield* yield*后面跟着的是一个可遍历结构,它会调用可遍历结构的遍历器。
let generate = function* () {
    yield 1;
    yield * [1, 2, 3]
}
const iterator = generate()
iterator.next() // => {done: false, value: 1}
iterator.next() // => {done: false, value: 1}
iterator.next() // => {done: false, value: 2}

迭代器的return throw方法

一般自己部署迭代器,那么next方法是必须的,但迭代器还有return throw方法。return方法在for...of循环提前退出时调用(如,出错、break continue return语句)。而throw方法配合生成器使用,使用极少,一般在迭代器中是用不到的。对于生成器生成迭代器的相关知识我在生成器一章再总结。

function readLineSync (file) {
    return {
        [Symbol.iterator] () {
            return {
                next () {
                    return {done: false}
                },
                return () {
                    file.close()
                    return {done: true}
                }
            }
        }
    }
}

参考文献

  1. 阮一峰的《es6入门》es6.ruanyifeng.com/#docs/let
  2. 《深入浅出es6》系列文章