可迭代对象 - 你不知道的 JS

  1. Symbol.iterator
  2. 字符串可迭代
  3. 显示调用迭代器方法
  4. 类数组对象
    1. Array.from😀
  5. 总结

似曾相识

Symbol.iterator

一个 range 对象,它代表了一个数字区间:

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

for ... in 可以遍历得到此对象属性 from,to
for ... of 无法用在此对象上,因为不可迭代。
为了让 range 对象可迭代,我们需要为对象添加名为 Symbol.iterator 方法。此 symbol 乃专门用于使对象可迭代的 内置 symbol

  • 此方法调用后,返回值时迭代器,它是一个对象,带有 next 方法
  • for ... of 尝试搜寻此方法,拿到此迭代器对象。(存不存在原型继承?存在)
  • for..of 循环希望取得下一个数值,它就调用这个对象的 next() 方法
  • next 方法返回格式:{done:Boolean, value:any}。当 done=true,迭代结束。

NOTE: 用了 ES6 的对象内方法的简写

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

// 是一个函数
range[Symbol.iterator] = function() {  
  // 函数返回迭代器对象
  return {
    current: this.from,
    last: this.to,
    next() {
      if (this.current <= this.last) {
      // 返回{done:...,value:...}格式对象
        return {
          done: false,
          value: this.current++
        };
      } else {
        return {
          done: true;  // 不需要有 value 了
        }
      }
    }
  }
}

现在 for ... of 可以运行了!
注意包含 next 方法的对象,里面直接用 this 并不能使用 range 的 this

for (let num of range) {
    alert(num); // 1, 然后是 2, 3, 4, 5
}

可以看出,迭代器对象和与其进行迭代的对象可以是分开的。
for ... of 拿到的是 range[Symbol.iterator] 函数返回的对象,迭代的就是这个对象

但是调用迭代器时,通过 range 对象调用的,因为如果 range[Symbol.iterator] 是箭头函数,this 会丢失

可以把迭代器直接写到对象里面。

// 写在里面的版本
let range = {
    from: 1,
    to: 5,
    [Symbol.iterator]: function () {
        // 函数返回一个对象, 对象必须包含一个 next 函数
        let cur = this.from;
        let last = this.to;
        // 这里的 this
        return {
            // 对象包含一个 next 方法, next 方法返回具有固定格式的对象
            next: () => {
                // this 就是 return 的对象 外层 的 this
                // console.log(this.to);
                if (cur <= last) {
                    return {
                        done: false,
                        value: cur++,
                    }
                } else {
                    return {
                        done: true,
                    }
                }
            }
        }
    }
}

更进一步,把迭代器对象的 next 剥离出来,直接放到需要迭代的对象中

// 剥离 next 函数
let range = {
    from: 1,
    to: 5,
    [Symbol.iterator]() {
        this.cur = this.from;
        return this;
    },
    next() {
        {
            if (this.cur <= this.to) {
                return {
                    done: false,
                    value: this.cur++,
                }
            } else {
                return {
                    done: true,
                }
            }

        }
    }
}

既然我们可以定义迭代器,那么,什么时候停止迭代,我们说了算。所以可以把迭代终止条件设为无限

字符串可迭代

代码验证:

Array.prototype.hasOwnProperty(Symbol.iterator)
true
String.prototype.hasOwnProperty(Symbol.iterator)
true

因此字符串也可以像数组一样使用 for...of

for (let char of "test") {
    // 触发 4 次,每个字符一次
    alert( char ); // t, then e, then s, then t
}

显示调用迭代器方法

这样做比 for。。。of 更加灵活

let str = "WORLD";
// 拿到迭代器
let iterator = str[Symbol.iterator];

while (true) {
  let result = iterator.next();
  if (result.done) break;
  console.log(result.value);
}

类数组对象

  • 类数组对象指:有索引,有 length 属性的对象
  • 可迭代对象:实现了 Symbol.iterator 的对象

确保正确地掌握它们,以免造成混淆

下面这个对象则是类数组的,但是不可迭代:

let arrayLike = { // 有索引和 length 属性 => 类数组对象
    0: "Hello",
    1: "World",
    length: 2
};

// Error (no Symbol.iterator)
for (let item of arrayLike) {}

Array.from😀

有一个全局方法 Array.from 可以接受一个 ** 可迭代或类数组 ** 的值,并从中获取一个 “真正的” 数组。然后我们就可以对其调用数组方法

对于上面那个类数组但是不可迭代的对象:

let arrayLike = {
    0: "Hello",
    1: "World",
    length: 2
};
let arr = Array.from(arrayLike); // (*)
alert(arr.pop()); // World(pop 方法有效)

对于可迭代,但是没有索引,没有 length 属性的对象:

// 假设 range 来自上文的例子中
let arr = Array.from(range);
alert(arr); // 1,2,3,4,5 (数组的 toString 转化方法生效)

Array.from 的完整语法允许我们提供一个可选的 “映射(mapping)” 函数

Array.from(obj, 映射函数, 映射函数的 this 参数)

例如:

// 假设 range 来自上文例子中
// 求每个数的平方
let arr = Array.from(range, num => num * num);
alert(arr); // 1,4,9,16,25

使用此方法处理字符串,相比使用字符串的 split 方法更方便处理 UTF-16 扩展字符

使用 Array.from 处理字符串相当于调用字符串的迭代器:

let str = '😂😂';
let chars = []; // Array.from 内部执行相同的循环
for (let char of str) {
    chars.push(char);
}
alert(chars);

有时,原生方法不支持 UTF16 扩展字符时,可以使用 Array.from

// 套一个 Array.from
function slice(str, start, end) {
    return Array.from(str).slice(start, end).join('');
}
let str = '𝒳😂𩷶';
alert( slice(str, 1, 3) ); // 😂𩷶
// 原生方法不支持识别代理对(译注:UTF-16 扩展字符)
alert( str.slice(1, 3) ); // 乱码(两个不同 UTF-16 扩展字符碎片拼接的结果)

总结

  • 可迭代对象必须实现 Symbol.iterator 方法,此方法返回一个迭代器对象,对象包含 next 方法,方法返回 {done: Boolean, value: any}
    象,这里 done:true 表明迭代结束, value 是一个值
  • Symbol.iterator 方法会被 for..of 自动调用,但我们也可以直接调用它
  • 内置的可迭代对象例如字符串和数组,都实现了 Symbol.iterator
  • 字符串迭代器可以正确识别 UTF-16 扩展字符😂

仔细研究一下规范 —— 就会发现大多数内建方法都假设它们需要处理的是可迭代对象或者类数组对象,而不是 “真正的” 数组,因为这样抽象度更高


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论。
我的空间