原文链接:http://javascript.info/iterable,translate with ❤️ by zhangbao.

可迭代对象是广义上的数组,这个概念让所有的对象都可以使用 for..of 循环。

数组本身是可迭代的,但又不只是数组。字符串也是可迭代的,许多内置的其他对象也是可迭代的。

可迭代对象在 JavaScript 核心里被广泛使用,我们将看到许多内置操作和方法依赖他们。

Symbol.iterator

我们可以很容易的制作自己的迭代器来理解这个概念。

例如,我们有一个对象,他不是数组,但是可以用 for..of 循环

比如,我们用 range 这个对象来表示某个区间里的整数集合。

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

为了能让对象可迭代(就是能用 for..of 工作),我们需要为这个对象添加一个属性,命名为 Symbol.iterator(一个特殊的内置 Symbol 值)。

  • for..of 循环执行的时候·,会调用这个方法(没有的话会报错)。

  • 该方法必须返回一个迭代器,一个携带 next 方法的对象。

  • 当 for..of 想要迭代下一个元素的时候,就会调用这个对象上的 next() 方法。

  • next() 方法的返回结果必须是 {done: Boolean, value: any} 的形式,done=true 表示迭代结束,否则 value 必须是新值。

这是 range 对象的完整实现:

  1. let range = {
  2. from: 1,
  3. to: 5
  4. };
  5. // 1. call to for..of initially calls this
  6. range[Symbol.iterator] = function() {
  7. // 2. ...it returns the iterator:
  8. return {
  9. current: this.from,
  10. last: this.to,
  11. // 3. next() is called on each iteration by the for..of loop
  12. next() {
  13. // 4. it should return the value as an object {done:.., value :...}
  14. if (this.current <= this.last) {
  15. return { done: false, value: this.current++ };
  16. } else {
  17. return { done: true };
  18. }
  19. }
  20. };
  21. };
  22. // now it works!
  23. for (let num of range) {
  24. alert(num); // 1, then 2, 3, 4, 5
  25. }

在这段代码中有一个重要的关注点分离:

  • range 对象本身没有 next 方法。

  • 相反,通过调用 rangeSymbol.iterator 会得到一个所谓的“迭代器”,他负责处理迭代操作。

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

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

像这样:

  1. let range = {
  2. from: 1,
  3. to: 5,
  4. [Symbol.iterator]() {
  5. this.current = this.from;
  6. return this;
  7. },
  8. next() {
  9. if (this.current <= this.to) {
  10. return { done: false, value: this.current++ };
  11. } else {
  12. return { done: true };
  13. }
  14. }
  15. };
  16. for (let num of range) {
  17. alert(num); // 1, then 2, 3, 4, 5
  18. }

现在 rangeSymbol.iterator 返回 range 对象本身:他有着必要的 next() 方法,并且会在 this.current 中记忆当前迭代到的元素。有时候这样也很好。不利的一面是,现在不可能有两个,同时在对象上运行的循环:它们将共享迭代状态,因为只有一个迭代器——对象本身。

tip: 无限迭代

无限迭代器也是可行的。例如,range 变成无限的因为有 range.to = Infinity。或者我们可以做一个可迭代的对象,它会产生一个无限序列的伪随机数字序列。也可以是有用的。

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

当然,for..of 循环在这样的迭代中循环是无穷无尽的,但是我们可以用 break 来阻止它。

字符串是可迭代的

数组和字符串有个最广泛使用的内置迭代器。

对字符串来说,for..of 可以用来遍历每一个字符:

  1. for (let char of "test") {
  2. alert( char ); // t, then e, then s, then t
  3. }

而且支持遍历代理对字符!

  1. let str = '𝒳😂';
  2. for (let char of str) {
  3. alert( char ); // 𝒳, and then 😂
  4. }

显式地调用迭代器

通常,可迭代对象隐藏在内部代码中。for..of 循环会自动作用它,这就是它所需要知道的。

但是为了更深入地理解事情,让我们看看如何显式地创建迭代器。

我们将以显示调用的方式实现跟用 for..of 迭代一个字符串一样的效果。这段代码得到一个字符串迭代器,并“手动”调用它:

  1. let str = "Hello";
  2. // does the same as
  3. // for (let char of str) alert(char);
  4. let iterator = str[Symbol.iterator]();
  5. while (true) {
  6. let result = iterator.next();
  7. if (result.done) break;
  8. alert(result.value); // outputs characters one by one
  9. }

