写点什么

JS Iterable object (可迭代对象)

作者:达摩
  • 2022 年 5 月 19 日
  • 本文字数:3516 字

    阅读完需:约 12 分钟

JS Iterable object (可迭代对象)

Iterable object (可迭代对象)

可迭代(Itearble) 对象是数组的泛化。这个概念是说任何对象都可以被定制为可在 for...of 循环中使用的对象。数组是可迭代的,但不仅仅是数组。很多其他内建对象也都是可迭代的。例如字符串也是可迭代的。如果从技术上讲,对象不是数组,而是表示某物的集合(列表,集合), for...of 是一个能够遍历它的很好的语法,因此,让我们来看看如何使其发挥作用。

Symbol.iteartor

通过自己创建一个对象,我们就可以很轻松地掌握可迭代的概念。例如,我们有一个对象,它并不是数组,但是看上去很适合使用 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 可以运行)我们需要为对象添加一个名为 Symble.iterator 的方法(一个专门用于使对象可迭代的内建 symbol)。


1.当 for...of 循环启动时,它会调用这个方法(如果没找到,就会报错)。这个方法必须返回一个 迭代器(iterator) —— 一个有 next 方法的对象。

2.从此开始,for...of 仅适用于这个被返回对象

3.当 for...of 循环希望取得下一个数值,它就调用这个对象的 next() 方法。

  1. 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() 方法。相反,是通过调用 rang[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}
复制代码

现在 rangeSymbol.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 "text"){  //触发 4 次,每个字符一次  alert(char);  //t,e,x,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){}
复制代码

可迭代对象和类数组对象通常都 不是数组,它们没有 pushpop 等方法。如果我们有一个这样的对象,并想像数组那样操作它,那就非常不方便。例如,我们想使用数组方法操作 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(rang);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);
arlert(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)。

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

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('');
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 扩展字符碎片拼接的结果)
复制代码


用户头像

达摩

关注

还未添加个人签名 2019.12.04 加入

还未添加个人简介

评论

发布
暂无评论
JS Iterable object (可迭代对象)_js_达摩_InfoQ写作社区