2022 年 7 月 15 日

可迭代对象

可迭代对象是数组的概括。这个概念允许我们在 for..of 循环中使用任何对象。

当然,数组是可迭代的。但还有许多其他内置对象也是可迭代的。例如,字符串也是可迭代的。

如果一个对象在技术上不是数组,但表示某个集合(列表、集合),那么 for..of 是遍历它的一个很好的语法,所以让我们看看如何让它发挥作用。

Symbol.iterator

我们可以通过制作我们自己的可迭代对象来轻松掌握可迭代对象的理念。

例如,我们有一个不是数组的对象,但看起来适用于 for..of

就像表示数字区间的一个 range 对象

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

// We want the for..of to work:
// for(let num of range) ... num=1,2,3,4,5

为了使 range 对象可迭代(从而让 for..of 工作),我们需要向该对象添加一个名为 Symbol.iterator 的方法(一个专门用于此的特殊内置符号)。

  1. for..of 开始时,它调用该方法一次(如果未找到则出错)。该方法必须返回一个迭代器——一个带有 next 方法的对象。
  2. 接下来,for..of仅使用该返回的对象工作。
  3. for..of 需要下一个值时,它在该对象上调用 next()
  4. next() 的结果必须具有 {done: Boolean, value: any} 的形式,其中 done=true 表示循环已完成,否则 value 是下一个值。

以下是带有注释的 range 的完整实现

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

// 1. call to for..of initially calls this
range[Symbol.iterator] = function() {

  // ...it returns the iterator object:
  // 2. Onward, for..of works only with the iterator object below, asking it for next values
  return {
    current: this.from,
    last: this.to,

    // 3. next() is called on each iteration by the for..of loop
    next() {
      // 4. it should return the value as an object {done:.., value :...}
      if (this.current <= this.last) {
        return { done: false, value: this.current++ };
      } else {
        return { done: true };
      }
    }
  };
};

// now it works!
for (let num of range) {
  alert(num); // 1, then 2, 3, 4, 5
}

请注意可迭代对象的核心理念:分离关注点。

  • range 本身没有 next() 方法。
  • 相反,另一个对象(所谓的“迭代器”)通过调用 range[Symbol.iterator]() 创建,其 next() 为迭代生成值。

因此,迭代器对象与其迭代的对象是分开的。

从技术上讲,我们可以合并它们并将 range 本身用作迭代器,以简化代码。

就像这样

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

  [Symbol.iterator]() {
    this.current = this.from;
    return this;
  },

  next() {
    if (this.current <= this.to) {
      return { done: false, value: this.current++ };
    } else {
      return { done: true };
    }
  }
};

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

现在 range[Symbol.iterator]() 返回 range 对象本身:它具有必要的 next() 方法,并在 this.current 中记住当前迭代进度。更短?是的。有时这样做也没问题。

缺点是现在不可能同时对该对象运行两个 for..of 循环:它们将共享迭代状态,因为只有一个迭代器——对象本身。但即使在异步场景中,两个并行的 for-of 也是罕见的。

无限迭代器

无限迭代器也是可能的。例如,对于 range.to = Infinityrange 变得无限。或者我们可以创建一个生成无限伪随机数序列的可迭代对象。这也可能有用。

next 没有限制,它可以返回越来越多的值,这是正常的。

当然,对这样一个可迭代对象的 for..of 循环将是无穷无尽的。但我们始终可以使用 break 停止它。

字符串是可迭代的

数组和字符串是最广泛使用的内置可迭代对象。

对于字符串,for..of 循环遍历其字符

for (let char of "test") {
  // triggers 4 times: once for each character
  alert( char ); // t, then e, then s, then t
}

它与代理对一起也能正常工作!

let str = '𝒳😂';
for (let char of str) {
    alert( char ); // 𝒳, and then 😂
}

显式调用迭代器

为了更深入地理解,让我们看看如何显式使用迭代器。

我们将以与 for..of 完全相同的方式遍历一个字符串,但使用直接调用。此代码创建一个字符串迭代器,并“手动”从中获取值

let str = "Hello";

// does the same as
// for (let char of str) alert(char);

let iterator = str[Symbol.iterator]();

while (true) {
  let result = iterator.next();
  if (result.done) break;
  alert(result.value); // outputs characters one by one
}

这很少需要,但与 for..of 相比,它给了我们对进程的更多控制权。例如,我们可以拆分迭代过程:迭代一段时间,然后停止,做其他事情,然后稍后继续。

可迭代对象和类数组

