JavaScript Iterable object(可迭代对象)

本文深入探讨了JavaScript中的可迭代对象和迭代器的概念,解释了如何通过Symbol.iterator使对象变得可迭代,并展示了如何手动创建迭代器。通过实例,阐述了字符串、数组等内建对象的可迭代性,以及如何使用Array.from将可迭代对象或类数组对象转换为数组。此外,文章还提到了无穷迭代器的可能性以及显式调用迭代器的方法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

可迭代(Iterable) 对象是数组的泛化。这个概念是说任何对象都可以被定制为可在 for..of 循环中使用的对象。

数组是可迭代的。但不仅仅是数组。很多其他内建对象也都是可迭代的。例如字符串也是可迭代的。

如果从技术上讲,对象不是数组,而是表示某物的集合(列表,集合),for..of 是一个能够遍历它的很好的语法,因此,让我们来看看如何使其发挥作用。

Symbol.iterator

通过自己创建一个对象,我们就可以轻松地掌握可迭代的概念。

例如,我们有一个对象,它并不是数组,但是看上去很适合使用 for..of 循环。

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

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

// 我们希望 for..of 这样运行:
// for(let num of range) ... num=1,2,3,4,5

为了让 range 对象可迭代(也就让 for..of 可以运行)我们需要为对象添加一个名为 Symbol.iterator 的方法(一个专门用于使对象可迭代的内置 symbol)。

  1. 当 for..of 循环启动时,它会调用这个方法(如果没找到,就会报错)。这个方法必须返回一个 迭代器(iterator) —— 一个有 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. for..of 调用首先会调用这个:
range[Symbol.iterator] = function() {

  // ……它返回迭代器对象(iterator object):
  // 2. 接下来,for..of 仅与此迭代器一起工作,要求它提供下一个值
  return {
    current: this.from,
    last: this.to,

    // 3. next() 在 for..of 的每一轮循环迭代中被调用
    next() {
      // 4. 它将会返回 {done:.., value :...} 格式的对象
      if (this.current <= this.last) {
        return { done: false, value: this.current++ };
      } else {
        return { done: true };
      }
    }
  };
};

// 现在它可以运行了!
for (let num of range) {
  alert(num); // 1, 然后是 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, 然后是 2, 3, 4, 5
}

现在 range[Symbol.iterator]() 返回的是 range 对象自身:它包括了必需的 next() 方法,并通过 this.current 记忆了当前的迭代进程。这样更短,对吗?是的。有时这样也可以。

但缺点是,现在不可能同时在对象上运行两个 for..of 循环了:它们将共享迭代状态,因为只有一个迭代器,即对象本身。但是两个并行的 for..of 是很罕见的,即使在异步情况下。

无穷迭代器(iterator)

无穷迭代器也是可能的。例如,将 range 设置为 range.to = Infinity,这时 range 则成为了无穷迭代器。或者我们可以创建一个可迭代对象,它生成一个无穷伪随机数序列。也是可能的。

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

当然,迭代这种对象的 for..of 循环将不会停止。但是我们可以通过使用 break 来停止它。

字符串是可迭代的

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

对于一个字符串,for..of 遍历它的每个字符:

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

对于代理对(surrogate pairs),它也能正常工作!(译注:这里的代理对也就指的是 UTF-16 的扩展字符)

let str = '';
for (let char of str) {
    alert( char ); // 
}

显式调用迭代器

为了更深层地了解底层知识,让我们来看看如何显式地使用迭代器。

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

let str = "Hello";

// 和 for..of 做相同的事
// 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); // 一个接一个地输出字符
}

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

可迭代(iterable)和类数组(array-like)

有两个看起来很相似,但又有很大不同的正式术语。请你确保正确地掌握它们,以免造成混淆。

  • Iterable 如上所述,是实现了 Symbol.iterator 方法的对象。
  • Array-like 是有索引和 length 属性的对象,所以它们看起来很像数组。

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

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

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

例如,上面例子中的 range 是可迭代的,但并非类数组对象,因为它没有索引属性,也没有 length 属性。

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

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

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

可迭代对象和类数组对象通常都 不是数组,它们没有 push 和 pop 等方法。如果我们有一个这样的对象,并想像数组那样操作它,那就非常不方便。例如,我们想使用数组方法操作 range,应该如何实现呢?

Array.from

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

例如:

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

let arr = Array.from(arrayLike); // (*)
alert(arr.pop()); // World(pop 方法有效)

在 (*) 行的 Array.from 方法接受对象,检查它是一个可迭代对象或类数组对象,然后创建一个新数组,并将该对象的所有元素复制到这个新数组。

如果是可迭代对象,也是同样:

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

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

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

可选的第二个参数 mapFn 可以是一个函数,该函数会在对象中的元素被添加到数组前,被应用于每个元素,此外 thisArg 允许我们为该函数设置 this

