js中的可迭代与类数组,你搞清楚了吗

776 阅读7分钟

前言

我们在学js时,有两个概念可能会弄混,可迭代对象和类数组对象。说某个对象是可迭代的,比如数组let arr = [1,2,3]或者let map = new Map([[1, 2], [3, 4]]),它们可以使用for of进行遍历。然后又说某个对象是类数组,我们可以将它转换为数组,因为它有数值索引和length属性。来看一个小例子:

let o1 = {
    0: 'aa',
    1: 'bb',
    length: 2
}

let arr1 = Array.from(o1)
console.log(arr1); // [ 'aa', 'bb' ]

for (const item of arr1) {
    console.log(item);
}
// aa
// bb

let map = new Map([[1, 2], [3, 4]])
for (const item of map) {
    console.log(item);
}
// [ 1, 2 ]
// [ 3, 4 ]

如上面的例子,有一个类数组对象o1,使用Array.from()显示的将其转换为数组,然后这个对象就可以使用for of进行遍历了,其实上面这个小例子就涉及到了这两个概念:

  1. 类数组对象(array-like object):有数值索引属性和length属性的对象,称为类数组对象,比如例子中的o1
  2. 可迭代对象(iterable object):可以使用for of 进行遍历的对象,就称为可迭代的,比如例子中的arr1(转换后的数组)和map

那么为什么有的对象可以使用for of遍历(也即是可迭代对象),而有的对象却不可以,这是为什么呢?

可迭代对象

实际上那些可以直接使用for of遍历的对象,是系统内置实现了Symbol.iterator迭代器方法。我们来看下面这个例子。

一个小例子

如下面的例子所示,有一个普通对象range,显然是不可迭代的(因为它就是一个普通对象,啥也没干),我们使用for of检验一下,果然也报错了,TypeError: range is not iterable。那么如果我们想要让它变为可迭代的,该怎么办呢?比如,我们想实现这样的效果:使range对象可以使用用for of进行遍历,依次从1输出到5

let range = {
    from: 1,
    to: 5
}

for (const iterator of range) {
    console.log(iterator);
}
// TypeError: range is not iterable

我们来改写一下,既然想让这个对象变为可迭代的,那么就必须实现Symbol.iterator迭代方法。

let range = {
    from: 1,
    to: 5
}

/* 
1. 迭代器实际就是一个对象,对象上有 next() 方法
2. 迭代器每次调用一个next()方法后,都会返回一个标识对象
{
    value: '', // 本次迭代获得的值
    done: true|false // 标识是否还有下个值
}
3. for of 会获取到这个迭代对象,每遍历一次,调用一次迭代器的next方法,输出返回值中的value
*/
range[Symbol.iterator] = function () {
    // 返回一个迭代器对象
    return {
        current: this.from,
        last: this.to,

        next() {
            // 结束条件
            if (this.current > this.last) {
                return { done: true }
            } else {
                return {
                    value: this.current++, // current++ 第一次调用的时候,取值还是 1,
                    done: false
                }
            }
        }
    }
}

for (const item of range) {
    console.log(item);
}
// 1 2 3 4 5

我们通过实现range[Symbol.iterator]方法,让range对象也变成了可迭代对象,是不是很简单。我们只需要注意下几点

  1. range本身是没有next()方法的,是通过调用range[Symbol.iterator]()方法,返回了一个对象,即所谓的迭代器对象,通过迭代器对象上的next()方法获取到的值。

  2. next()方法返回的值也是一个对象,对象的格式遵循下面的规范

    {
        value: '', // 本次迭代的值
        done: true|false // 标识是否迭代完成 
    }
    
  3. for of的工作原理,实际上就是调用了对象的[Symbol.iterator]()方法,生成了一个迭代器对象,然后每次循环就调用一次next()方法,然后获取到返回值上的value,当标识符done === true时,就结束循环。

显示调用迭代器

还用上面的例子举例,其实我们可以显示的调用range的迭代器方法,生成一个迭代器对象,然后模拟出和for of一样的效果,比如:

// 获取迭代器对象
let iterator = range[Symbol.iterator]()
while (true) {
    let result = iterator.next()
    // 出口
    if (result.done) {
        break
    }
    console.log(result.value);
}
// 1 2 3 4 5

很少需要我们这样做,但是比 for..of 给了我们更多的控制权。例如,我们可以拆分迭代过程:迭代一部分,然后停止,做一些其他处理,然后再恢复迭代。

