mirror of
https://github.com/qianguyihao/Web.git
synced 2024-11-01 13:34:46 +08:00
464 lines
14 KiB
Markdown
464 lines
14 KiB
Markdown
## 迭代器
|
||
|
||
### 概念
|
||
|
||
**迭代器**(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知识,就能融会贯通了。
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|