多种方式让JS的对象可以被 for...of

228 阅读3分钟

什么是可迭代对象

可迭代对象指的是可被迭代的对象, 可以使用for...of或者...,就像对数组使用for...of会得到每一个数组元素的值, 对map使用for...of会得到每一个键值对组成的数组

let m = new Map()
m.set('1', 'hello')
m.set('2', 'world')
for(let value of m) {
    console.log(value)
}
// ['1', 'hello']
// ['2', 'world']

可以这样被遍历的对象就是可迭代对象

可迭代对象必须满足可迭代协议,即具备一个属性 —— Symbol.iterator,它是一个函数, 返回一个调用者的迭代器对象, 其中具备next()方法,这个next()调用时,会返回一个对象,其类型如下:

{
    done: Boolean,
    value: Any
}
  • done: 表示是否已经结束迭代
  • value: 表示当前迭代到的值

image.png

ArraySetMap天生实现了迭代器方法

  • Array 的迭代器
let arr = [1,2,3]
// 获取数组arr的迭代器
let iter = arr[Symbol.iterator]()
iter.next(); // { value: 1, done: false }
iter.next(); // { value: 2, done: false }
iter.next(); // { value: 3, done: false }
iter.next(); // { value: undefined; true }
  • Set 的迭代器
let s = new Set()
s.add(1);
s.add(2);
let iter = s[Symbol.iterator]()
iter.next(); // { value: 1, done: false }
iter.next(); // { value: 2, done: false }
iter.next(); // { value: undefined; true }
  • Map 的迭代器
let m = new Map()
m.set('1', 'hello');
m.set('2', 'world');
let iter = m[Symbol.iterator]()
iter.next(); // { value: ['1', 'hello'], done: false }
iter.next(); // { value: ['2', 'world'], done: false }
iter.next(); // { value: undefined, done: true }

所以这三者都可以使用 for...of 遍历。但是 Object 由于没有实现 Symbol.iterator,所以 Object 先天无法使用 for...of, 如果想要使用 for...of, 那么需要手动在Object上实现Symbol.iterator

在Object上实现Symbol.iterator

根据上面对 Symbol.iterator 的分析,我们可以得到其运作流程:

  • 被迭代时,自动触发并执行Symbol.iterator设置的函数,拿到迭代器, 我们命名为iter
  • 每迭代一次,执行迭代器的next()方法,运行其中的逻辑后,返回一个对象{ value: xxx, done: yyy }
  • 返回其中的 value, 供用户使用

于是我们可以用以下方法让Object可迭代

let obj = {
    foo: 1,
    bar: 2,
    // 定义迭代器, 迭代器不接受参数, 不然你打一下会发现是undefined
    [Symbol.iterator](){
        let keys = Object.keys(this)
        // 通过索引控制每一次迭代返回的value
        let curIndex = 0
        return {
            // 每次迭代自动触发next()方法
            next(){
                if(curIndex < keys.length) {
                    return {
                        value: obj[keys[curIndex++]],
                        done: false
                    }
                }else {
                    return {
                        value: undefined,
                        done: true
                    }
                }
            }
        }
    }
}
for(let i of obj) {
    console.log(i)
}

image.png

手写 Object.values

利用迭代器

把迭代器写到 Object 的原型上

Object.prototype[Symbol.iterator] = function() {
    let keys = Object.keys(this)
    let curIndex = 0
    let self = this
    return {
        next() {
            if(curIndex < keys.length) {
                return {
                    value: self[keys[curIndex++]],
                    done: false
                }
            }else {
                return {
                    value: undefined,
                    done: true
                }
            }
        }
    }
}

然后再写自己的Object.values()

Object.prototype.myValues = function(obj) {
    let res = []
    for(let v of obj) {
        res.push(v)
    }
    return res
}

obj = {
    a: 1,
    b: 2,
    c: 3
}
Object.myValues(obj)

image.png

兼容版

Object.prototype.myValues = function(obj) {
    let res = []
    if(obj === null) {
        return res
    }
    
    // 如果传入的对象实现了Symbol.iterator, 那么就使用for...of
    if(typeof obj[Symbol.iterator] === 'function') {
        for(let v of obj) {
            res.push(v)
        }
        return res
    }
    
    // 没有迭代器那就使用for...in
    for(let key in obj) {
        if(obj.hasOwnProperty(key)) {
            res.push(obj[key])
        }
    }
    
    return res
}

生成器

生成器函数(Generator Functions)是 ECMAScript 6(ES6)引入的一项新特性,它允许你在函数中使用 yield 关键字来暂停和继续函数的执行。生成器函数的定义使用 function* 语法。

function* simpleGenerator() {
    yield 1;
    yield 2;
    yield 3;
}

和迭代器的关系

执行生成器函数,会返回一个名为生成器的特殊迭代器对象, 这个迭代器与 Iterator 不同的是,它不是连续执行的,它每一次执行,都会暂停到最近的下一个 yield 处,并返回其后表达式的结果,再一次调用 next(),继续向下执行, 直到执行完毕。

生成器函数带来的迭代是可控的,因为你可以通过设置yield使它暂停在任意的位置并拿到当前的值

let iter = simpleGenerator()
for(let i of iter) {
    console.log(i)
}

image.png

利用生成器实现for...of

Object.prototype[Symbol.iterator] = function*() {
    for (const key in this) {
        yield {key, value: this[key]}
    }
}

let tempObj = {a: 1, b: 'hello'}
for (const item of tempObj) {
    console.log(item.key, item.value)
}