这是很少需要的,但它让我们对这个过程有更多的控制比用 for..of 循环。例如,我们可以分割迭代过程:迭代一点,然后停止,做其他的事情,然后再继续。

可迭代对象和类数组

有两种官方术语看起来很相似,但却截然不同。请确保你能很好地理解它们,以避免混淆。

  • 可迭代对象是实现了 Symbol.iterator 方法属性的对象,就像上面描述的。

  • 类数组对象是拥有数值索引和 length 属性的对象,它们看起来像数组。

自然地,这些属性可以结合。例如,字符串就既是可迭代对象(可使用 for..of 循环遍历),也是一个类数组对象(拥有数值索引和 length 属性)。

但是可迭代对象不一定是类数组,反过来,类数组也不一定是可迭代对象。

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

下面在举一个是数组对象,但不是迭代对象的例子:

  1. let arrayLike = { // has indexes and length => array-like
  2. 0: "Hello",
  3. 1: "World",
  4. length: 2
  5. };
  6. // Error (no Symbol.iterator)
  7. for (let item of arrayLike) {}

那它们有没有什么共同点呢?可迭代对象和类数组都不是数组,它们没有 push,pop 等方法。如果我们有这样一个对象,并且想要像数组那样处理它,那就太不方便了。

Array.from

有一个通用的方法 Array.from 可以把这两者连接起来,它需要一个可迭代或数组般的值,并从中生成一个“真正的”数组。然后我们可以调用数组方法。

例如:

  1. let arrayLike = {
  2. 0: "Hello",
  3. 1: "World",
  4. length: 2
  5. };
  6. let arr = Array.from(arrayLike); // (*)
  7. alert(arr.pop()); // World (method works)

在 (*) 一行,Array.from 方法接收一个对象作为参数,如果发现是可迭代对象或者类数组的话,就从中生成一个新的数组。

同样的事情也发生在一个可迭代对象上:

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

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

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

第二个参数 mapFn 表示被处理的每个数组元素在最终添加到结果之前进行的操作,thisArg 允许设置 this 值

例如:

  1. // assuming that range is taken from the example above
  2. // square each number
  3. let arr = Array.from(range, num => num * num);
  4. alert(arr); // 1,4,9,16,25

这里我们使用 Array.from 把一个字符串变成一个字符数组:

  1. let str = '𝒳😂';
  2. // splits str into array of characters
  3. let chars = Array.from(str);
  4. alert(chars[0]); // 𝒳
  5. alert(chars[1]); // 😂
  6. alert(chars.length); // 2

与 str.split 不同,它依赖于字符串的可迭代性质,所以,可以正确地处理代理对。

从技术上讲,它的作用与此相同:

  1. let str = '𝒳😂';
  2. let chars = []; // Array.from internally does the same loop
  3. for (let char of str) {
  4. chars.push(char);
  5. }
  6. alert(chars);

……但更短。

我们甚至可以在它基础之上,创建一个能正确处理代理对字符的 slice 方法:

  1. function slice(str, start, end) {
  2. return Array.from(str).slice(start, end).join('');
  3. }
  4. let str = '𝒳😂𩷶';
  5. alert( slice(str, 1, 3) ); // 😂𩷶
  6. // native method does not support surrogate pairs
  7. alert( str.slice(1, 3) ); // garbage (two pieces from different surrogate pairs)

总结

可以使用 for..of 循环遍历的对象称之为可迭代对象

  • 从技术上讲,可迭代对象必须实现名为 Symbol.iteratir 的属性方法。

    • obj[Symbol.iterator] 的调用结果是一个迭代器。它用来处理之后的迭代处理。

    • 一个迭代器必须有 next 方法,返回结果是 {done: Boolean, value: any} 的形式,done: true 表示迭代结束,否则 value 指向写一个新值

  • for..of 循环会自动调用 Symbol.iterator 方法,当然我们也可以直接调用这个方法。

  • 数组和字符串都内置了迭代器,即部署了 Symbol.iterator。

  • 字符串迭代器能正确处理代理对字符串。

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

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

Array.from(obj[, mapFn, thisArg]) 用于从可迭代或者类数组对象 obj 中获得真实数组,然后我们就可以用数组方法了。可选的参数 mapFn 和 thisArg 允许我们在每一次迭代中对当前迭代元素进行处理。

(完)