类数组对象

通过上面的学习,我们已经认清了可迭代对象的本质,实际上就是对象按照规范实现了Symbol[iterator]方法。那什么是类数组对象呢?我们工作中总能碰到这样的场景,某个对象它是可迭代的,某个对象它是类数组,某个对象即可迭代又是个类数组。可迭代我们已经知道了,类数组的本质其实也很简单,就是那些具有数值索引和length属性的对象。

一个小例子

如下面的例子,arrayLike对象即是一个类数组对象,它具备了类数组对象的特征,有数值索引、有length。但是类数组对象没有数组上的方法,比如pushpop等等,如果想用,就得使用call或者apply等改变一下this指向,借用一下Array原型上的方法。

let arrayLike = { // 有索引和 length 属性 => 类数组对象
    0: "Hello",
    1: "World",
    sayHi() {
        console.log('hhh');
    },
    length: 2
};
[].push.call(arrayLike, 'nn');
console.log(arrayLike);
/* 
{
  '0': 'Hello',
  '1': 'World',
  '2': 'nn',
  sayHi: [Function: sayHi],
  length: 3
}
*/
let str = [].join.call(arrayLike, '-')
console.log(str); // Hello-World-nn

我们发现,arrayLike对象是个类数组对象,但是它是不可迭代的,因为我们没有实现它的Symbol[iterator]迭代器方法。而在可迭代对象中举的例子,range对象,它是可迭代的,但它不是一个类数组,因为它没有类数组对象的特征(没有数值索引和length)。不过在我们实际工作中,有很多对象它即是类数组,又是可迭代的。比如字符串(for..of 对它们有效,并且有数值索引和 length 属性)。

Array.from

Array.from我们常用来将一个类数组对象转换为真正的数组。其实它还可以将可迭代对象也转换为数组。

类数组转数组

先看一下类数组转数组的例子:

let arrayLike = {
    0: 'hello',
    1: 'world',
    length: 2
}

console.log(Array.from(arrayLike)); // [ 'hello', 'world' ]

实际就是按照数值索引的位置,将对应的值塞到一个新数组中,数组的长度取决于length的长度,长度不够就截断,长度超出就在对应位置补undefined

可迭代转数组

再看一个可迭代转数组的例子,还拿我们写的range举例子。

let range = {
    from: 1,
    to: 5
}

range[Symbol.iterator] = function () {
    // 返回一个迭代器对象
    return {
        current: this.from,
        last: this.to,

        next() {
            // 结束条件
            if (this.current > this.last) {
                return { done: true }
            } else {
                return {
                    value: this.current++, // current++ 第一次调用的时候,取值还是 1,
                    done: false
                }
            }
        }
    }
}

// 将可迭代对象,转换为了数组
console.log(Array.from(range)); // [ 1, 2, 3, 4, 5 ]

实际就是获取到迭代器的next().value的值,依次塞到新数组中去,然后返回这个新数组。

总结

  1. 可迭代对象(iterable object):可以使用for of遍历的对象就是可迭代对象。

    可迭代的本质是该对象实现了Symbol.iterator迭代器方法。迭代器返回的是一个对象,即是所谓的迭代器对象。

    迭代器对象有next()方法,next()方法的返回值也是一个对象,遵循这样的规范{value: 'xxx', done: true|false}

  2. for of实际就是调用了一下[Symbol.iterator]()方法,获取了迭代器对象,然后每次遍历时调用一次迭代器对象的next()方法,获取到value的值,当标识符done === true时,表示遍历完成,结束for of

  3. 类数组对象(array-like object):具有数值索引和length属性的对象。

    类数组对象没有数组上的push、pop等方法,要想使用的话有两个办法:

    • 转换为数组,使用Array.from(arrayLike)
    • 使用call、apply、bind改变this的指向,借用Array原型上的方法,比如[].push.call(arrayLike, 'xxx')
  4. Array.from可以将可迭代对象或者类数组对象转换为一个真正的数组。

    • 转换类数组对象时,就是把索引值获取到,依次按照索引值塞进一个新数组中(长度由length属性确定,塞入的位置由索引值确定,长度不够就截断,长度超出就在对应位置塞undefined)。
    • 转换可迭代对象时,就是获取到每次迭代器对象next().value的值,依次塞到一个新数组中。

参考

zh.javascript.info/iterable#ar…