ARTICLE DETAIL

资讯详情

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

前端精读周刊:深入理解 JavaScript 迭代器 Iterable 协议与实战应用

前端精读周刊:深入理解 JavaScript 迭代器 Iterable 协议与实战应用 文档技术博客教程【免费下载链接】weekly前端精读周刊。帮你理解最前沿、实用的技术。项目地址https://gitcode.com/GitHub_Trending/we/weekly点击查看免费下载本篇精读源自 前沿技术/262.精读《迭代器 Iterable》.md以[Symbol.iterator]协议为线索系统讲解 JS 迭代器「为什么需要、如何设计、如何手动控制、与 ArrayLike 的差异、内置可迭代类型及适用 API」六大主题。读完你将能够手写任意自定义对象的迭代协议、正确区分可迭代与类数组、熟练运用for...of、Array.from、展开/解构等一切基于迭代器的语法与 API并理解Map/Set/generator在底层是如何协作的。为什么需要迭代器for...of循环数组非常方便但如果只有数组才支持这个语法就太麻烦了。我们自然希望for...of可以遍历字符串的每个字符希望new Set([1, 2, 3])可以快速初始化一个新的Set。以上能力 JS 都支持。那么问题来了JS 引擎凭什么知道字符串该如何遍历凭什么知道数组[1, 2, 3]与Set类型每一个 Key 之间的对应关系实现这些能力背后的统一原理就是迭代器Iterables。因为Array、Set都是可迭代的所以它们都可以被for...of遍历JS 引擎也自然知道它们之间相互转换的关系。换句话说迭代器协议定义了一套跨类型统一的遍历契约任何实现了该契约的对象引擎就统一按该契约处理而无需针对每种类型单独写遍历逻辑。迭代器是如何设计的有两种定义迭代器的方法独立定义与合并在对象里定义。独立定义为对象拓展[Symbol.iterator]属性为对象拓展[Symbol.iterator]属性即可。之所以规范采用[Symbol.iterator]这个 Symbol 键是为了防止普通的字面量 Key 与对象自身的 OwnProperties 冲突——普通字符串 Key 可能被用户数据覆盖而 Symbol 键天然不会与字符串键冲突const obj {} obj[Symbol.iterator] function() { return { someValue: 1, next() { // 可通过 this.someValue 访问与修改该值可定义任意数量的变量作为迭代过程中的辅助变量 if (...) { return { done: false, value: this.current } // 表示迭代还没完当前值为 value } return { done: true } // 表示迭代完毕 } }; };在for...of时只要没有读到done: true就会一直循环。这段代码揭示了迭代器协议的两个核心约定返回对象必须包含next()方法引擎在每次循环迭代时调用它next()必须返回形如{ done, value }的结果对象done: false表示迭代继续此时value携带当前元素done: true表示迭代结束。规范要求done为true时value可以省略这也是末尾常写成return { done: true }的原因。值得一提的是这里的this.current展示了一个常见技巧迭代器闭包/对象内部可以持有任意辅助变量用来记录「当前游标」这类迭代状态。这与 设计模式/182.精读《设计模式 - Iterator 迭代器模式》.md 中提到的index游标设计一脉相承——迭代器模式的核心就是把「如何遍历」与「内部存储结构」解耦外部只关心next()与done。合并在对象里定义对象自身即是迭代器简化一点可以将迭代定义在对象本身let 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 }; } }, };这里的[Symbol.iterator]()不再返回一个新对象而是直接返回this——因为this自身已经实现了next()本身就是合法的迭代器。这么定义的缺点是并行迭代对象时可能触发 BUG因为每个迭代间共享了同一份状态变量。例如同时启动两个for...of遍历同一个range两个循环会互相修改this.current导致遍历结果错乱。而「独立定义」方式每次调用[Symbol.iterator]()都生成独立的新迭代器对象各自持有独立状态天然规避了并发迭代的共享状态问题。手动控制迭代迭代器完全可以自定义触发时机这是理解其本质的关键一步const myObj iterable[Symbol.iterator](); myObj.next(); // { value: 1, done: false } myObj.next(); // { value: 2, done: false } myObj.next(); // { value: 3, done: false } myObj.next(); // { done: true }当done为true时你就知道迭代停止了。手动控制迭代的好处是你可以自由控制next()触发的时机与频率甚至提前终止——只要不再调用next()即可。这带来了比for...of更大的自由度。这一思想在仓库 设计模式/182.精读《设计模式 - Iterator 迭代器模式》.md 中有更系统的延伸迭代器实现时要考虑「外部控制迭代还是内部控制迭代」——类似 KOA 由插件调用next()控制迭代还是由外层统一控制迭代这直接决定了迭代器的 API 形态与职责边界。可迭代与 ArrayLike 的区别如果不了解迭代器可能会以为for...of是通过下标访问的从而把一个对象能否用obj[index]访问与它是否可迭代弄混。读过上面的介绍你应该理解到可迭代的唯一条件是实现[Symbol.iterator]而与对象是否是数组、或者 ArrayLike 没有关系。这两个概念正交可以组合出三种对象形态// 该对象可迭代不是 ArrayLike const range { from: 1, to: 5, }; range[Symbol.iterator] function () { // ... };// 该对象不可迭代是 ArrayLike const range { 0: a, 1: b, length: 2, };// 该对象可迭代是 ArrayLike const range { 0: a, 1: b, length: 2, }; range[Symbol.iterator] function () { // ... };三个例子分别对应三种形态可迭代但非 ArrayLike只有from/to属性没有数字下标和length但因实现了[Symbol.iterator]而可迭代ArrayLike 但不可迭代有数字下标0、1与length能用下标访问但不能被for...of遍历两者皆是既有下标与length又实现了迭代协议。顺带一提JS 的数组类型就是典型的既可迭代又属于 ArrayLike的类型——它既能按下标访问也原生实现了[Symbol.iterator]。需要留意的是ArrayLike 对象若要真正被for...of遍历仍需要补上[Symbol.iterator]实现而「可迭代但非 ArrayLike」的对象无法用下标访问只能通过迭代器消费。理解这组正交关系是写出正确数据转换代码的前提。可迭代的内置类型String、Array、TypedArray、Map、Set都支持迭代其表现为const myString abc; for (let val of myString) { console.log(val); } // a, b, c const myArr [a, b, c]; for (let val of myArr) { console.log(val); } // a, b, c const myMap [ [1, a], [2, b], [3, c], ]; for (let val of myMap) { console.log(val); } // [1, a], [2, b], [3, c] const mySet new Set([a, b, c]); for (let val of mySet) { console.log(val); } // a, b, c几个容易忽略的细节String的迭代按码元code unit输出因此可迭代遍历字符串的每个字符含 Unicode 字符的代理对拆分问题Map迭代时value是一个[key, value]二元组数组。在 设计模式/182.精读《设计模式 - Iterator 迭代器模式》.md 中可以看到更底层的等价操作Map实例的entries()返回的就是标准迭代器run.next()依次产出{value: [北京, 1], done: false}这类结构Set迭代顺序即插入顺序每个元素只出现一次。可迭代对象可以适用哪些 API可迭代对象首先支持上文提到的for...of语法。此外许多内置函数的入参都支持传入可迭代对象Map()、WeakMap()、Set()、WeakSet()、Promise.all()、Promise.allSettled()、Promise.race()、Promise.any()、Array.from()。Array.from可迭代 → 真数组以Array.from为例它可以将可迭代对象变成真正的数组。该数组的下标就是执行next()的次数值就是next().valueArray.from(new Set([1, 2, 3])); // [1, 2, 3]这一映射关系也印证了「独立定义」示例中value: this.current的设计next()每被调用一次就产生一个数组元素下标即调用次序。generator 也是迭代器generator也是迭代器的一种属于异步迭代器所以你甚至可以将yield一个generator函数作为上面这些内置函数的参数new Set( (function* () { yield 1; yield 2; yield 3; })() );结果为Set {1, 2, 3}。这与 设计模式/182.精读《设计模式 - Iterator 迭代器模式》.md 中的结论一致「generator天生为迭代器的 API」——调用run.next()即可按yield顺序拿到{value, done}无需关心内部存储结构。解构 / Spread 本质也是迭代器运算最后一种就是上周精读提到的 精读《Rest vs Spread 语法》——解构本质也是用迭代器进行运算的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 }; } }, }; [...range]; // [1, 2, 3, 4, 5]同一个range对象因为实现了迭代协议无需任何额外代码即可被展开为数组。这与 前沿技术/261.精读《Rest vs Spread 语法》.md 中[...mySet] // [a, b, c]、[...myMap] // [[a, 1], [b, 2], [c, 3]]的结论完全同源——凡是实现了迭代器的对象都可以用数组解构展开。该文还从反面印证了迭代协议的作用边界{...mySet}得到{}{...myMap}得到{}因为Map、Set不存在数字下标无法将迭代结果映射为对象的 Key——对象解构走的是属性枚举而非迭代协议这正好与本文「可迭代 ≠ ArrayLike」的区分形成互补迭代协议负责「按序取值」下标/属性枚举负责「按 Key 存取」。总结生活中我们可以数苹果的数量数大楼的窗户数杂乱的衣物有多少个——不同场景下这些对象的排列形式都不同甚至老师在黑板上写0~10我们按照这 4 个字符也能从 1 数到 10。这背后的原理抽象到程序里就是迭代器。一个对象黑盒不论内部怎么实现如果我们能按照顺序数出内部结构那么这个对象就是可迭代的。这就是[Symbol.iterator]定义要解决的问题把「按顺序产出元素」的能力抽象成统一协议让for...of、Array.from、new Set(...)、展开/解构等一切语言特性与内置 API 都能无差别地消费任何实现了该协议的对象。生活中与程序中都有一些默认的迭代器可以仔细领悟一下它们之间的关系内置可迭代类型String/Array/TypedArray/Map/Set自带迭代器是「默认迭代器」的典型自定义对象只需补上[Symbol.iterator]next()即可无缝接入整个语言生态generator以yield语法声明式地描述迭代序列是迭代协议最优雅的实现方式与 设计模式/182.精读《设计模式 - Iterator 迭代器模式》.md 结合看JS 的迭代器协议正是「行为型设计模式 Iterator」在语言层面的原生落地统一遍历 API、隐藏内部结构、支持外部控制迭代节奏。更进一步迭代器协议的组合能力远不止遍历本身迭代器可以和组合模式配合在组合结构内递归一个迭代器遍历完所有组合可以用工厂模式 多态实例化不同迭代器还可以与备忘录模式配合还原迭代器状态详见 设计模式/182.精读《设计模式 - Iterator 迭代器模式》.md 的补充章节。掌握迭代协议意味着你写出的每一份自定义数据结构都自动获得了整个 JS 标准库与语法糖的通行证。赞分享文档技术博客教程【免费下载链接】weekly前端精读周刊。帮你理解最前沿、实用的技术。项目地址https://gitcode.com/GitHub_Trending/we/weekly点击查看免费下载相关推荐前端精读周刊现代JavaScript异步编程模型详解前端精读周刊现代JavaScript异步编程模型详解 引言异步编程的痛点与解决方案 你是否曾被JavaScript中的回调地狱折磨得晕头转向或者在使用as文档技术博客教程前端精读周刊前端内存管理实践前端精读周刊前端内存管理实践 引言 你是否曾遇到过网页随着使用时间增长而变得越来越卡顿是否在开发复杂单页应用时明明代码逻辑没问题却总是出现莫名其妙的性能文档技术博客教程BurpSuiteHTTPSmuggler开发者指南源码解析与扩展开发教程BurpSuiteHTTPSmuggler开发者指南源码解析与扩展开发教程 BurpSuiteHTTPSmuggler是一款强大的Burp Suite扩展工具上一篇IDA Pro 地址区间管理ida_range 模块 range_t / rangeset_t 完全指南下一篇WorkshopDL终极指南如何免费下载Steam创意工坊模组无需安装Steam客户端创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表