数据类型Iterableobject(可迭代对象)
数据类型 Iterable object(可迭代对象)
可迭代(Iterable)对象是数组的泛化。这个概念是说任何对象都可以被定制为可在 f 循环中使⽤的对象。
数组是可迭代的。但不仅仅是数组。很多其他内建对象也都是可迭代的。例如字符串也是可迭代的。
如果从技术上讲,对象不是数组,⽽是表⽰某物的集合(列表,集合),f 是⼀个能够遍历它的很好的语法,因此,让我们来看看如何使其发挥作⽤。Symbol.iterator
通过⾃⼰创建⼀个对象,我们就可以轻松地掌握可迭代的概念。
例如,我们有⼀个对象,它并不是数组,但是看上去很适合使⽤ f 循环。
⽐如⼀个 range 对象,它代表了⼀个数字区间:
let range = {
from: 1,
to: 5
};
// 我们希望 f 这样运⾏:
// for(let num of range) ... num=1,2,3,4,5
为了让 range 对象可迭代(也就让 f 可以运⾏)我们需要为对象添加⼀个名为 Symbol.iterator 的⽅法(⼀个专门⽤于使对象可迭代的内置 symbol)。
当 f 循环启动时,它会调⽤这个⽅法(如果没到,就会报错)。这个⽅法必须返回⼀个迭代器(iterator) —— ⼀个有 next ⽅法的对象。
从此开始,f 仅适⽤于这个被返回的对象。
当 f 循环希望取得下⼀个数值,它就调⽤这个对象的 next() ⽅法。
next() ⽅法返回的结果的格式必须是 {done: Boolean, value: any},当 done=true 时,表⽰迭代结束,否则 value 是下⼀个值。
这是带有注释的 range 的完整实现:
let range = {
from: 1,
to: 5
};
// 1. f 调⽤⾸先会调⽤这个:
range[Symbol.iterator] = function() {
// ……它返回迭代器对象(iterator object):
// 2. 接下来,f 仅与此迭代器⼀起⼯作,要求它提供下⼀个值
return {
current: this.from,
last: ,
// 3. next() 在 f 的每⼀轮循环迭代中被调⽤
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 <= ) {
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 记忆了当前的迭代进程。这样更短,对吗?是的。有时这样也可以。
但缺点是,现在不可能同时在对象上运⾏两个 f 循环了:它们将共享迭代状态,因为只有⼀个迭代器,即对象本⾝。但是两个并⾏的 f 是很罕见的,即使在异步情况下。
⽆穷迭代器(iterator)
⽆穷迭代器也是可能的。例如,将 range 设置为 = Infinity,这时 range 则成为了⽆穷迭代器。或者我们可以创建⼀个可迭代对象,它⽣成⼀个⽆穷伪随机数序列。也是可能的。
next 没有什么限制,它可以返回越来越多的值,这是正常的。
当然,迭代这种对象的 f 循环将不会停⽌。但是我们可以通过使⽤ break 来停⽌它。
字符串是可迭代的
数组和字符串是使⽤最⼴泛的内建可迭代对象。
对于⼀个字符串,f 遍历它的每个字符:
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 ); // ,然后是
}
显式调⽤迭代器
为了更深层地了解底层知识,让我们来看看如何显式地使⽤迭代器。
我们将会采⽤与 f 完全相同的⽅式遍历字符串,但使⽤的是直接调⽤。这段代码创建了⼀个字符串迭代器,并“⼿动”从中获取值。
let str = "Hello";
// 和 f 做相同的事
// for (let char of str) alert(char);
let iterator = str[Symbol.iterator]();
while (true) {
let result = ();
if (result.done) break;
alert(result.value); // ⼀个接⼀个地输出字符
}
很少需要我们这样做,但是⽐ f 给了我们更多的控制权。例如,我们可以拆分迭代过程:迭代⼀部分,然后停⽌,做⼀些其他处理,然后再恢复迭代。
可迭代(iterable)和类数组(array-like)
有两个看起来很相似,但⼜有很⼤不同的正式术语。请你确保正确地掌握它们,以免造成混淆。
Iterable 如上所述,是实现了 Symbol.iterator ⽅法的对象。
Array-like 是有索引和 length 属性的对象,所以它们看起来很像数组。
当我们将 JavaScript ⽤于编写在浏览器或其他环境中的实际任务时,我们可能会遇到可迭代对象或类数组对象,或两者兼有。
例如,字符串即是可迭代的(f 对它们有效),⼜是类数组的(它们有数值索引和 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 ⽅法不同,它依赖于字符串的可迭代特性。因此,就像 f ⼀样,可以正确地处理代理对(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 扩展字符碎⽚拼接的结果)
总结
可以应⽤ f 的对象被称为可迭代的。
技术上来说,可迭代对象必须实现 Symbol.iterator ⽅法。
obj[Symbol.iterator]() 的结果被称为迭代器(iterator)。由它处理进⼀步的迭代过程。
⼀个迭代器必须有 next() ⽅法,它返回⼀个 {done: Boolean, value: any} 对象,这⾥ done:true 表明迭代结束,否则 value 就是下⼀个值。
Symbol.iterator ⽅法会被 f ⾃动调⽤,但我们也可以直接调⽤它。
内置的可迭代对象例如字符串和数组,都实现了 Symbol.iterator。
字符串迭代器能够识别代理对(surrogate pair)。(译注:代理对也就是 UTF-16 扩展字符。)
有索引属性和 length 属性的对象被称为类数组对象。这种对象可能还具有其他属性和⽅法,但是没有数组的内建⽅法。
如果我们仔细研究⼀下规范 —— 就会发现⼤多数内建⽅法都假设它们需要处理的是可迭代对象或者类数组对象,⽽不是“真正的”数组,因为这样抽象度更⾼。Array.from(obj[, mapFn, thisArg]) 将可迭代对象或类数组对象 obj 转化为真正的数组 Array,然后我们就可以对它应⽤数组的⽅法。可选参数 mapFn 和thisArg 允许我们将函数应⽤到每个元素。
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论