咳咳,各位观众老爷们,晚上好!我是你们今晚的JS讲师,咱们今晚聊点有意思的,关于JS里“Iterator”(迭代器)和“Generator”(生成器)这哥俩,以及怎么用它们打造我们自己的可迭代对象,外加体验一下“惰性求值”的快感。
一、啥是迭代器?为啥我们需要它?
想象一下,你有一堆东西,比如一个数组[1, 2, 3, 4, 5]
。你想一个一个地把它们拿出来,做点处理,比如打印出来,或者加个1啥的。 你肯定不想每次都手动去 array[0]
,array[1]
,这样写代码吧? 太low了!
这时候,迭代器就派上用场了。它可以像一个“指针”一样,指向数组中的某个元素,并且提供一种统一的方式,让你能一个一个地访问到数组里的所有元素,而不用关心数组内部是怎么实现的。
简单来说,迭代器就是一个对象,它定义了一个序列,并在终止时返回一个值。更具体地说,它是一个具有 next()
方法的对象,该方法返回一个包含 value
和 done
两个属性的对象。
value
:序列中的下一个值。done
:一个布尔值,指示迭代器是否已完成。如果为true
,则迭代器已到达序列的末尾,并且value
可以被忽略。
代码示例:
const myArray = [1, 2, 3];
const iterator = myArray[Symbol.iterator](); // 获取数组的迭代器
console.log(iterator.next()); // { value: 1, done: false }
console.log(iterator.next()); // { value: 2, done: false }
console.log(iterator.next()); // { value: 3, done: false }
console.log(iterator.next()); // { value: undefined, done: true }
看到了没? myArray[Symbol.iterator]()
这玩意儿返回的就是一个迭代器。 Symbol.iterator
是一个特殊的 symbol,它定义了对象的默认迭代器。 然后,我们调用 iterator.next()
就可以一个一个地拿到数组里的元素了。 当 done
为 true
的时候,就说明迭代器已经到头了,没东西可拿了。
为啥我们需要迭代器?
- 统一的访问方式: 不管是数组、字符串、Set、Map,还是你自己定义的奇奇怪怪的数据结构,只要实现了迭代器,你就可以用统一的方式(比如
for...of
循环)来访问它们。 - 代码更简洁: 告别
for (let i = 0; i < array.length; i++)
这种老掉牙的写法吧!用for...of
循环,代码更简洁易懂。 - 支持惰性求值: 这个后面会详细讲,简单来说,就是只有在你需要用到某个值的时候,才去计算它,可以节省资源。
二、 for...of
循环:迭代器的最佳搭档
for...of
循环是专门用来遍历可迭代对象的。 它会自动调用对象的迭代器,并且每次循环都会拿到迭代器返回的 value
值。
代码示例:
const myArray = [1, 2, 3];
for (const element of myArray) {
console.log(element); // 依次打印 1, 2, 3
}
是不是比 for
循环简洁多了? 而且,for...of
循环不仅可以遍历数组,还可以遍历字符串、Set、Map 等等,只要它们实现了迭代器。
三、如何创建自定义的可迭代对象?
现在,我们来学习如何创建自己的可迭代对象。 这可是个很有用的技能,以后你想定义任何奇葩的数据结构,都可以让它支持迭代。
要让一个对象变成可迭代的,你需要做两件事:
- 实现
Symbol.iterator
方法: 这个方法必须返回一个迭代器对象。 - 迭代器对象需要有
next()
方法:next()
方法必须返回一个包含value
和done
属性的对象。
代码示例:
const myIterable = {
data: [1, 2, 3],
[Symbol.iterator]() {
let index = 0;
return {
next: () => {
if (index < this.data.length) {
return { value: this.data[index++], done: false };
} else {
return { value: undefined, done: true };
}
}
};
}
};
for (const element of myIterable) {
console.log(element); // 依次打印 1, 2, 3
}
这个例子中,我们定义了一个 myIterable
对象,它有一个 data
属性,存储了我们要迭代的数据。 然后,我们实现了 Symbol.iterator
方法,它返回一个迭代器对象。 这个迭代器对象有一个 next()
方法,每次调用 next()
方法,它都会返回 data
数组中的下一个元素,直到数组的末尾。
四、 Generator
:更优雅的迭代器创建方式
手动实现迭代器对象还是有点麻烦的,特别是当你的数据结构比较复杂的时候。 这时候,Generator
就派上用场了。
Generator
是一种特殊的函数,它可以让你更轻松地创建迭代器。 它使用 function*
语法来定义,并且可以使用 yield
关键字来暂停函数的执行,并返回一个值。
代码示例:
function* myGenerator() {
yield 1;
yield 2;
yield 3;
}
const iterator = myGenerator();
console.log(iterator.next()); // { value: 1, done: false }
console.log(iterator.next()); // { value: 2, done: false }
console.log(iterator.next()); // { value: 3, done: false }
console.log(iterator.next()); // { value: undefined, done: true }
看到了没? 我们用 function*
定义了一个 myGenerator
函数。 在函数内部,我们使用 yield
关键字来返回一个值。 每次调用 iterator.next()
,函数都会从上次 yield
的地方继续执行,直到遇到下一个 yield
或者函数结束。
用 Generator
创建可迭代对象:
const myIterable = {
data: [1, 2, 3],
[Symbol.iterator]: function*() {
for (const element of this.data) {
yield element;
}
}
};
for (const element of myIterable) {
console.log(element); // 依次打印 1, 2, 3
}
这个例子中,我们用 Generator
函数来实现 Symbol.iterator
方法。 代码更加简洁易懂了。
五、惰性求值:按需计算,节省资源
Generator
的一个重要特性就是支持惰性求值。 这意味着,只有在你需要用到某个值的时候,才会去计算它。 这在处理大量数据或者计算密集型任务时非常有用,可以节省大量的资源。
代码示例:
function* infiniteSequence() {
let n = 0;
while (true) {
yield n++;
}
}
const iterator = infiniteSequence();
console.log(iterator.next()); // { value: 0, done: false }
console.log(iterator.next()); // { value: 1, done: false }
console.log(iterator.next()); // { value: 2, done: false }
// ...
这个例子中,我们定义了一个 infiniteSequence
生成器函数,它可以生成一个无限的数字序列。 如果我们一次性把所有的数字都计算出来,那肯定会爆内存。 但是,由于 Generator
支持惰性求值,所以只有在我们需要用到某个数字的时候,才会去计算它。
惰性求值的优势:
- 节省内存: 不需要一次性加载所有数据到内存中。
- 提高性能: 只计算需要用到的数据,避免不必要的计算。
- 可以处理无限序列: 可以处理无法一次性加载到内存中的无限序列。
*六、 `yield`:代理迭代,化繁为简**
yield*
是一种特殊的 yield
语法,它可以让你把一个迭代器代理给另一个迭代器。 简单来说,就是在一个 Generator
函数里,你可以用 yield*
来迭代另一个可迭代对象。
代码示例:
function* generator1() {
yield 1;
yield 2;
}
function* generator2() {
yield* generator1(); // 代理 generator1 的迭代
yield 3;
yield 4;
}
const iterator = generator2();
console.log(iterator.next()); // { value: 1, done: false }
console.log(iterator.next()); // { value: 2, done: false }
console.log(iterator.next()); // { value: 3, done: false }
console.log(iterator.next()); // { value: 4, done: false }
console.log(iterator.next()); // { value: undefined, done: true }
这个例子中,generator2
使用 yield* generator1()
来代理 generator1
的迭代。 这意味着,generator2
会先迭代 generator1
产生的所有值,然后再继续执行自己的代码。
yield*
可以让你更方便地组合多个迭代器,构建更复杂的迭代逻辑。
七、实际应用场景:
- 处理大型文件: 可以逐行读取大型文件,而不需要一次性加载整个文件到内存中。
- 实现无限滚动加载: 可以根据用户的滚动位置,动态加载数据,而不需要一次性加载所有数据。
- 构建复杂的数据流: 可以把多个
Generator
函数组合起来,构建复杂的数据处理流程。 - 异步编程:
Generator
函数可以和Promise
结合使用,实现更优雅的异步编程。 (这个比较高级,以后有机会再细说)
八、 总结:
特性 | Iterator | Generator |
---|---|---|
定义 | 具有 next() 方法的对象 |
一种特殊的函数,使用 function* 定义 |
创建方式 | 手动实现 next() 方法 |
使用 yield 关键字暂停和恢复函数执行 |
主要用途 | 提供一种统一的访问集合元素的方式 | 更简洁地创建迭代器,支持惰性求值 |
惰性求值 | 不支持 | 支持 |
yield* |
不支持 | 支持,用于代理迭代 |
总而言之,Iterator
和 Generator
是 JS 中非常强大的特性,它们可以让你更轻松地处理各种数据结构,并且可以提高代码的效率和可读性。 希望今天的讲解能帮助大家更好地理解和运用这两个特性。
好了,今天的讲座就到这里。 大家还有什么问题吗? 没有的话,就散会啦! 记得回去好好练习哦! 咱们下期再见!