|
| 1 | +# Iterator(迭代器模式) |
| 2 | + |
| 3 | +Iterator(迭代器模式)属于行为型模式。 |
| 4 | + |
| 5 | +**意图:提供一种方法顺序访问一个聚合对象中的各个元素,而又不需要暴露该对象的内部表示。** |
| 6 | + |
| 7 | +这种设计模式要解决的根本问题是,聚合的种类有很多,比如对象、链表、数组、甚至自定义结构,但遍历这些结构时,不同结构的遍历方式又不同,所以我们必须了解每种结构的内部定义才能遍历。 |
| 8 | + |
| 9 | +比如数组我们可以利用 length + for 循环,对象我们可以 Object.keys,链表比较麻烦,需要内部暴露出元素的 `next` 以操作指向下一个元素。 |
| 10 | + |
| 11 | +迭代器模式可以做到用同一种 API 遍历任意类型聚合对象,且不用关心聚合对象的内部结构。 |
| 12 | + |
| 13 | +这种模式和 `Array.from` 有点像,但其实真正的迭代器在 JS 里是 `obj[Symbol.iterator]()`,也就是一个对象实现了 `[Symbol.interator]`,就认为是可遍历的。 |
| 14 | + |
| 15 | +## 举例子 |
| 16 | + |
| 17 | +如果看不懂上面的意图介绍,没有关系,设计模式需要在日常工作里用起来,结合例子可以加深你的理解,下面我准备了三个例子,让你体会什么场景下会用到这种设计模式。 |
| 18 | + |
| 19 | +迭代器的例子非常简单,我们平时工作中有大量使用到。 |
| 20 | + |
| 21 | +### generator |
| 22 | + |
| 23 | +`generator` 天生为迭代器的 API: |
| 24 | + |
| 25 | +```typescript |
| 26 | +function* func () { |
| 27 | + yield 'a'; |
| 28 | + yield 'b'; |
| 29 | + return 'c'; |
| 30 | +} |
| 31 | + |
| 32 | +var run = func(); |
| 33 | +run.next() // {value: "a", done: false} |
| 34 | +run.next() // {value: "b", done: false} |
| 35 | +run.next() // {value: "c", done: true} |
| 36 | +``` |
| 37 | + |
| 38 | +我们无需关心 generator 内部是何种存储结构,只需要调用 `.next()`,并根据返回的 `done` 来判断是否遍历完即可。在 generator 的场景中,迭代器不仅用来遍历聚合,还用于执行代码。 |
| 39 | + |
| 40 | +### 数组迭代器 |
| 41 | + |
| 42 | +我们可以用迭代器的方式遍历数组: |
| 43 | + |
| 44 | +```typescript |
| 45 | +const arr = [1, 2, 3] |
| 46 | +const run = arr[Symbol.iterator]() |
| 47 | + |
| 48 | +run.next() // {value: 1, done: false} |
| 49 | +run.next() // {value: 2, done: false} |
| 50 | +run.next() // {value: 2, done: false} |
| 51 | +run.next() // {value: undefined, done: true} |
| 52 | +``` |
| 53 | + |
| 54 | +可能有人觉得这是画蛇添足,因为毕竟遍历数组用 for 循环更方便,但这就是设计模式与非设计模式思维的区别,重要的不是用熟悉简单的 API 快速满足需求,**设计模式关注的是如何统一、抽象、低耦合的编码**。 |
| 55 | + |
| 56 | +### Map 迭代器 |
| 57 | + |
| 58 | +Map 对象也可以用迭代器方式遍历: |
| 59 | + |
| 60 | +```typescript |
| 61 | +const citys = new Map([['北京', 1], ['上海', 2], ['杭州', 3]]) |
| 62 | +const run = citys.entries() |
| 63 | + |
| 64 | +run.next() // {value: ['北京', 1], done: false} |
| 65 | +run.next() // {value: ['上海', 2], done: false} |
| 66 | +run.next() // {value: ['杭州', 3], done: false} |
| 67 | +run.next() // {value: undefined, done: true} |
| 68 | +``` |
| 69 | + |
| 70 | +## 意图解释 |
| 71 | + |
| 72 | +从上面的例子可以看出,虽然用迭代器遍历数组看上去比 for 循环麻烦一点,但当我们把所有聚合类型放到一起看时,可以发现只有迭代器的 API 是最统一的,是唯一一个不需要关心聚合类型就可以完成遍历的方案。 |
| 73 | + |
| 74 | +**意图:提供一种方法顺序访问一个聚合对象中的各个元素,而又不需要暴露该对象的内部表示。** |
| 75 | + |
| 76 | +再来看意图,就非常好理解了,我们无需关心 数组、generator、Map 内部是如何存储的,就可以进行遍历。实际上,深究 generator 内部的存储结构也没有意义,如果我们不用迭代器进行遍历,那么对于复杂结构的遍历成本是非常高的。 |
| 77 | + |
| 78 | +## 结构图 |
| 79 | + |
| 80 | +<img width=600 src="https://img.alicdn.com/imgextra/i1/O1CN01lYSkni1DgFh6V3P85_!!6000000000245-2-tps-1658-754.png"> |
| 81 | + |
| 82 | +- `Aggregate`: 聚合,需要定义创建迭代器的接口。比如前端规范的 `[Symbol.iterator]()`,或者这里定义的 `CreateIterator()`。 |
| 83 | +- `Iterator`: 迭代器,定义了访问与遍历的 API。 |
| 84 | + |
| 85 | +迭代器的定义很简单,实现时要考虑的因素可不少,包括: |
| 86 | + |
| 87 | +- 健壮性。即迭代过程中增加、删除元素后,还能正常遍历。或者遍历空聚合时也要能正常工作。 |
| 88 | +- 外部控制迭代还是内部。即类似 KOA 由插件调用 `next()` 控制迭代,还是由外层统一控制迭代。 |
| 89 | +- 如何定义遍历算法。即便对于对象这种简单场景,也存在深度优先和广度优先、冒泡与捕获这几种遍历顺序,迭代器可以提供选择或者拓展的方式,自定义遍历算法。 |
| 90 | + |
| 91 | +## 代码例子 |
| 92 | + |
| 93 | +下面例子使用 typescript 编写。 |
| 94 | + |
| 95 | +```typescript |
| 96 | +// 定义聚合接口 |
| 97 | +interface Aggregate{ |
| 98 | + getIterator: () => Iterator |
| 99 | +} |
| 100 | + |
| 101 | +// 定义迭代器接口 |
| 102 | +interface Iterator { |
| 103 | + // 指向下一个 |
| 104 | + next: () => void |
| 105 | +} |
| 106 | + |
| 107 | +// 定义一个聚合 |
| 108 | +class List implements Aggregate { |
| 109 | + // 存储元素 |
| 110 | + public values: string[] |
| 111 | + |
| 112 | + // 游标 |
| 113 | + public index: number |
| 114 | + |
| 115 | + getIterator() { |
| 116 | + return new ConcreteIterator(this); |
| 117 | + } |
| 118 | +} |
| 119 | + |
| 120 | +// List 的迭代器 |
| 121 | +class ConcreteIterator implements Iterator { |
| 122 | + constructor(list: List) { |
| 123 | + this.list = list |
| 124 | + } |
| 125 | + |
| 126 | + next() { |
| 127 | + return this.list.values[this.list.index] // 注意边界情况,这里就不展开 |
| 128 | + this.list.index++ |
| 129 | + } |
| 130 | +} |
| 131 | +``` |
| 132 | + |
| 133 | +## 弊端 |
| 134 | + |
| 135 | +如果你只是遍历数组,直接用 for 循环会比迭代器方便很多,没必要为了用设计模式而用设计模式。迭代器仅在以下情况可以考虑用于数组: |
| 136 | + |
| 137 | +1. 这个数组比较特殊,是 N 维数组,需要一次性遍历完,那么可以用迭代器。 |
| 138 | +2. 同时遍历数组和其他类型的聚合,则不论数组还是其他聚合,都用相同的迭代器模式遍历最好。 |
| 139 | + |
| 140 | +## 总结 |
| 141 | + |
| 142 | +迭代器模式比较好理解,这里补充几个相关设计模式: |
| 143 | + |
| 144 | +- 迭代器可以和组合模式配合,在组合结构内进行递归,这样一个迭代器就可以遍历完所有组合。 |
| 145 | +- 可以用工厂模式 + 多态模式,实例化不同的迭代器的实例。 |
| 146 | +- 迭代器模式还可以与备忘录模式配合使用,当我们要还原迭代器状态时,适合在迭代器内部使用备忘录模式进行状态存储。 |
| 147 | + |
| 148 | +> 讨论地址是:[精读《设计模式 - Iterator 迭代器模式》· Issue #298 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/298) |
| 149 | +
|
| 150 | +**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。** |
| 151 | + |
| 152 | +> 关注 **前端精读微信公众号** |
| 153 | +
|
| 154 | +<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg"> |
| 155 | + |
| 156 | +> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh)) |
0 commit comments