相关文章推荐
捣蛋的手术刀  ·  将java.io.BufferedReade ...·  2 年前    · 

我们在学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进行遍历了,其实上面这个小例子就涉及到了这两个概念:

  • 类数组对象(array-like object):有数值索引属性和length属性的对象,称为类数组对象,比如例子中的o1
  • 可迭代对象(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对象也变成了可迭代对象,是不是很简单。我们只需要注意下几点

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

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

    value: '', // 本次迭代的值 done: true|false // 标识是否迭代完成

    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的值,依次塞到新数组中去,然后返回这个新数组。

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

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

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

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

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

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

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

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

    分类:
    前端
    标签: