2023-05-25 21:23:39 +08:00
## 迭代器
2023-05-24 17:38:38 +08:00
### 概念
**迭代器**( Iterator) 是 JavaScript 中一种特殊的对象,它提供了一种**统一的、通用的**方式**遍历**个各种不同类型的数据结构。可以遍历的数据结构包括: 数组、字符串、Set、Map 等**可迭代对象**。我们也可以自定义实现迭代器,以支持遍历自定义的数据结构。
2023-05-25 21:23:39 +08:00
通过迭代器, 我们可以按顺序逐个获取数据中的元素, 不需要手动跟踪索引( 索引也可称之为指针、游标) 。迭代器的行为很像数据库中的游标( cursor) 。
2023-05-24 17:38:38 +08:00
2023-05-25 21:23:39 +08:00
我们也不需要关心可迭代对象内部的实现细节,即不需要关心目标对象是数组还是字符串,还是其他的数据结构。对于迭代器来说,这些数据结构都是一样的处理方式。
2023-05-24 17:38:38 +08:00
2023-05-25 21:23:39 +08:00
迭代器是一种常见的编程模式, 最早出现在1974年设计的CLU编程语言中。不仅仅在JS中, 其他许多编程语言( 比如 Java、Python 等)都提供了迭代器的概念和实现。技术实现各有不同,但目的都是帮助我们用通用的方式遍历对象的数据结构,提高代码的简洁性、可读性和维护性。
2023-05-24 17:38:38 +08:00
### 迭代协议
2023-05-25 21:23:39 +08:00
迭代协议并不是编程语言的内置实现或语法,而是协议。迭代协议具体分为两个协议:可迭代协议、迭代器协议。
2023-05-24 17:38:38 +08:00
**迭代器协议**规定了产生一系列值(无论是有限个还是无限个)的标准方式。
迭代器是一个具体的对象,这个对象要符合迭代器协议。**在JS中, 某个对象只有实现了符合特定要求的 next() 方法,这个对象才能成为迭代器**。
### 实现原理: next() 方法
2023-05-25 21:23:39 +08:00
在JS中, 迭代器的实现原理是通过定义一个特定的`next()` 方法, 该方法在每次迭代中返回一个包含两个属性的对象: done 和 value。
2023-05-24 17:38:38 +08:00
2023-05-25 21:23:39 +08:00
具体来说, next() 方法有如下要求:
2023-05-24 17:38:38 +08:00
( 1) 参数: 无参数或者有一个参数。
2023-05-25 21:23:39 +08:00
( 2) 返回值: 返回一个应当有以下两个属性的对象。属性值如下:
2023-05-24 17:38:38 +08:00
2023-05-25 21:23:39 +08:00
- done 属性( Boolean 类型) : 表示迭代是否已经完成。当迭代器遍历完所有元素时, done 为 true, 否则为 false。具体解释如下:
2023-05-24 17:38:38 +08:00
- 如果迭代器可以产生序列中的下一个值,则为 false, 这等价于没有指定 done 属性。
- 如果迭代器已将序列迭代完毕,则为 true。这种情况下, value 可以省略,如果 value 依然存在,即为迭代结束之后的默认返回值。
2023-05-25 21:23:39 +08:00
- 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}
```
你可能会有疑问:实际开发中,我们真的需要大费周章地为一个简单的数组写一个迭代器函数吗?数组直接拿来遍历不就完事了吗?
是的,这大可不必。初衷是为了了解迭代器的原理。
## 可迭代对象
2023-05-27 17:36:47 +08:00
我们要注意区分一些概念:迭代器、可迭代对象、容器。迭代器是提供迭代功能的对象。可迭代对象是被迭代的目标对象,也称之为容器。
### 概念
当一个对象实现了 **iterable protocol 协议**时,它就是一个可迭代对象。这个对象要求必须实现了 `@@iterator` 方法,在内部封装了迭代器。我们可以通过 `Symbol.iterator` 函数调用该迭代器。
2023-05-25 21:23:39 +08:00
当我们使用迭代器的方式去遍历数组、字符串、Set、Map 等数据结构时,这些数据对象就属于可迭代对象。这些数据对象本身,内部就自带了迭代器。
可是,有些数据对象,并不具备可迭代的能力,那要怎么封装成可迭代对象呢?以及,可迭代对象需要具备什么特征?可迭代对象有什么用处?这就是本段要讲的内容。
2023-05-26 07:47:42 +08:00
### 可迭代对象的特征
2023-05-27 18:05:38 +08:00
凡是可迭代对象,都具备如下特征:
2023-05-26 07:47:42 +08:00
2023-05-27 18:05:38 +08:00
1、可迭代对象都有一个 [Symbol.iterator] 函数。通过这个函数,我们可以进行数据遍历操作。以一个简单的数组进行举例:
2023-05-26 07:47:42 +08:00
```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%;" / >
2023-05-27 17:36:47 +08:00
2、可迭对象可以进行 for ... of 操作。其实 for ... of 底层就是调用了 `@@iterator` 方法。代码举例:
2023-05-26 07:47:42 +08:00
```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
```
2023-05-27 18:05:38 +08:00
### 原生可迭代对象
以下这些对象,都是原生可迭代对象,请务必记住:
- 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知识, 就能融会贯通了。
## 手写迭代器
2023-05-25 21:23:39 +08:00
2023-05-27 18:05:38 +08:00
很多数据对象由于不是可迭代对象,我们可以为其手动创建一个迭代器,这个数据对象就成了可迭代对象。
2023-05-27 22:58:15 +08:00
### 为普通对象创建外部迭代器
2023-05-26 07:47:42 +08:00
2023-05-27 22:58:15 +08:00
代码举例:
2023-05-25 21:23:39 +08:00
```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}
```
### 将普通对象封装为可迭代对象
2023-05-27 18:05:38 +08:00
上面的数据 myObj1, 不属于可迭代对象, 因此我们单独写了一个迭代器对象 strArrIterator。但是这两个对象是分开的。
2023-05-25 21:23:39 +08:00
还有一种更高级的做法是,把迭代器封装到数据对象的内部。完事之后,这个数据对象就是妥妥的可迭代对象。
将普通的数据对象封装为可迭代对象时,**具体做法**是:在数据对象内部,创建一个名为`[Symbol.iterator]`的迭代器函数,这个函数名是固定的(这种写法属于计算属性名);然后这个函数内需要返回一个迭代器,用于迭代当前的数据对象。
2023-05-26 07:47:42 +08:00
我们以下面这两个对象为例:
```js
const myObj1 = {
strArr: ['qian', 'gu', 'yi', 'hao'],
};
const myObj2 = {
name: 'qianguyihao',
skill: 'web',
};
```
如果尝试用 for of 去遍历它们,会报错:
2023-05-25 21:23:39 +08:00
```js
const myObj2 = {
2023-05-26 07:47:42 +08:00
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 = {
2023-05-25 21:23:39 +08:00
strArr: ['qian', 'gu', 'yi', 'hao'],
2023-05-26 07:47:42 +08:00
// 在 myObj1 的内部创建一个迭代器
2023-05-25 21:23:39 +08:00
[Symbol.iterator]: function () {
let index = 0;
const strArrIterator = {
next: function () {
2023-05-26 07:47:42 +08:00
if (index < myObj1.strArr.length ) {
return { done: false, value: myObj1.strArr[index++] };
2023-05-25 21:23:39 +08:00
} else {
return { done: true };
}
},
};
return strArrIterator;
},
};
2023-05-26 07:47:42 +08:00
// 获取 myObj1 的迭代器对象
2023-05-25 21:23:39 +08:00
const strArrIterator = myObj2[Symbol.iterator]();
2023-05-26 07:47:42 +08:00
// 通过迭代器遍历 myObj1.strArr 的数据
2023-05-25 21:23:39 +08:00
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}
```
2023-05-27 22:58:15 +08:00
上方代码有一个改进之处,如果把迭代器函数改成箭头函数,就可以通过 `this.strArr` 代表 `myObj2.strArr` 了,写法更简洁。代码改进如下:
2023-05-25 21:23:39 +08:00
2023-05-25 21:37:02 +08:00
```js
2023-05-26 07:47:42 +08:00
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;
},
};
2023-05-25 21:23:39 +08:00
2023-05-26 07:47:42 +08:00
// 获取 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());
2023-05-25 21:23:39 +08:00
```
2023-05-26 07:47:42 +08:00
打印结果不变。
2023-05-25 21:23:39 +08:00
2023-05-26 07:47:42 +08:00
2、将 myObj2 封装为可迭代对象,遍历里面的键值对。代码举例如下:
2023-05-25 21:23:39 +08:00
```js
2023-05-26 07:47:42 +08:00
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 = {
2023-05-27 22:58:15 +08:00
next: () => {
2023-05-26 07:47:42 +08:00
if (index < entries.length ) {
return { done: false, value: entries[index++] };
} else {
return { done: true };
}
},
};
return iterator;
},
};
2023-05-25 21:23:39 +08:00
2023-05-26 07:47:42 +08:00
// 可迭对象可以进行for of操作, 遍历对象的键值对
for (const item of myObj2) {
const [key, value] = item;
console.log(key, value);
2023-05-25 21:23:39 +08:00
}
```
打印结果:
```
2023-05-26 07:47:42 +08:00
name qianguyihao
skill web
2023-05-25 21:23:39 +08:00
```
2023-05-27 18:05:38 +08:00
### 将自定义类封装为可迭代对象
2023-05-27 17:36:47 +08:00
2023-05-27 22:58:15 +08:00
在面向对象开发时,如果你希望自己创建的类也具备可迭代的能力,那么,你可以在定义类的时候手动添加一个 `@@iterator` 方法,让其成为可迭代对象。
2023-05-27 17:36:47 +08:00
代码举例:
2023-05-27 18:05:38 +08:00
```json
// 定义类
class Person {
constructor(name, arr) {
this.name = name;
this.arr = arr;
}
2023-05-27 17:36:47 +08:00
2023-05-27 18:05:38 +08:00
// 定义一个名为 [Symbol.iterator] 的实例方法,封装迭代器
[Symbol.iterator]() {
let index = 0;
const iterator = {
next: () => {
if (index < this.arr.length ) {
return { done: false, value: this.arr[index++] };
} else {
return { done: true };
}
},
};
return iterator;
}
}
2023-05-27 17:36:47 +08:00
2023-05-27 18:05:38 +08:00
const person1 = new Person('千古壹号', ['前端', '工程师']);
const person2 = new Person('许嵩', ['想象之中', '有何不可']);
2023-05-27 17:36:47 +08:00
2023-05-30 08:26:12 +08:00
// Person的实例已经封装为可迭代对象了, 可以通过 for ... of 进行遍历
2023-05-27 18:05:38 +08:00
for (const item of person2) {
console.log(item);
2023-05-27 17:36:47 +08:00
}
```
打印结果:
```
2023-05-27 18:05:38 +08:00
想象之中
有何不可
2023-05-27 17:36:47 +08:00
```
2023-05-27 22:58:15 +08:00
### 如何中断迭代器,停止继续遍历
迭代器在遍历数据对象的过程中,如果我们希望在符合指定条件下停止继续遍历,那么,我们可以使用 break、return、throw 等关键字中断迭代器。其中, break 关键字用得最多。
此外,我们还可在迭代器函数中添加一个名为`return()`的方法,这个方法的作用是监听迭代器的中断,书写代码的位置与 `next()` 方法并列。
代码举例如下:
```js
const myObj2 = {
name: 'qianguyihao',
skill: 'web',
// 将普通对象 myObj2 封装为可迭代对象,目的是遍历 myObj2 的键值对
[Symbol.iterator]: function () {
const entries = Object.entries(this); // 获取对象的键值对
let index = 0;
const iterator = {
next: () => {
if (index < entries.length ) {
return { done: false, value: entries[index++] };
} else {
return { done: true };
}
},
2023-05-28 20:30:46 +08:00
// 【关键代码】监听迭代器的中断
2023-05-27 22:58:15 +08:00
return: () => {
console.log('迭代器被中断了');
return { done: true };
},
};
return iterator;
},
};
// 可迭对象可以进行 for of 操作,遍历对象的键值对
for (const item of myObj2) {
const [key, value] = item;
console.log(key, value);
if (value == 'qianguyihao') {
2023-05-28 20:30:46 +08:00
// 【关键代码】如果发现 value 为 qianguyihao, 则中断迭代器, 停止继续遍历
2023-05-27 22:58:15 +08:00
break;
}
}
```
打印结果:
```
name qianguyihao
迭代器被中断了
```
2023-05-27 17:36:47 +08:00
2023-05-27 22:58:15 +08:00
根据打印结果可以看出,迭代器只遍历了 myObj2 对象的第一个元素,符合指定条件后,通过 break 语句中断了迭代器,停止了继续遍历;与此同时,迭代器中的 return() 函数监听到了迭代器的中断。对了, return() 函数中,还需要写 `return { done: true }` 表示迭代器的使命已结束;如果不写这行则会报错:`Uncaught TypeError: Iterator result undefined is not an object`。
2023-05-27 17:36:47 +08:00
2023-05-28 20:30:46 +08:00
## 生成器
### 概念
我们平时写的函数,基本是通过 return 返回值,或者发生异常,函数才会终止执行。这还不够灵活。
2023-06-24 20:53:27 +08:00
**生成器**是 ES6 中新增的一种特殊的函数,所以也称为“生成器函数”。它可以更灵活地控制函数什么时候执行, 什么时候暂停等等,**控制精度很高**。
2023-05-28 20:30:46 +08:00
生成器函数使用 `function*` 语法编写。最初调用时,生成器函数不执行任何代码,而是返回一个称为 Generator 的迭代器。通过调用生成器的 next() 方法时, Generator 函数将执行,直到遇到 yield 关键字时暂停执行。
可以根据需要多次调用该函数,并且每次都返回一个新的 Generator, 但每个 Generator 只能迭代一次。
### 生成器函数和普通函数的区别
- 生成器函数需要在 `function` 关键字后面加一个符号 `*` 。
- 生成器函数可以通过 `yield` 关键字控制函数的执行流程。
- 生成器函数的返回值是一个生成器( Generator) 。生成器是一种特殊的迭代器。
## 生成器函数拆解
2023-06-24 20:53:27 +08:00
### 定义一个生成器函数
2023-05-28 20:30:46 +08:00
2023-06-24 20:53:27 +08:00
如果要定义一个生成器函数,我们需要在`function`单词和函数名之间加一个`*`符号。
`*` 符号有下面四种写法,最推荐的是第一种写法:
```js
function* generator1() { /*code*/ } // 推荐写法
function *generator2() { /*code*/ }
function * generator3() { /*code*/ }
function*generator4() { /*code*/ }
```
截图如下:
![image-20230624184902630 ](https://img.smyhvae.com/image-20230624184902630.png )
代码举例:
2023-05-28 20:30:46 +08:00
```js
function* foo() {
console.log('1');
console.log('2');
console.log('3');
}
foo();
```
2023-06-24 20:53:27 +08:00
### yield 表达式
2023-05-28 20:30:46 +08:00
2023-06-24 20:53:27 +08:00
但是上面的代码写完后,并不会有打印结果,因为**我们还需要调用生成器的 next()方法,生成器函数才会执行,直到遇到 yield 关键字后暂停执行**;反反复复,**直到遇到 return关键字, 或者遇到函数末尾时, 结束执行**。代码举例:
2023-05-28 20:30:46 +08:00
```js
// 通过 * 符号,定义一个生成器函数
function* foo() {
console.log('1');
yield;
2023-05-31 20:39:42 +08:00
2023-05-28 20:30:46 +08:00
console.log('2');
// 下面这行 console.log('a') 会跟 yield 一起执行
yield console.log('a');
2023-05-31 20:39:42 +08:00
2023-05-28 20:30:46 +08:00
console.log('3');
}
const generator = foo(); // 返回一个生成器对象
// 调用生成器的 next()方法,生成器才会执行,直到遇到 yield 后暂停执行
generator.next(); // 这行代码执行后, 打印结果是: 1
generator.next(); // 这行代码执行后, 打印结果是: 1 2 a
generator.next(); // 这行代码执行后, 打印结果是: 1 2 a 3
```
仔细看注释,生成器 generator 每调用一次 next() , foo()函数里的代码就往下执行一次,直到遇到 yield 后暂停。
### next() 方法的返回值
生成器既然是一种特殊的迭代器,那么它也有 next()方法,而且 next()方法里同样有 done 和 value 这两个属性。我们来看看这两个属性的**默认属性值**是什么:
```js
// 通过 * 符号,定义一个生成器函数
function* foo() {
console.log('阶段1');
yield;
2023-05-31 20:39:42 +08:00
2023-05-28 20:30:46 +08:00
console.log('阶段2');
yield;
2023-05-31 20:39:42 +08:00
2023-05-28 20:30:46 +08:00
console.log('阶段3');
return; // 执行 return 之后,函数不再继续往下走了,生成器的 next()方法的 done 属性值为 true。
2023-05-31 20:39:42 +08:00
2023-05-28 20:30:46 +08:00
console.log('阶段4');
}
// 执行生成器函数,返回一个生成器对象
const generator = foo();
// 调用生成器的 next()方法,生成器才会执行,直到遇到 yield 后暂停执行;遇到 return关键字, 或者遇到函数末尾时, 结束执行。
console.log(generator.next());
console.log(generator.next());
console.log(generator.next());
```
打印结果:
```
阶段1
{value: undefined, done: false}
阶段2
{value: undefined, done: false}
阶段3
{value: undefined, done: true}
```
上方代码的打印结果可以看出,生成器函数在遇到函数的末尾,或者遇到 return 之后, 函数就不再继续往下走了, next()方法的 done 属性值为 true。
还可以看到, next()方法的 value 属性值默认为 undefined, **如果某些情况下我们希望 value属性有值**的话,可以通过 yield 关键字进行传递。代码举例:
```js
// 通过 * 符号,定义一个生成器函数
function* foo() {
console.log('阶段1');
yield 'a'; // 【关键代码】yield 后面写的内容,就是传递给 next() 方法的 value 属性值
2023-05-31 20:39:42 +08:00
2023-05-28 20:30:46 +08:00
console.log('阶段2');
yield 'b';
2023-05-31 20:39:42 +08:00
2023-05-28 20:30:46 +08:00
console.log('阶段3');
return; // 这里的 return, 相当于 return undefined
2023-05-31 20:39:42 +08:00
2023-05-28 20:30:46 +08:00
console.log('阶段4');
}
// 执行生成器函数,返回一个生成器对象
const generator = foo();
// 调用生成器的 next()方法,生成器才会执行,直到遇到 yield 后暂停执行;遇到 return关键字, 或者遇到函数末尾时, 结束执行。
console.log(generator.next()); // 打印生成器对象的 next()方法
console.log(generator.next());
console.log(generator.next());
```
打印结果:
```
阶段1
{value: 'a', done: false}
阶段2
{value: 'b', done: false}
阶段3
{value: undefined, done: true}
```
### next() 方法的参数
根据前面的代码实例得知,生成器函数是分多个阶段执行的。此时有一个诉求:如何给当前阶段传递参数呢?
答案是:可以通过当前阶段 next() 方法的参数,给当前阶段传值。这个参数值会成为**上一个阶段** yield 语句的返回值。
代码举例:
```js
// 通过 * 符号,定义一个生成器函数
function* foo() {
console.log('阶段1');
// 【关键代码】第二次调用 next()方法时,通过 res2 接收 next()方法的参数值
const res2 = yield 'a';
2023-05-31 20:39:42 +08:00
2023-05-28 20:30:46 +08:00
console.log('阶段2:', res2);
// 第三次调用 next()方法时,通过 res3 接收 next()方法的参数值
const res3 = yield 'b';
2023-05-31 20:39:42 +08:00
2023-05-28 20:30:46 +08:00
console.log('阶段3:', res3);
return;
}
// 执行生成器函数,返回一个生成器对象
const generator = foo();
// 调用生成器的 next()方法,生成器才会执行,直到遇到 yield 后暂停执行;遇到 return关键字, 或者遇到函数末尾时, 结束执行。
console.log(generator.next()); // 执行第一阶段
console.log(generator.next('next2')); // 执行第二阶段,并传参
console.log(generator.next('next3')); // 指定第三阶段
```
打印结果:
```
阶段1
{value: 'a', done: false}
阶段2: next2
{value: 'b', done: false}
阶段3: ntext3
{value: undefined, done: true}
```
2023-06-06 21:14:11 +08:00
第一次学习时,这段代码可能比较难理解。在理解时需要注意的是,将 next2 这个属性值赋值给 res2, 这个操作的执行时机是在**第二阶段的最开始**做的,不是在第一阶段的末尾做的。并且,这个属性值是通过第一阶段的 yield 返回值接收的。
2023-05-28 20:30:46 +08:00
2023-05-30 08:26:12 +08:00
### 如何中途结束生成器的执行
如果想在中途结束生成器的执行,有三种方式:
- 方式1: return 语句。这个在前面已经讲过。
- 方式2: 通过生成器的 return() 函数。
- 方式3: 通过生成器的 throw() 函数抛出异常。
方式2的代码举例:
```js
// 通过 * 符号,定义一个生成器函数
function* foo() {
console.log('阶段1');
const res2 = yield 'a';
console.log('阶段2:', res2);
const res3 = yield 'b';
console.log('阶段3:', res3);
return;
}
// 执行生成器函数,返回一个生成器对象
const generator = foo();
console.log(generator.next());
// 【关键代码】通过生成器的 return()方法, 立即结束 foo 函数的执行
console.log(generator.return('next2'));
// 这行写了也没用, 阶段2、阶段3都不会执行的
console.log(generator.next('next3'));
```
打印结果:
```
阶段1
{value: 'a', done: false}
{value: 'next2', done: true}
{value: undefined, done: true}
```
上方代码可以看出, 阶段2、阶段3都不会执行; return()方法里的参数传给了 value 属性。
方式3的代码举例:
```js
// 通过 * 符号,定义一个生成器函数
function* foo() {
console.log('阶段1');
const res2 = yield 'a';
console.log('阶段2:', res2);
const res3 = yield 'b';
console.log('阶段3:', res3);
return;
}
// 执行生成器函数,返回一个生成器对象
const generator = foo();
console.log(generator.next());
// 【关键代码】通过生成器的 throw()方法抛出异常, 立即结束 foo 函数的执行
console.log(generator.throw(new Error('next2 error')));
// 这行写了也没用, 阶段2、阶段3都不会执行的
console.log(generator.next('next3'));
```
打印结果:
```
阶段1
{value: 'a', done: false}
Uncaught Error: next2 error
```
## 生成器的应用
### 用生成器代替迭代器
在前面的迭代器内容中,我们学习过“将普通对象封装为可迭代对象”。那段代码改用生成器的写法也可以实现。代码举例:
```js
const myObj2 = {
name: 'qianguyihao',
skill: 'web',
// 将普通对象 myObj2 封装为可迭代对象,目的是遍历 myObj2 的键值对。通过生成器 function* 的的方式实现
[Symbol.iterator]: function* () {
const entries = Object.entries(this); // 获取对象的键值对
for (let index = 0; index < entries.length ; index + + ) {
// 【关键代码】通过 yield 控制迭代器分阶段执行;并将每个阶段的值存放到迭代器的 next() 方法的 value 属性中
yield entries[index];
}
},
};
// 写法1: 通过 for ... of 遍历可迭代对象
for (const item of myObj2) {
const [key, value] = item;
console.log(key, value);
}
console.log('---');
// 写法2: 通过 next() 方法遍历可迭代对象。与写法1等价。
const iterator = myObj2[Symbol.iterator]();
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());
```
打印结果:
```
name qianguyihao
demo.html:30 skill web
---
done: false, value:['name', 'qianguyihao']
done: false, value:['skill', 'web']
done: true, value:undefined
```
### 在指定数字范围内生成一个值
代码举例:
```js
function* createValueGenerator(start, end) {
// 前闭后开
for (let i = start; i < end ; i + + ) {
yield i;
}
}
const valueGenerator = createValueGenerator(1, 3);
console.log(valueGenerator.next());
console.log(valueGenerator.next());
console.log(valueGenerator.next());
```
打印结果:
```
{value: 1, done: false}
{value: 2, done: false}
{value: undefined, done: true}
```
2023-05-31 20:39:42 +08:00
## yield* 的使用
2023-05-30 08:26:12 +08:00
2023-05-31 20:39:42 +08:00
### 使用 yield* 迭代可迭代对象
2023-05-30 08:26:12 +08:00
语法格式:
```js
yield* 某个可迭代对象
```
2023-06-06 21:14:11 +08:00
`yield*` 是 yield 的一种语法糖,也就是一种简写形式。它会依次迭代一个**可迭对对象**,每次迭代一个值,并且会产生一个**新的可迭代对象**。
2023-05-30 08:26:12 +08:00
2023-05-31 20:39:42 +08:00
我们在前面讲过可迭代对象的应用场景,简单提到过 `yield*` ,接下来讲讲它具体如何使用的。
2023-05-30 08:26:12 +08:00
先来看下面这段代码,用生成器的方式迭代数组:
```js
function* createArrayGenerator(arr) {
for (const item of arr) {
yield item;
}
}
const myArr = ['a', 'b', 'c'];
const arrGenerator = createArrayGenerator(myArr);
console.log(arrGenerator.next());
console.log(arrGenerator.next());
console.log(arrGenerator.next());
console.log(arrGenerator.next());
```
打印结果:
```
{value: 'a', done: false}
{value: 'b', done: false}
{value: 'c', done: false}
{value: undefined, done: true}
```
上面这段代码,换成 `yield*` 的写法会非常简洁,如下:
```js
function* createArrayGenerator(arr) {
// 【关键代码】yield* 的后面必须是一个可迭代对象
yield* arr;
}
const myArr = ['a', 'b', 'c'];
const arrGenerator = createArrayGenerator(myArr);
console.log(arrGenerator.next());
console.log(arrGenerator.next());
console.log(arrGenerator.next());
console.log(arrGenerator.next());
```
2023-05-31 20:39:42 +08:00
打印结果不变。代码解释:`yield*` 的后面必须是一个可迭代对象,而且 createArrayGenerator()函数会返回一个可迭代对象 arrGenerator。
2023-05-30 08:26:12 +08:00
### 将自定义类封装为可迭代对象
我们在前面学习迭代器时,曾通过迭代器“将自定义类封装为可迭代对象”。那段代码,改用生成器 `yield*` 的方式也可以实现。代码举例:
```js
// 定义类
class Person {
constructor(name, arr) {
this.name = name;
this.arr = arr;
}
2023-05-31 20:39:42 +08:00
// 【关键代码】在定义生成器函数时,如果没有 function 这个单词的话,也可以直接在函数名的前面添加 * 符号。
2023-05-30 08:26:12 +08:00
*[Symbol.iterator]() {
// 【关键代码】一行代码,直接遍历 this.arr 这个可迭代对象,同时返回一个新的可迭代对象
yield* this.arr;
}
}
const person1 = new Person('千古壹号', ['前端', '工程师']);
const person2 = new Person('许嵩', ['想象之中', '有何不可']);
// Person的实例已经封装为可迭代对象了, 可以通过 for ... of 进行遍历
for (const item of person1) {
console.log(item);
}
console.log('---');
// 也可以通过 Symbol.iterator() 方法进行遍历
const personIterator = person2[Symbol.iterator]();
console.log(personIterator.next());
console.log(personIterator.next());
console.log(personIterator.next());
```
打印结果:
```
前端
工程师
---
{value: '想象之中', done: false}
{value: '有何不可', done: false}
{value: undefined, done: true}
```
2023-05-31 20:39:42 +08:00
## 赞赏作者
2023-05-30 08:26:12 +08:00
2023-05-31 20:39:42 +08:00
创作不易,你的赞赏和认可,是我更新的最大动力:
2023-05-30 08:26:12 +08:00
2023-05-31 20:39:42 +08:00
![](https://img.smyhvae.com/20220401_1800.jpg)