在前端开发中,设计模式是提升代码可读性、可扩展性和可维护性的关键。迭代器模式(Iterator Pattern)是行为型设计模式中的一种,能够让我们顺序访问一个集合中的元素,而不暴露其底层的结构。在 TypeScript 这样具有类型检查的语言中,迭代器模式应用更加方便。
简单for循环
迭代器模式解决的问题是如何更加方便,更加简易,更加友好地去遍历一个顺序集合。用for循环不就行了?不行,普通for循环不是迭代器,它有两个缺点,不符合我们的设计原则。
const arr = [10, 20, 30]
const length = arr.length
for (let i = 0; i < length; i++) {
console.log(arr[i])
}
如上代码,首先需要知道数组的长度 length ,需要知道如何获取数组的元素 arr[i] 。这样就不符合高内聚低耦合的设计原则,这样数组的很多信息就被公开了。作为一个数组被公开没什么问题,但是如果这个数组是一个比较复杂的数据结构呢?或者第三方服务呢?把场景放大就会是很大的问题。所以普通for循环不是迭代器,迭代器解决的问题是解决数据源里面数据不确定问题、数据封装问题,更好地遍历有序集合的问题。
简易迭代器
const pList = document.querySelectorAll("p");
pList.forEach((p) => console.log(p));
这里没有使用for循环,而使用forEach,本质的区别是,forEach遍历的时候,不需要知道数组长度,也不需要知道怎么获取一个 元素,它直接通过参数传过来了。
迭代器模式
UML类图如下:
class DataIterator {
private data: number[];
private index: number = 0;
constructor(container: DataContainer) {
this.data = container.data;
}
next() {
if (this.hasNext()) {
return this.data[this.index++];
}
return null;
}
hasNext() {
return this.index < this.data.length;
}
}
class DataContainer {
data = [10, 11, 12, 13, 14, 15];
getIterator() {
return new DataContainer(this);
}
}
const dataContainer = new DataContainer();
const dataIterator = new DataIterator(dataContainer);
while (dataIterator.hasNext()) {
console.log(dataIterator.next());
}
以上输出结果和for循环遍历是一样的,但我们费这么大力气是为了什么呢?就是为了掩饰迭代器背后它是怎样不用for循环,不用获取长度、不用通过index获取数据的这种高内聚低耦合的方式,更符合设计原则:使用者和目标分离解耦;目标能自行控制其内部逻辑;使用者不关心目标的内部结构。
迭代器场景
1. 有序结构。包含:字符串、数组、NodeList等DOM集合、Map、Set、arguments
2. Symbol.iterator
所有有序对象,都内置Symbol.iterator方法,执行该方法,会返回一个迭代器对象。
done为false,说明还没有到最后一个。和我们的 hasNext() 差不多。
我们可以自己定义一个迭代器。
interface IteratorRes {
value: number | undefined;
done: boolean;
}
class CustomIterator {
private length = 3;
private index = 0;
next(): IteratorRes {
this.index++;
if (this.index <= this.length) {
return { value: this.index, done: false };
}
return { value: undefined, done: true };
}
[Symbol.iterator]() {
return this;
}
}
const customIterator = new CustomIterator();
console.log(customIterator.next());
console.log(customIterator.next());
console.log(customIterator.next());
console.log(customIterator.next());
迭代器作用
1. 用于for of
只要有内置了Symbol.iterator,都可以执行。
const set = new Set([1, 2, 3]);
for (const num of set) {
console.log(num);
}
const pList = document.querySelectorAll("p");
for (const p of pList) {
console.log(p);
}
// 包括我们上面的例子
const customIterator = new CustomIterator();
for (const ci of customIterator) {
console.log(ci);
2. 数组解构、扩展操作符、Array.from (可用于有任何迭代器模式的数据结构)
// 解构 数组
const arr = [10, 11, 12];
const [a1, a2] = arr;
console.log("a1", a1); // 10
console.log("a2", a2); // 11
// 解构 Set
const set = new Set([100, 200, 300]);
const [s1, s2] = set;
console.log("s1", s1); // 100
console.log("s2", s2); // 200
// 扩展操作符
const arr = [10, 11, 12];
console.log([...arr]) // [10, 11, 12]
const set = new Set([100, 200, 300]);
console.log([...set]) // [100, 200, 300]
// Array.form
console.log(Array.from(set)) // [100, 200, 300]
3. 用于创建Map和Set
4. 用于 Promise.all 和 Promise.race
5. 用于 yield*
Generator 生成器
function* getNums(){
yield 10
yield 20
yield 30
}
const iterator = getNums()
console.log(iterator.next()) // {value: 10, done: false}
console.log(iterator.next()) // {value: 20, done: false}
console.log(iterator.next()) // {value: 30, done: false}
console.log(iterator.next()) // {value: undefined, done: true}
// 将上面的 console.log注释,接下来使用 for of
for (const number of iterator) {
console.log(number) // 10 20 30
}
可见生成器返回的就是一个迭代器。
function* getNums(){
yield* [10,20,30] // 有序结构,已经实现了[Symbol.Iterator]
}
const iterator = getNums()
for (const number of iterator) {
console.log(number) // 10,20,30
}
接下来重构一下上面的 CustomIterator
class CustomIterator {
private data: number[];
constructor() {
this.data = [10, 20, 30];
}
*[Symbol.iterator]() {
yield* this.data;
}
}
const iterator = new CustomIterator();
for (const n of iterator) {
console.log(n); // 10 20 30
}
接下来我们使用 Generator + yield 深度优先遍历 DOM 树
<div id="app">
<p>
<b>hello</b>
<i>world</i>
</p>
<p>
<img src="https://www.baidu.com/img/PCfb_5bf082d29588c07f842ccde3f97243ea.png" />
</p>
</div>
function* traverse(elementList: Element[]): any {
for (const element of elementList) {
yield element;
const children = Array.from(element.children);
if (children.length) {
yield* traverse(children);
}
}
}
const iterator = document.getElementById("app");
if (iterator) {
for (const iteratorElement of traverse([iterator])) {
console.log(iteratorElement);
}
}