Web/07-JavaScript进阶/03-迭代器和生成器.md
2023-05-27 17:36:47 +08:00

464 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

## 迭代器
### 概念
**迭代器**Iterator是 JavaScript 中一种特殊的对象,它提供了一种**统一的、通用的**方式**遍历**个各种不同类型的数据结构。可以遍历的数据结构包括数组、字符串、Set、Map 等**可迭代对象**。我们也可以自定义实现迭代器,以支持遍历自定义的数据结构。
通过迭代器我们可以按顺序逐个获取数据中的元素不需要手动跟踪索引索引也可称之为指针、游标。迭代器的行为很像数据库中的游标cursor
我们也不需要关心可迭代对象内部的实现细节,即不需要关心目标对象是数组还是字符串,还是其他的数据结构。对于迭代器来说,这些数据结构都是一样的处理方式。
迭代器是一种常见的编程模式最早出现在1974年设计的CLU编程语言中。不仅仅在JS中其他许多编程语言比如 Java、Python 等)都提供了迭代器的概念和实现。技术实现各有不同,但目的都是帮助我们用通用的方式遍历对象的数据结构,提高代码的简洁性、可读性和维护性。
### 迭代协议
迭代协议并不是编程语言的内置实现或语法,而是协议。迭代协议具体分为两个协议:可迭代协议、迭代器协议。
**迭代器协议**规定了产生一系列值(无论是有限个还是无限个)的标准方式。
迭代器是一个具体的对象,这个对象要符合迭代器协议。**在JS中某个对象只有实现了符合特定要求的 next() 方法,这个对象才能成为迭代器**。
### 实现原理next() 方法
在JS中迭代器的实现原理是通过定义一个特定的`next()` 方法该方法在每次迭代中返回一个包含两个属性的对象done 和 value。
具体来说next() 方法有如下要求:
1参数无参数或者有一个参数。
2返回值返回一个应当有以下两个属性的对象。属性值如下
- done 属性Boolean 类型表示迭代是否已经完成。当迭代器遍历完所有元素时done 为 true否则为 false。具体解释如下
- 如果迭代器可以产生序列中的下一个值,则为 false这等价于没有指定 done 属性。
- 如果迭代器已将序列迭代完毕,则为 true。这种情况下value 可以省略,如果 value 依然存在,即为迭代结束之后的默认返回值。
- value 属性:包含当前迭代步骤的值,可能是具体的值,也可能是 undefined。每次调用 next() 方法时迭代器返回下一个值。done 为true时可以省略。
### 举例:为数组创建迭代器
按照上面讲的迭代器协议,我们可以给一个数组手动创建一个用于遍历的迭代器。代码举例如下:
```js
const strArr = ['qian', 'gu', 'yi', 'hao'];
// 为数组封装迭代器
function createArrayIterator(arr) {
let index = 0;
return {
next: () => {
if (index < arr.length) {
return { done: false, value: arr[index++] };
} else {
return { done: true };
}
},
};
}
const strArrIterator = createArrayIterator(strArr);
console.log(JSON.stringify(strArrIterator.next()));
console.log(JSON.stringify(strArrIterator.next()));
console.log(JSON.stringify(strArrIterator.next()));
console.log(JSON.stringify(strArrIterator.next()));
console.log(JSON.stringify(strArrIterator.next()));
```
打印结果:
```
{"done":false,"value":"qian"}
{"done":false,"value":"gu"}
{"done":false,"value":"yi"}
{"done":false,"value":"hao"}
{"done":true}
```
你可能会有疑问:实际开发中,我们真的需要大费周章地为一个简单的数组写一个迭代器函数吗?数组直接拿来遍历不就完事了吗?
是的,这大可不必。初衷是为了了解迭代器的原理。
## 可迭代对象
我们要注意区分一些概念:迭代器、可迭代对象、容器。迭代器是提供迭代功能的对象。可迭代对象是被迭代的目标对象,也称之为容器。
### 概念
当一个对象实现了 **iterable protocol 协议**时,它就是一个可迭代对象。这个对象要求必须实现了 `@@iterator` 方法,在内部封装了迭代器。我们可以通过 `Symbol.iterator` 函数调用该迭代器。
当我们使用迭代器的方式去遍历数组、字符串、Set、Map 等数据结构时,这些数据对象就属于可迭代对象。这些数据对象本身,内部就自带了迭代器。
可是,有些数据对象,并不具备可迭代的能力,那要怎么封装成可迭代对象呢?以及,可迭代对象需要具备什么特征?可迭代对象有什么用处?这就是本段要讲的内容。
### 可迭代对象的特征
上面的代码中,我们将 myObj2 数据对象封装成了可迭代对象。凡是可迭代对象,都具备如下特征:
1、可迭代对象都有一个 [Symbol.iterator] 函数。通过这个函数,我们可以进行一些数据遍历操作。以一个简单的数组进行举例:
```js
const myArr = ['qian', 'gu', 'yi', 'hao'];
console.log(typeof myArr[Symbol.iterator]);
console.log(myArr[Symbol.iterator]);
console.log(typeof myArr[Symbol.iterator]());
console.log(myArr[Symbol.iterator]());
// 获取数组自带的迭代器对象
const myIterator = myArr[Symbol.iterator]();
// 通过迭代器的 next() 方法遍历数组
console.log(myIterator.next());
console.log(myIterator.next());
console.log(myIterator.next());
console.log(myIterator.next());
console.log(myIterator.next());
```
打印结果:
<img src="https://img.smyhvae.com/image-20230525211636012.png" alt="image-20230525211636012" style="zoom:50%;" />
2、可迭对象可以进行 for ... of 操作。其实 for ... of 底层就是调用了 `@@iterator` 方法。代码举例:
```js
const myArr = ['qian', 'gu', 'yi', 'hao'];
// 可迭代对象可以进行 for ... of 操作。for ... of 也是一种遍历操作。
for (const item of myArr) {
// 这里的 item其实就是迭代器里的 value 属性的值。
console.log(item);
}
```
打印结果:
```
qian
gu
yi
hao
```
### 为普通对象创建迭代器
现在有下面这两个对象,我们想为其创建迭代器
代码举例:(这段代码和上一段代码比较类似)
```js
const myObj1 = {
strArr: ['qian', 'gu', 'yi', 'hao'],
};
// 为 myObj.strArr 封装迭代器
let index = 0;
const strArrIterator = {
next: () => {
if (index < myObj1.strArr.length) {
return { done: false, value: myObj1.strArr[index++] };
} else {
return { done: true };
}
},
};
console.log(strArrIterator.next());
console.log(strArrIterator.next());
console.log(strArrIterator.next());
console.log(strArrIterator.next());
console.log(strArrIterator.next());
```
打印结果:
```
{done: false, value: 'qian'}
{done: false, value: 'gu'}
{done: false, value: 'yi'}
{done: false, value: 'hao'}
{done: true}
```
### 将普通对象封装为可迭代对象
上面的数据 myObj1不属于可迭代对象因此我们单独写了一个迭代器 strArrIterator 对象,这两个对象是分开的。
还有一种更高级的做法是,把迭代器封装到数据对象的内部。完事之后,这个数据对象就是妥妥的可迭代对象。
将普通的数据对象封装为可迭代对象时,**具体做法**是:在数据对象内部,创建一个名为`[Symbol.iterator]`的迭代器函数,这个函数名是固定的(这种写法属于计算属性名);然后这个函数内需要返回一个迭代器,用于迭代当前的数据对象。
我们以下面这两个对象为例:
```js
const myObj1 = {
strArr: ['qian', 'gu', 'yi', 'hao'],
};
const myObj2 = {
name: 'qianguyihao',
skill: 'web',
};
```
如果尝试用 for of 去遍历它们,会报错:
```js
const myObj2 = {
name: 'qianguyihao',
skill: 'web',
};
for (const item of myObj2) {
// 打印报错Uncaught TypeError: myObj2 is not iterable。意思是myObj2 不是可迭代对象
console.log(item);
}
```
所以,我们可以将这两个普通对象封装为可迭代对象。
1、将 myObj1 封装为可迭代对象,遍历 myObj1.strArr。代码举例如下
```js
const myObj1 = {
strArr: ['qian', 'gu', 'yi', 'hao'],
// 在 myObj1 的内部创建一个迭代器
[Symbol.iterator]: function () {
let index = 0;
const strArrIterator = {
next: function () {
if (index < myObj1.strArr.length) {
return { done: false, value: myObj1.strArr[index++] };
} else {
return { done: true };
}
},
};
return strArrIterator;
},
};
// 获取 myObj1 的迭代器对象
const strArrIterator = myObj2[Symbol.iterator]();
// 通过迭代器遍历 myObj1.strArr 的数据
console.log(strArrIterator.next());
console.log(strArrIterator.next());
console.log(strArrIterator.next());
console.log(strArrIterator.next());
console.log(strArrIterator.next());
```
打印结果:
```
{done: false, value: 'qian'}
{done: false, value: 'gu'}
{done: false, value: 'yi'}
{done: false, value: 'hao'}
{done: true}
```
上方代码有一个改进之处,如果把迭代器函数改成箭头函数,就可以通过 `this.strArr` 代表 `myObj2.strArr` 了。代码改进如下:
```js
const myObj1 = {
strArr: ['qian', 'gu', 'yi', 'hao'],
// 在 myObj1 的内部创建一个迭代器
[Symbol.iterator]: function () {
let index = 0;
const strArrIterator = {
next: () => {
if (index < this.strArr.length) {
return { done: false, value: this.strArr[index++] };
} else {
return { done: true };
}
},
};
return strArrIterator;
},
};
// 获取 myObj1 的迭代器对象
const strArrIterator = myObj2[Symbol.iterator]();
// 通过迭代器遍历 myObj1.strArr 的数据
console.log(strArrIterator.next());
console.log(strArrIterator.next());
console.log(strArrIterator.next());
console.log(strArrIterator.next());
console.log(strArrIterator.next());
```
打印结果不变。
2、将 myObj2 封装为可迭代对象,遍历里面的键值对。代码举例如下:
```js
const myObj2 = {
name: 'qianguyihao',
skill: 'web',
// 将普通对象 myObj2 封装为可迭代对象,目的是遍历 myObj2 的键值对
[Symbol.iterator]: function () {
// const keys = Object.keys(this); // 获取对象的 key
// const values = Object.values(this); // 获取对象的 value
const entries = Object.entries(this); // 获取对象的键值对
let index = 0;
const iterator = {
next: function () {
if (index < entries.length) {
return { done: false, value: entries[index++] };
} else {
return { done: true };
}
},
};
return iterator;
},
};
// 可迭对象可以进行for of操作遍历对象的键值对
for (const item of myObj2) {
const [key, value] = item;
console.log(key, value);
}
```
打印结果:
```
name qianguyihao
skill web
```
### 原生可迭代对象
以下这些对象,都是原生可迭代对象,请务必记住:
- String 字符串
- Array 数组
- Map
- Set
- arguments 对象
- NodeList 对象DOM节点的集合
原生可迭代对象的内部已经实现了可迭代协议,它们都符合可迭代对象的特征。比如,它们内部都有一个迭代器;他们可以用 for ... of 进行遍历。
为何要记住上面这些可迭代对象,因为可迭代对象的应用场景非常多,且非常好用。我们继续往下学习。
## 可迭代对象的应用场景
可迭代对象有许多应用场景,包括但不仅限于:
1、JavaScript的语法
- for ... of
- 展开语法 ...
- yield*
- 解构赋值
2、创建一些对象
- new Map([Iterable]):参数是可选的,可不传参数,也可以传一个可迭代对象作为参数
- new WeakMap([iterable])
- new Set([iterable])
- new WeakSet([iterable])
3、方法调用
- Array.from(iterable):将一个可迭代对象转为数组
- Promise.all(iterable)
- Promise.race(iterable)
今后在遇到这些应用场景时,这些原生可迭代对象可以直接拿来用。
比如说,通过阅读官方文档后我们得知,`new Set()`的写法中,括号里的参数可以不写,也可以传入一个可迭代对象 `iterable`。那么字符串、数组、Set、Map等可迭代对象在你需要的时候都可以传进去使用。而且`const a = new Set()`写法中,变量 a 也是一个可迭代对象。
`Promise.all(iterable)` 只能传数组吗非也。准确来说Promise.all()的参数中,传入的不是数组,而是一个可迭代对象。代码举例:
```js
const promise1 = Promise.resolve('promise1 resolve');
const promise2 = Promise.resolve('promise2 resolve');
const promise3 = Promise.resolve('promise3 resolve');
const promiseSet = new Set();
promiseSet.add(promise1);
promiseSet.add(promise2);
promiseSet.add(promise3);
// 准确来说Promise.all()的参数中,传入的不是数组,而是一个可迭代对象
Promise.all(promiseSet).then(res => {
console.log('res:', res);
});
```
代码举例:
```
res: ['promise1 resolve', 'promise2 resolve', 'promise3 resolve']
```
arguments 同样是一个可迭代对象,但不是数组。我们可以通过`Array.from(iterable)`方法将 arguments 转为数组,进而让其享受数组的待遇,调用数组的各种方法。代码举例:
```js
foo('a', 'b', 'c');
// 定义函数
function foo() {
// Array.from() 中的参数可以传入可迭代对象将参数转为数组。arguments 是 foo() 函数的参数
const arr = Array.from(arguments);
console.log(arr);
}
```
打印结果:
```
['a', 'b', 'c']
```
学完了迭代器、可迭代对象的知识之后很多关于函数传参、数据遍历、数据结构等方面的JS知识就能融会贯通了。