两个官方术语看起来相似,但差别很大。请确保你充分理解它们,以避免混淆。

  • 可迭代对象是实现 Symbol.iterator 方法的对象,如上所述。
  • 类数组是有索引和 length 的对象,因此它们看起来像数组。

当我们在浏览器或任何其他环境中将 JavaScript 用于实际任务时,我们可能会遇到可迭代对象或类数组,或两者兼有。

例如,字符串既是可迭代的(for..of 对它们有效),又是类数组(它们具有数字索引和 length)。

但可迭代对象可能不是类数组。反之亦然,类数组可能不是可迭代对象。

例如,上面示例中的 range 是可迭代的,但不是类数组,因为它没有索引属性和 length

这是一个类数组但不可迭代的对象

let arrayLike = { // has indexes and length => array-like
  0: "Hello",
  1: "World",
  length: 2
};

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

可迭代对象和类数组通常不是数组,它们没有 pushpop 等。如果我们有这样的对象并希望像使用数组一样使用它,那会相当不方便。例如,我们希望使用数组方法处理 range。如何实现?

Array.from

有一个通用方法 Array.from,它接受一个可迭代对象或类数组值,并从中生成一个“真正的”Array。然后,我们可以在它上面调用数组方法。

例如

let arrayLike = {
  0: "Hello",
  1: "World",
  length: 2
};

let arr = Array.from(arrayLike); // (*)
alert(arr.pop()); // World (method works)

(*) 行的 Array.from 接受对象,检查它是否是可迭代对象或类数组,然后创建一个新数组并将所有项目复制到其中。

可迭代对象也是如此

// assuming that range is taken from the example above
let arr = Array.from(range);
alert(arr); // 1,2,3,4,5 (array toString conversion works)

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

Array.from(obj[, mapFn, thisArg])

可选的第二个参数 mapFn 可以是一个函数,在将每个元素添加到数组之前应用该函数,而 thisArg 允许我们为其设置 this

例如

// assuming that range is taken from the example above

// square each number
let arr = Array.from(range, num => num * num);

alert(arr); // 1,4,9,16,25

这里我们使用 Array.from 将字符串转换为字符数组

let str = '𝒳😂';

// splits str into array of characters
let chars = Array.from(str);

alert(chars[0]); // 𝒳
alert(chars[1]); // 😂
alert(chars.length); // 2

str.split 不同,它依赖于字符串的可迭代性,因此,就像 for..of 一样,可以正确处理代理对。

从技术上讲,它在这里的作用与

let str = '𝒳😂';

let chars = []; // Array.from internally does the same loop
for (let char of str) {
  chars.push(char);
}

alert(chars);

相同,但更简洁。

我们甚至可以在其上构建代理感知的 slice

function slice(str, start, end) {
  return Array.from(str).slice(start, end).join('');
}

let str = '𝒳😂𩷶';

alert( slice(str, 1, 3) ); // 😂𩷶

// the native method does not support surrogate pairs
alert( str.slice(1, 3) ); // garbage (two pieces from different surrogate pairs)

总结

可以在 for..of 中使用的对象称为可迭代对象

  • 从技术上讲,可迭代对象必须实现名为 Symbol.iterator 的方法。
    • obj[Symbol.iterator]() 的结果称为迭代器。它处理进一步的迭代过程。
    • 迭代器必须具有名为 next() 的方法,该方法返回一个对象 {done: Boolean, value: any},其中 done:true 表示迭代过程的结束,否则 value 是下一个值。
  • Symbol.iterator 方法由 for..of 自动调用,但我们也可以直接调用它。
  • 字符串或数组等内置可迭代对象也实现了 Symbol.iterator
  • 字符串迭代器了解代理对。

具有索引属性和 length 的对象称为类数组。此类对象可能还具有其他属性和方法,但缺少数组的内置方法。

如果我们查看规范,我们会看到大多数内置方法都假定它们使用可迭代对象或类数组而不是“真实”数组,因为这更抽象。

Array.from(obj[, mapFn, thisArg]) 从可迭代对象或类数组 obj 中生成一个真正的 Array,然后我们可以在其上使用数组方法。可选参数 mapFnthisArg 允许我们对每个项目应用一个函数。

教程地图

评论

在评论之前先阅读此内容…
  • 如果您有改进建议,请 提交 GitHub 问题 或提交拉取请求,而不是进行评论。
  • 如果您无法理解文章中的某些内容,请详细说明。
  • 要插入一些代码,请使用 <code> 标记,对于多行代码,请将其包装在 <pre> 标记中,对于 10 行以上的代码,请使用沙盒 (plnkrjsbincodepen…)