ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

前端精读周刊:Iterator 迭代器模式 —— 用统一 API 遍历任意聚合对象

前端精读周刊:Iterator 迭代器模式 —— 用统一 API 遍历任意聚合对象 文档技术博客教程【免费下载链接】weekly前端精读周刊。帮你理解最前沿、实用的技术。项目地址https://gitcode.com/GitHub_Trending/we/weekly点击查看免费下载本文以《设计模式 - Iterator 迭代器模式》精读为核心讲解迭代器模式的行为型设计意图、JS 中[Symbol.iterator]协议的具体形态、generator / 数组 / Map 三类典型应用、TypeScript 手写实现与实现时的健壮性考量并联动仓库内《精读《迭代器 Iterable》》一文帮助你从设计模式与语言内置协议两个视角吃透迭代器掌握用同一套 API 遍历任意聚合对象而不暴露其内部结构的能力。为什么需要迭代器模式从遍历方式的割裂说起迭代器模式Iterator属于行为型模式其经典意图是提供一种方法顺序访问一个聚合对象中的各个元素而又不需要暴露该对象的内部表示。这个意图要解决的根本问题是聚合的种类非常多——对象、链表、数组甚至自定义结构——而不同结构的遍历方式各不相同所以我们必须了解每种结构的内部定义才能遍历它数组可以利用lengthfor循环遍历对象可以使用Object.keys遍历链表比较麻烦需要内部暴露出元素的next指针由调用方手动沿着next走到下一个元素。如果业务代码里到处散落着针对不同结构写死的遍历逻辑那么一旦结构发生变化或引入新的聚合类型所有调用点都要跟着改。迭代器模式的价值在于用同一种 API 遍历任意类型的聚合对象且调用方完全不用关心聚合对象内部是怎么存储的。在 JS 里这种统一 API并不是抽象概念而是语言层面的真实协议。它和Array.from有点像但真正的迭代器在 JS 中是obj[Symbol.iterator]()——一个对象只要实现了[Symbol.iterator]就被认为是可遍历的。这一点在仓库姊妹篇 精读《迭代器 Iterable》 中有更完整的展开JS 引擎之所以知道字符串该如何逐字符遍历、数组[1, 2, 3]与Set每个 Key 如何对应背后的原理就是迭代器协议Array、Set都是可迭代的所以它们都能被for...of遍历也能相互转换。三个工作里触手可及的例子设计模式需要在日常工作里用起来。下面三个例子能让你直观体会什么场景下会用到迭代器以及统一遍历 API 到底意味着什么。例子一generator 天生就是迭代器 APIgenerator天生为迭代器而生function* func () { yield a; yield b; return c; } var run func(); run.next() // {value: a, done: false} run.next() // {value: b, done: false} run.next() // {value: c, done: true}调用方无需关心 generator 内部是何种存储结构只需要调用.next()并根据返回的done字段判断是否遍历完即可。注意在 generator 的场景中迭代器不仅用来遍历聚合还用于执行代码——next()每次调用都会让函数体推进到下一个yield处这比单纯遍历更进了一步。例子二用迭代器遍历数组数组同样可以用迭代器的方式遍历const arr [1, 2, 3] const run arr[Symbol.iterator]() run.next() // {value: 1, done: false} run.next() // {value: 2, done: false} run.next() // {value: 3, done: false} run.next() // {value: undefined, done: true}说明next()返回的{ value, done }二元结构是整个迭代协议的契约——done: false表示迭代尚未结束、value是当前元素done: true表示遍历完毕。遍历到末尾后再调用next()会返回{ value: undefined, done: true }。可能有人觉得这是画蛇添足——遍历数组用for循环显然更方便。但这就是设计模式思维与非设计模式思维的区别重要的不是用熟悉简单的 API 快速满足眼前需求而是关注如何统一、抽象、低耦合地编码。例子三Map 迭代器Map 对象也可以用迭代器方式遍历const citys new Map([[北京, 1], [上海, 2], [杭州, 3]]) const run citys.entries() run.next() // {value: [北京, 1], done: false} run.next() // {value: [上海, 2], done: false} run.next() // {value: [杭州, 3], done: false} run.next() // {value: undefined, done: true}这里用的是Map.prototype.entries()——它返回的迭代器以[key, value]二元组为元素依然是同一个{ value, done }契约。事实上 精读《迭代器 Iterable》 指出String、Array、TypedArray、Map、Set等内置类型全部支持迭代它们都能被for...of统一遍历这就是迭代器协议在语言层面的统一性。意图解释为什么迭代器是唯一不需要关心聚合类型的方案把上面所有聚合类型放到一起看结论就很清晰了虽然用迭代器遍历数组看上去比for循环麻烦一点但在所有聚合类型的遍历方案中只有迭代器的 API 是最统一的是唯一一个不需要关心聚合类型就可以完成遍历的方案。再回到意图提供一种方法顺序访问一个聚合对象中的各个元素而又不需要暴露该对象的内部表示。此时就非常好理解了我们无需关心数组、generator、Map 内部是如何存储的就可以进行遍历。实际上深究 generator 内部的存储结构也没有意义如果我们不用迭代器进行遍历那么对复杂结构的遍历成本会非常高。这种黑盒遍历的思想在 精读《迭代器 Iterable》 中被概括为一句话一个对象黑盒不论内部怎么实现如果我们能按照顺序数出内部结构那么这个对象就是可迭代的。这正是[Symbol.iterator]协议要解决的问题也正是 Iterator 设计模式在语言层面落地的形态。结构图与两个核心角色迭代器模式的类图结构由两个核心角色组成Aggregate聚合定义创建迭代器的接口。在前端规范里对应[Symbol.iterator]()在经典类图里对应CreateIterator()。Iterator迭代器定义访问与遍历的 API。在 JS 中这两个角色的边界由迭代协议天然划清聚合对象实现[Symbol.iterator]()方法并返回一个迭代器对象迭代器对象实现next()方法返回{ value, done }。参考 精读《迭代器 Iterable》在 JS 里自定义迭代器通常有两种写法写法一独立定义迭代器const obj {} obj[Symbol.iterator] function() { return { someValue: 1, next() { // 可通过 this.someValue 访问与修改该值可定义任意数量的变量作为迭代过程中的辅助变量 if (/* 未遍历完 */) { return { done: false, value: this.current } // 迭代还没完当前值为 value } return { done: true } // 迭代完毕 } } }之所以规范采用[Symbol.iterator]这个符号属性是为了防止普通的字面量 Key 与对象自身的 OwnProperties 冲突。写法二合并在对象里定义迭代器即自身const range { from: 1, to: 5, [Symbol.iterator]() { this.current this.from return this }, next() { if (this.current this.to) { return { done: false, value: this.current } } else { return { done: true } } } }这种写法的缺点是并行迭代同一个对象时可能触发 BUG因为每次迭代共享了同一份状态变量current。这也是实现时要考虑的因素之一——迭代器的状态管理需要谨慎设计。实现时需要考虑的三个因素迭代器的定义很简单实现时要考虑的因素可不少健壮性迭代过程中增加、删除元素后还能正常遍历遍历空聚合时也要能正常工作即空聚合也应立刻返回{ done: true }而不是报错或死循环。外部控制迭代还是内部控制类似 KOA 由插件调用next()控制迭代还是由外层统一控制迭代。这决定了迭代器的推进权归属——JS 中for...of属于外层自动控制而手动调用iterator.next()则把控制权完全交给你可以自由控制next()的触发时机与频率甚至提前终止详见 精读《迭代器 Iterable》 中手动控制迭代一节。如何定义遍历算法即便对于对象这种简单场景也存在深度优先和广度优先、冒泡与捕获这几种遍历顺序。迭代器可以提供选择或者扩展的方式自定义遍历算法从而把怎么走的算法细节封装在迭代器内部对调用方透明。TypeScript 手写实现从接口到具体迭代器下面是用 TypeScript 实现的一个最小可运行的迭代器模式版本对应原文档代码此处补全了边界处理与使用示例// 定义聚合接口聚合需要提供创建迭代器的方法 interface Aggregate { getIterator: () Iterator } // 定义迭代器接口定义访问与遍历的 API interface Iterator { // 指向下一个返回 { value, done } next: () { value?: any; done: boolean } } // 定义一个聚合List 内部用数组存储元素并维护一个游标 class List implements Aggregate { // 存储元素 public values: string[] // 游标 public index: number constructor(values: string[]) { this.values values this.index 0 } getIterator() { return new ConcreteIterator(this) } } // List 的迭代器 class ConcreteIterator implements Iterator { private list: List constructor(list: List) { this.list list } next() { // 边界情况游标越过末尾时返回 done: true避免越界访问 if (this.list.index this.list.values.length) { return { value: undefined, done: true } } const value this.list.values[this.list.index] this.list.index return { value, done: false } } } // 使用调用方完全不关心 List 内部如何存储只依赖统一 API 遍历 const list new List([a, b, c]) const iterator list.getIterator() iterator.next() // { value: a, done: false } iterator.next() // { value: b, done: false } iterator.next() // { value: c, done: false } iterator.next() // { value: undefined, done: true }从代码可以看出迭代器模式在 OO 世界的标准形态Aggregate聚合负责生产Iterator迭代器Iterator内部持有对聚合的引用并维护游标状态。原文档中的示例代码刻意省略了边界处理注释说明注意边界情况这里就不展开本文在实现中补全了游标越界时返回done: true的健壮性逻辑——这正是上文实现时需要考虑的因素中健壮性要求的具体落地。值得一提的是上面这套{ value, done }的返回契约与 JS 内置迭代协议完全一致。也就是说如果你让上面的ConcreteIterator同时暴露[Symbol.iterator]()返回this它就能直接支持for...of、Array.from、展开运算符等语言级能力。在 精读《迭代器 Iterable》 中可以看到更多可迭代对象适用的内置 APIMap()、WeakMap()、Set()、WeakSet()、Promise.all()、Promise.allSettled()、Promise.race()、Promise.any()、Array.from()以及依赖迭代器实现的解构与展开语法// Array.from 可以将可迭代对象变成真正的数组 Array.from(new Set([1, 2, 3])) // [1, 2, 3] // 解构与展开本质也是用迭代器进行运算 [...range] // [1, 2, 3, 4, 5]range 为上面实现了 [Symbol.iterator] 的对象弊端与适用场景不要为了模式而模式迭代器模式并非万能。如果你只是遍历数组直接用for循环会比迭代器方便很多没必要为了用设计模式而用设计模式。迭代器仅在以下情况可以考虑用于数组这个数组比较特殊是 N 维数组需要一次性遍历完那么可以用迭代器把维度展开的算法封装进迭代器调用方无需递归。同时遍历数组和其他类型的聚合不论数组还是其他聚合都用相同的迭代器模式遍历最好——此时统一 API 的收益最大正如本文意图解释部分所强调的唯一统一方案。换句话说迭代器模式的价值在异构聚合的统一遍历场景下才会真正体现对单一结构、简单场景直接用语言原生的for循环反而更直白、更高效。与其他设计模式的关系迭代器模式比较好理解这里补充几个密切相关的设计模式与组合模式配合组合模式Composite将对象组合成树形结构以表示部分 - 整体的层次结构详见 精读《设计模式 - Composite 组合模式》。迭代器可以在组合结构内进行递归这样一个迭代器就能遍历完整个组合树的所有节点把树的遍历算法从调用方手中解放出来。与工厂模式 多态模式配合可以用工厂模式 多态模式实例化不同的迭代器的实例——聚合对象通过工厂方法按需创建不同策略的迭代器例如深度优先遍历器、广度优先遍历器再通过多态统一使用。与备忘录模式配合当我们要还原迭代器状态时适合在迭代器内部使用备忘录模式Memento进行状态存储。备忘录模式的意图是在不破坏封装性的前提下捕获一个对象的内部状态并在该对象之外保存这个状态以便之后恢复详见 精读《设计模式 - Memoto 备忘录模式》。把迭代游标等状态快照存入备忘录就可以随时将迭代器恢复到之前的遍历位置。总结迭代器模式的核心思想可以用一句话概括把遍历这件事从被遍历的结构中解耦出来——聚合对象不暴露内部表示只负责交出迭代器迭代器封装遍历算法与游标状态对外只暴露统一的next()API。在 JS 中这一思想已经内化为语言协议实现[Symbol.iterator]即可获得for...of、Array.from、展开、解构等一整套语言级能力的支持详见 精读《迭代器 Iterable》。在仓库的 设计模式 模块中本文属于行为型模式系列的一环其姊妹文章 精读《设计模式 - Composite 组合模式》、精读《设计模式 - Memoto 备忘录模式》 分别从树形结构与状态恢复两个角度与迭代器模式形成了完整的协同关系readme.md 汇总了整套精读周刊的全部模块索引可作为继续深入学习的导航。赞分享文档技术博客教程【免费下载链接】weekly前端精读周刊。帮你理解最前沿、实用的技术。项目地址https://gitcode.com/GitHub_Trending/we/weekly点击查看免费下载相关推荐scrcpy安卓投屏3个命令把手机屏幕搬进电脑scrcpy安卓投屏3个命令把手机屏幕搬进电脑 如果你想在电脑上直接看到手机屏幕再用鼠标键盘去操作它scrcpy就是干这件事的工具。它通过USB或WiFi音视频fe-interview前端迭代器模式集合遍历访问fe interview前端迭代器模式集合遍历访问 引言为什么需要迭代器模式 在日常前端开发中我们经常需要处理各种数据集合的遍历操作。从简单的数组遍历到前端知识库教程别再手动点了青龙面板 API 批量运维全流程别再手动点了青龙面板 API 批量运维全流程 凌晨两点一批定时任务要在整点前停掉你只能打开青龙面板的网页一个个点想改脚本里的账号配置又要翻到环境变量页任务调度后端前端创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表