例如:

// 假设 range 来自上文例子中

// 求每个数的平方
let arr = Array.from(range, num => num * num);

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

现在我们用 Array.from 将一个字符串转换为单个字符的数组:

let str = '';

// 将 str 拆分为字符数组
let chars = Array.from(str);

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

与 str.split 方法不同,它依赖于字符串的可迭代特性。因此,就像 for..of 一样,可以正确地处理代理对(surrogate pair)。(译注:代理对也就是 UTF-16 扩展字符。)

技术上来讲,它和下面这段代码做的是相同的事:

let str = '';

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

alert(chars);

……但 Array.from 精简很多。

我们甚至可以基于 Array.from 创建代理感知(surrogate-aware)的slice 方法(译注:也就是能够处理 UTF-16 扩展字符的 slice 方法):

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 扩展字符碎片拼接的结果)

总结

可以应用 for..of 的对象被称为 可迭代的

  • 技术上来说,可迭代对象必须实现 Symbol.iterator 方法。
    • obj[Symbol.iterator]() 的结果被称为 迭代器(iterator)。由它处理进一步的迭代过程。
    • 一个迭代器必须有 next() 方法,它返回一个 {done: Boolean, value: any} 对象,这里 done:true 表明迭代结束,否则 value 就是下一个值。
  • Symbol.iterator 方法会被 for..of 自动调用,但我们也可以直接调用它。
  • 内置的可迭代对象例如字符串和数组,都实现了 Symbol.iterator
  • 字符串迭代器能够识别代理对(surrogate pair)。(译注:代理对也就是 UTF-16 扩展字符。)

有索引属性和 length 属性的对象被称为 类数组对象。这种对象可能还具有其他属性和方法,但是没有数组的内建方法。

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

Array.from(obj[, mapFn, thisArg]) 将可迭代对象或类数组对象 obj 转化为真正的数组 Array,然后我们就可以对它应用数组的方法。可选参数 mapFn 和 thisArg 允许我们将函数应用到每个元素。

### 解决JavaScript中`Array.every()`方法报错的问题 当遇到`Array.every()`方法报错的情况时,常见的原因可能涉及传入的参数不合法、数组为空或是回调函数内部逻辑有误。为了更好地理解并解决问题,先来回顾一下`every()`方法的工作原理。 #### `every()` 方法概述 `every()`方法测试一个数组内的所有元素是否都能通过某个指定函数的测试。它返回一个布尔值。如果该函数为数组中的每一个元素都返回真值,则返回true;否则返回false。此方法不会改变原数组[^1]。 ```javascript // 正确使用 every() 的例子 const numbers = [1, 2, 3, 4]; const allPositive = numbers.every((value) => value >= 0); console.log(allPositive); // 输出: true ``` #### 常见错误及其解决方案 - **传递给`every()`的方法不是有效的函数** 如果作为第一个参数传递给`every()`的内容不是一个可调用的对象(即函数),那么将会抛出TypeError异常。确保总是提供有效函数作为检测条件。 - **尝试对非数组对象应用`every()`** 尝试在一个不是真正的数组类型的对象上调用`every()`也会引发错误。应确认操作的目标确实是一个数组实例。 - **处理空数组情况** 对于空数组而言,`every()`会默认认为所有的成员均满足条件而立即返回`true`。这通常是预期行为,但如果这不是期望的结果,在设计算法时需特别注意这一点。 - **回调函数内存在未定义变量或语法错误** 这是最容易被忽视的地方之一。仔细检查回调函数体内的每一行代码是否有潜在问题,比如拼写错误、缺少分号等基本语法规则违反情形。 ```javascript try { let result; // 错误示范:试图对字符串执行 every() const str = "hello"; result = str.every(() => true); } catch (error) { console.error(error.message); } // 修改后的版本:转换成数组后再调用 every() const strArr = Array.from("hello"); result = strArr.every(() => true); console.log(result); // 输出: true ``` #### 实际案例分析 考虑到提供的具体场景并未直接提及`every()`的具体实现细节,这里假设遇到了由于输入数据不符合预期而导致的错误。对于原始给出的例子来说: ```javascript const arr1 = 'jimmy'.split(''); const arr2 = arr1.reverse(); const arr3 = arr2.splice(0,5); const arr4 = arr3.slice(0,5); const arrControl = 'jimmy'.split('').reverse().splice(0,5).slice(0,5); ``` 这段代码实际上并没有展示`every()`的应用,因此无法从中推断具体的错误所在。不过,基于上述讨论的原则,可以建议如下做法以避免可能出现的相关错误: - 确认任何用于迭代的数据结构都是实际存在的数组; - 验证所提供的回调函数能够正常工作而不依赖外部状态; - 使用严格模式(`'use strict';`)帮助捕获一些隐式的编程失误。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值