Javascript 笔记:迭代器和生成器

迭代器是一种数据遍历的算法和协议,也可以用于程序流程的控制,是 Javascript 中比较重要的知识点。

这篇文章简单记录了迭代器的基本概念和使用的场景。

迭代器

迭代器(Iterator)是 JavaScript 中一种特殊的对象,它提供了一种统一的、通用的方式遍历个各种不同类型的数据结构。可以遍历的数据结构包括:数组、字符串、Set、Map 等可迭代对象。我们也可以自定义实现迭代器,以支持遍历自定义的数据结构。

迭代器是一个具体的对象,这个对象要符合迭代器协议。在JS中,某个对象只有实现了符合特定要求的 next() 方法,这个对象才能成为迭代

实现原理

在JS中,迭代器的实现原理是通过定义一个特定的next() 方法,该方法在每次迭代中返回一个包含两个属性的对象:done 和 value。

具体来说,next() 方法有如下要求:

  1. 参数:无参数或者有一个参数。
  2. 返回值:返回一个应当有以下两个属性的对象。属性值如下:
    • done 属性(Boolean 类型):表示迭代是否已经完成。当迭代器遍历完所有元素时,done 为 true,否则为 false。具体解释如下:
      • 如果迭代器可以产生序列中的下一个值,则为 false,这等价于没有指定 done 属性。
      • 如果迭代器已将序列迭代完毕,则为 true。这种情况下,value 可以省略,如果 value 依然存在,即为迭代结束之后的默认返回值。
    • value 属性:包含当前迭代步骤的值,可能是具体的值,也可能是 undefined。每次调用 next() 方法时,迭代器返回下一个值。done 为true时,可以省略。

举例,为数组创建迭代器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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()));

打印结果:

1
2
3
4
5
{"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 等数据结构时,这些数据对象就属于可迭代对象。这些数据对象本身,内部就自带了迭代器。

原生可迭代对象

以下这些对象,都是原生可迭代对象:

  • 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)

将对象封装为可迭代对象

要使一个普通对象可以使用 for ... of 进行遍历,需要实现 Symbol.iterator 方法并返回一个迭代器对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
const myObj1 = {
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 };
}
},
};
return iterator;
},
};

// 可迭对象可以进行for of操作,遍历对象的键值对
for (const item of myObj2) {
const [key, value] = item;
console.log(key, value);
}

中断迭代器

迭代器在遍历数据对象的过程中,如果我们希望在符合指定条件下停止继续遍历,那么,我们可以使用 break、return、throw 等关键字中断迭代器。其中, break 关键字用得最多。

此外,我们还可在迭代器函数中添加一个名为return()的方法,这个方法的作用是监听迭代器的中断,书写代码的位置与 next()方法并列。

代码举例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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 };
}
},
// 【关键代码】监听迭代器的中断
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') {
// 【关键代码】如果发现 value 为 qianguyihao,则中断迭代器,停止继续遍历
break;
}
}

打印结果:

1
2
name qianguyihao
迭代器被中断了

return() 函数中,还需要写 return { done: true } 表示迭代器的使命已结束;如果不写这行则会报错:Uncaught TypeError: Iterator result undefined is not an object

生成器

生成器是 ES6 中新增的一种特殊的函数,所以也称为“生成器函数”。它可以更灵活地控制函数什么时候执行, 什么时候暂停等等,控制精度很高。

生成器函数使用 function* 语法编写。最初调用时,生成器函数不执行任何代码,而是返回一个称为 Generator 的迭代器。通过调用生成器的 next() 方法时,Generator 函数将执行,直到遇到 yield 关键字时暂停执行。

可以根据需要多次调用该函数,并且每次都返回一个新的 Generator,但每个 Generator 只能迭代一次。

生成器函数和普通函数的区别

  • 生成器函数需要在 function 关键字后面加一个符号 *
  • 生成器函数可以通过 yield 关键字控制函数的执行流程。
  • 生成器函数的返回值是一个 生成器(Generator), 生成器是一种特殊的迭代器。

定义生成器函数

如果要定义一个生成器函数,我们需要在 function 单词和函数名之间加一个 * 符号。

* 符号有下面四种写法,最推荐的是第一种写法:

1
2
3
4
function* generator1() { /*code*/ } // 推荐写法
function *generator2() { /*code*/ }
function * generator3() { /*code*/ }
function*generator4() { /*code*/ }

实例:

1
2
3
4
5
6
7
function* foo() {
console.log('1');
console.log('2');
console.log('3');
}

foo();

调用 foo() 函数返回一个迭代器,此时代码不会执行,需要调用 next 方法才会执行。

生成器执行流程

  1. 调用生成器函数返回一个迭代器 iterator1;
  2. 调用迭代器的 next() 方法;
  3. 执行生成器函数中的代码,直到遇到 yield 表达式 和 return 关键字或函数结尾;
  4. yield 表达式会暂停执行代码,而遇到 return 和 函数结尾则结束执行。

代码实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 通过 * 符号,定义一个生成器函数
function* foo() {
console.log('1');
yield;

console.log('2');
yield;

console.log('3');
}

const generator = foo(); // 返回一个生成器对象

// 调用生成器的 next()方法,生成器才会执行,直到遇到 yield 后暂停执行
generator.next(); // 这行代码执行后,打印结果是:1
generator.next(); // 这行代码执行后,打印结果是:1 2
generator.next(); // 这行代码执行后,打印结果是:1 2 3

生成器 generator 每调用一次 next() ,foo() 函数里的代码就往下执行一次,直到遇到 yield 后暂停。

传递参数

执行 next() 方法之后会包含两个属性的对象,两个属性分别为 donevalue,这与迭代器中 next 方法返回的值是一致的,该 value的值来自那里呢?实际上,我们通过一定的机制在生成器函数外部和内部之间值。

  • yield 表达式: yield 带上表达式,该表达式可以传递给 next 方法的 value 属性上;
  • next(参数):next方法带上参数,该参数会传递到下一个 yield 表达式的返回值。

传递参数如下所示: Js generator

如何中途结束生成器的执行

如果想在中途结束生成器的执行,有三种方式:

  • 方式1:return 语句。这个在前面已经讲过。
  • 方式2:通过生成器的 return() 函数。
  • 方式3:通过生成器的 throw() 函数抛出异常。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 通过 * 符号,定义一个生成器函数
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'));
// 【关键代码】通过生成器的 throw()方法抛出异常, 立即结束 foo 函数的执行
console.log(generator.throw(new Error('next2 error')));
// 这行写了也没用,阶段2、阶段3都不会执行的
console.log(generator.next('next3'));


参考:


1. 迭代器