Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
145 changes: 76 additions & 69 deletions 1-js/05-data-types/06-iterable/article.md
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@

# Iterables(可迭代对象)

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

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

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

在核心 JavaScript 中,可迭代对象用途广泛。我们将会看到,很多内建的操作和方法都依赖于它。

## Symbol.iterator

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

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

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

```js
let range = {
Expand All @@ -25,30 +26,31 @@ let range = {
// for(let num of range) ... num=1,2,3,4,5
```

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

- 当 `for..of` 循环开始,它将会调用这个方法(如果没找到,就会报错)。
- 这个方法必须返回一个迭代器 —— 一个有 `next` 方法的对象
- 当 `for..of` 循环希望取得下一个数值,它就调用这个对象的 `next()` 方法。
- `next()` 返回结果的格式必须是 `{done: Boolean, value: any}`,当 `done=true` 时,表示迭代结束,否则 `value` 必须是一个未被迭代的新值
1. 当 `for..of` 循环启动时,它会调用这个方法(如果没找到,就会报错)。这个方法必须返回一个 **迭代器(iterator)** —— 一个有 `next` 方法的对象
2. 从此开始,`for..of` **仅适用于这个被返回的对象**
3. 当 `for..of` 循环希望取得下一个数值,它就调用这个对象的 `next()` 方法。
4. `next()` 方法返回的结果的格式必须是 `{done: Boolean, value: any}`,当 `done=true` 时,表示迭代结束,否则 `value` 是下一个值

这是 `range` 的全部实现
这是带有注释的 `range` 的完整实现

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

// 1. 使用 for..of 将会首先调用它
// 1. for..of 调用首先会调用这个
range[Symbol.iterator] = function() {

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

// 3. next() 将在 for..of 的每一轮循环迭代中被调用
// 3. next() for..of 的每一轮循环迭代中被调用
next() {
// 4. 它将会返回 {done:.., value :...} 格式的对象
if (this.current <= this.last) {
Expand All @@ -62,18 +64,18 @@ range[Symbol.iterator] = function() {

// 现在它可以运行了!
for (let num of range) {
alert(num); // 1, 然后 2, 3, 4, 5
alert(num); // 1, 然后是 2, 3, 4, 5
}
```

这段代码中有几点需要着重关注:
请注意可迭代对象的核心功能:关注点分离。

- `range` 自身没有 `next()` 方法。
- 相反,是调用 `range[Symbol.iterator]()` 时将会被创建的另一个所谓的“迭代器”对象,将会处理迭代操作
- 相反,是通过调用 `range[Symbol.iterator]()` 创建了另一个对象,即所谓的“迭代器”对象,并且它的 `next` 会为迭代生成值

所以,迭代器对象和迭代的对象其实是分离的
因此,迭代器对象和与其进行迭代的对象是分开的

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

就像这样:

Expand All @@ -97,100 +99,105 @@ let range = {
};

for (let num of range) {
alert(num); // 1, 然后 2, 3, 4, 5
alert(num); // 1, 然后是 2, 3, 4, 5
}
```

现在 `range[Symbol.iterator]()` 返回了 `range` 对象自身:它包括了必需的 `next()` 方法并通过 `this.current` 记忆了当前迭代进程。有时候,这样也可以。但缺点是,现在不可能同时在 `range` 上运行两个 `for..of` 循环了:这两个循环将会共享迭代状态,因为仅有一个迭代器 —— 也就是对象自身。
现在 `range[Symbol.iterator]()` 返回的是 `range` 对象自身:它包括了必需的 `next()` 方法,并通过 `this.current` 记忆了当前的迭代进程。这样更短,对吗?是的。有时这样也可以。

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

```smart header="Infinite iterators"
无穷迭代也是可行的。例如,`range` 设置为 `range.to = Infinity` 则成为无穷迭代。或者我们可以创建一个可迭代对象,它生成一个伪随机数无穷序列。也是可用的
```smart header="无穷迭代器(iterator)"
无穷迭代器也是可能的。例如,`range` 设置为 `range.to = Infinity`,这时 `range` 则成为了无穷迭代器。或者我们可以创建一个可迭代对象,它生成一个无穷伪随机数序列。也是可能的

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

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


## 字符串可迭代
## 字符串是可迭代的

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

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

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

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

```js run
let str = '𝒳😂';
for (let char of str) {
alert( char ); // 𝒳,然后 😂
alert( char ); // 𝒳,然后是 😂
}
```

## 显式调用迭代器

通常情况下,迭代器的内部函数对外部代码是隐藏的。`for..of` 循环可以工作,就是代码需要了解的所有内容了
为了更深层地了解底层知识,让我们来看看如何显式地使用迭代器

但是为了更深层的了解知识概念,我们来看看如何显式的创建迭代器。

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

```js run
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); // 一个一个输出字符
alert(result.value); // 一个接一个地输出字符
}
```

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

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

## 可迭代对象和类数组对象 [#array-like]
有两个看起来很相似,但又有很大不同的正式术语。请你确保正确地掌握它们,以免造成混淆。

这两个正式的术语很相似,但是却非常不同。请你确保良好的掌握它们,并避免混淆。
- **Iterables** 如上所述,是实现 `Symbol.iterator` 方法的对象。
- **Array-likes** 是有索引和 `length` 属性的对象,所以它们看起来很像数组。

- **Iterables** 是应用于 `Symbol.iterator` 方法的对象,像上文所述。
- **Array-likes** 是有索引和 `length` 属性的对象,所以它们很像数组。
当我们将 JavaScript 用于编写在浏览器或其他环境中的实际任务时,我们可能会遇到可迭代对象或类数组对象,或两者兼有。

很自然的,这些属性都可以结合起来。例如,字符串既是可迭代对象(`for..of` 可以迭代字符串)也是类数组对象(它们有数字索引也有 `length` 属性)。
例如,字符串即使可叠戴的(`for..of` 对它们有效),又是类数组地(它们有数值索引和 `length` 属性)。

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

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

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

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

*!*
// 错误(没有 Symbol.iterator
// Error (no Symbol.iterator)
for (let item of arrayLike) {}
*/!*
```

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

## Array.from

有一个全局方法 [Array.from](mdn:js/Array/from) 可以把它们全都结合起来。它以一个可迭代对象或者类数组对象作为参数并返回一个真正的 `Array` 数组。然后我们就可以用该对象调用数组的方法了
有一个全局方法 [Array.from](mdn:js/Array/from) 可以可以接受一个可迭代或类数组的值,并从中获取一个“真实的”数组。然后我们就可以对其调用数组方法了

例如:

Expand All @@ -204,25 +211,25 @@ let arrayLike = {
*!*
let arr = Array.from(arrayLike); // (*)
*/!*
alert(arr.pop()); // World(pop 方法生效
alert(arr.pop()); // World(pop 方法有效
```

在行 `(*)``Array.from` 方法以一个对象为参数,检测到它是一个可迭代对象或类数组对象,然后将它转化为一个新的数组并将所有元素拷贝进去
`(*)` 行的 `Array.from` 方法接受对象,检测它是一个可迭代对象或类数组对象,然后创建一个新数组,并将该对象的所有元素复制到这个新数组

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

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

`Array.from` 的完整语法允许提供一个可选的 "mapping"(映射)函数:
```js
Array.from(obj[, mapFn, thisArg])
```

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

例如:

Expand All @@ -235,7 +242,7 @@ let arr = Array.from(range, num => num * num);
alert(arr); // 1,4,9,16,25
```

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

```js run
let str = '𝒳😂';
Expand All @@ -248,24 +255,24 @@ alert(chars[1]); // 😂
alert(chars.length); // 2
```

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

技术上来说,它和下文做了同样的事:

```js run
let str = '𝒳😂';

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

alert(chars);
```

...但是精简很多
……但 `Array.from` 精简很多

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

```js run
function slice(str, start, end) {
Expand All @@ -276,25 +283,25 @@ let str = '𝒳😂𩷶';

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

// 原生方法不支持识别 UTF-16 扩展字符
// 原生方法不支持识别代理对(译注:UTF-16 扩展字符
alert( str.slice(1, 3) ); // 乱码(两个不同 UTF-16 扩展字符碎片拼接的结果)
```


## 总结

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

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


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

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

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