资讯详情

资讯详情

深入ES6迭代器:协议、生成器与统一遍历实战

如果你写过一段时间 JavaScript大概率经历过这种场面数据源五花八门数组、普通对象、Map、Set还有诡异的类数组对象每一种都有自己的循环方式而真正能统一回答怎么遍历的是 ES6 的迭代器Iterator。迭代器不是某个函数而是一套协议对象通过 Symbol.iterator 暴露遍历入口for...of、展开、解构这些日常语法都建立在它之上。这篇文章不打算只讲概念我会从协议、手写迭代器、生成器、真实落地场景几个角度把它彻底拆开。我在做一个数据看板项目时后台返回的数据五花八门主体列表是数组配置项是普通对象权限标签是 Set指标维度是 Map还有一些老接口吐出来的 arguments 和类数组对象。遍历逻辑写到最后每个数据结构都在自己的循环体系里打转——数组用 for 和 forEach对象用 for...inSet 和 Map 用自带的 forEach类数组得先 Array.prototype.slice 转一下才能进 forEach。一段简单的把所有 key 拼成字符串的逻辑因为数据来源不同硬生生写了五个分支。这种混乱不是代码风格能救的。for...in 遍历对象会带上原型链上可枚举的属性for...of 当时还不能直接用在普通对象上各种循环之间没有统一的约定。我需要的是一个不管你给我什么数据结构我都能用同一种方式把它遍历完的能力。ES6 迭代器Iterator干的就是这件事。迭代器不是什么新概念它是一套已经存在了很久的编程范式抽象出逐项访问集合元素的行为把遍历逻辑和数据存储解耦。ES6 把它正式纳入了语言规范并且让它成为 for...of、展开运算符、解构赋值这些语法特性的底层支撑。理解迭代器你才能真正明白 for...of 为什么能遍历数组却不能遍历普通对象也才能写出让对象、让自定义类天然支持统一遍历的代码。这篇文章适合两类人一类是 JavaScript 基础已经过关、但每次看到 Symbol.iterator 和 function* 就头大的开发者另一类是觉得自己会用 for...of 就够了的务实派——知道怎么用是不够的你还得知道它为什么能用以及当标准行为满足不了业务时怎么造出属于自己的迭代器。读完你至少能回答三个问题迭代器和可迭代对象到底有什么区别生成器函数为什么是迭代器的最佳搭档项目里什么场景值得亲手实现一个迭代器1. 为什么需要迭代器从一次数据遍历的狼狈现场说起故事背景我不想再重复了直接从结论切入JavaScript 在 ES6 之前遍历行为是严重分裂的。这种分裂不是某个库设计失误而是语言本身长期缺少一个数据结构的统一遍历协议。迭代器的出现本质上就是给语言补上了这块地基。1.1 遍历方案四分五裂的时代先看看迭代器出现之前JavaScript 的遍历世界是什么样。数组是老大自带全套工具for 循环靠下标forEach 靠回调还有 map、filter、reduce 这一整条链。普通对象走另一套逻辑for...in 或者 Object.keys().forEach。Map 和 Set 加入之后文档里推荐你直接用它们自带的 forEach 方法。再到函数内的 arguments既不是数组也没有 Symbol.iterator旧环境只能靠下标循环。问题在什么地方没有统一的协议。每种数据结构自己定义一个遍历入口调用方式还不一样。我那个看板项目里的实际代码大概长这样function collectAllKeys(data) { const keys []; if (Array.isArray(data)) { data.forEach(function (item) { keys.push(JSON.stringify(item)); }); } else if (data instanceof Map) { data.forEach(function (value, key) { keys.push(key); }); } else if (data instanceof Set) { data.forEach(function (value) { keys.push(value); }); } else { for (const k in data) { keys.push(k); } } return keys; }这段代码每个分支都在做同一件事——把数据里的值挨个拿出来却被迫写成了各不相同的五种写法。更麻烦的是这个函数接收的 data 一旦是某种新型数据结构又得加分支。写业务还好写底层工具库就痛苦了——想把一个遍历逻辑抽象出来复用你面对的不是一个接口而是无数个名字不同的接口。1.2 迭代器给出的统一答案迭代器的解决方案很朴素想被统一遍历就先过一道加工——调用数据的 Symbol.iterator 方法得到一个迭代器对象然后反复调用迭代器的 next 方法每次拿到一个值。所有数据结构只要愿意实现这个 Symbol.iterator 和 next 约定就能被 for...of 等语法统一消费。核心价值有三点第一解耦。数据结构只负责产出下一个值外部怎么消费它不关心消费方只负责拿到一个值就处理一个不用知道数据存在数组里还是链表里还是延迟生成的。第二惰性。迭代器的 next 是你想要才取一个天然支持无限序列和按需计算。数组是全部数据都躺在内存里迭代器可以是现算现卖。第三统一。for...of、展开、解构等只要遇到可迭代对象就可以工作不需要针对每种类型写分支。我用一个生活化的类比再压一遍数组像超市里已经装好的购物车你不知道里面有什么但你知道从头推到尾就能看完迭代器像传送带你不按按钮它就不往前送而 Symbol.iterator 就是传送带的开关位置所有传送带都统一把开关装在同一个位置。你只要找这个开关就能驱动任何一条传送带。好理论说多了容易飘。下一节我们直接看协议本尊这是整个迭代器体系的地基。2. 迭代器协议与可迭代协议先把底层约定讲透很多教程喜欢把迭代器和可迭代对象混着讲结果就是代码照抄能跑但改一点就报错。这两个其实是两个不同的协议一个负责定义什么样的对象能遍历一个负责定义遍历的过程长什么样。2.1 两个容易混淆的协议可迭代协议Iterable Protocol说的是一个对象只要实现了 [Symbol.iterator] 方法并且这个方法返回一个迭代器它就是可迭代的iterable。注意判断标准不是它有 next而是它有 Symbol.iterator 通道。迭代器协议Iterator Protocol约束的是迭代器本身迭代器必须实现 next 方法next 每次返回一个对象形如 { value: 具体值, done: false }当没有更多值的时候返回 { value: undefined, done: true }。这是个拉模型——每调用一次 next就把下一个值拉出来。用比喻来说可迭代对象是一摞扑克牌Symbol.iterator 是发牌员迭代器是发牌员准备好开始发牌这件事next 就是一次发牌动作。发牌员不是牌本身但有了发牌员你才能一张一张取牌。举个例子数组是一个可迭代对象因为 arr Symbol.iterator 能返回一个数组迭代器。数组迭代器本身也是可迭代的它内部的 Symbol.iterator 返回自身所以你可以把迭代器直接放进 for...of 再遍历一次也可以手动逐个调用 next。这种迭代器自己也可迭代的设计不是巧合是为了让消费方拿到的永远是可迭代的东西用起来更顺手。2.2 for...of 背后到底做了什么当你在代码里写 for (const item of arr)引擎做的事大致是拿到 arr 的迭代器const iterator arr Symbol.iterator循环调用 iterator.next()每次得到一个 result判断 result.done如果 done 为 true退出否则把 result.value 赋给 item执行循环体过程简单但有几个细节值得记住迭代器只会被获取一次每次 next 只会前进不会后退done 为 true 之后再调用 next 返回什么规范没有强制约束常见实现是继续返回 { value: undefined, done: true }。所以你在写自定义迭代器时别在 done 之后又憋出一个值来这不符合约定。还有一个容易忽略的点for...of 拿到迭代器之后如果循环体里遇到 break、return、throw引擎会尝试调用迭代器的 return 方法如果存在把迭代器关闭。这个 return 方法属于迭代器协议的一个可选部分很多业务代码用不到但我在后面会专门讲因为它在资源清理场景里非常有用。2.3 手动调用迭代器的过程演示你可以完全不用 for...of手动把遍历拆开彻底看清协议长什么样const arr [A, B, C]; const iterator arr[Symbol.iterator](); console.log(iterator.next()); // { value: A, done: false } console.log(iterator.next()); // { value: B, done: false } console.log(iterator.next()); // { value: C, done: false } console.log(iterator.next()); // { value: undefined, done: true } console.log(iterator.next()); // { value: undefined, done: true }看出来了吗迭代器是一次性的游标它不会自己复位。如果你需要重新遍历必须重新调用 Symbol.iterator 拿一个新的迭代器。还有个有趣的地方[Symbol.iterator] 这个名字前面是中括号因为 Symbol.iterator 是一个 symbol 类型的 well-known symbol不是字符串属性名。所以你在 console 里打印对象属性时它不会平平无奇地把 iterator 当普通 key 列出来得用 Reflect.ownKeys 才能看到。理解了这两个协议接下来就可以动手写自己的迭代器了。这才是这篇文章值钱的部分。3. 手写一个迭代器从普通对象到任意数据结构的跨越协议看懂了但直接上手写迭代器的人不多因为平时用不到。真正需要用到的场景往往是你有一个自定义的数据结构希望它像数组一样能被 for...of、展开、解构你有一段遍历逻辑比较特殊希望把如何取下一个值封装起来复用。这两种场景都需要手写迭代器。3.1 让普通对象变得可迭代普通对象默认不是可迭代的所以 for...of 直接对一个普通对象用会抛 TypeError: object is not iterable。这个限制让很多人疑惑过为什么数组能、Map 能、普通对象就不能因为规范没有给普通对象提供默认的 Symbol.iterator——毕竟对象字段本身没有明确顺序。但你可以自己加。比如我想让一个用户列表对象可以被直接遍历遍历得到每个用户的 nameconst userGroup { users: [ { id: 1, name: A }, { id: 2, name: B }, { id: 3, name: C } ], [Symbol.iterator]() { let index 0; const users this.users; return { next() { if (index users.length) { return { value: undefined, done: true }; } return { value: users[index].name, done: false }; } }; } }; for (const name of userGroup) { console.log(name); // A B C }看到没有协议就是这么直接。一个对象把自己的 Symbol.iterator 注册进去返回一个携带闭包变量 index 的迭代器。这个写法有个小讲究迭代器里用闭包记下标比用 this 记状态稳妥。因为说不定有人会把 next 解构出去单独调用此时 this 指向就乱了。3.2 带业务逻辑的迭代器真实业务里迭代器不会只是数组下标加一往往承载真正的遍历规则。比如我做过一个日志聚合模块日志不是一次性数组而是从多个来源分批取来源之间还有优先级顺序。让每次 next 都去各来源取一条直到所有来源耗尽这就是一个典型的自定义迭代器。再举一个容易理解的例子做一个逢奇数才输出的迭代器它遍历一组数据但只有满足条件的值才会出现在 next 的 value 里。这个逻辑用纯 for 循环当然也能写但迭代器的好处是消费方根本不关心规则它只管 next。你可以在多个业务场景里复用同一套遍历规则。function createOddIterator(list) { let index 0; return { next() { while (index list.length) { const value list[index]; if (value % 2 1) { return { value, done: false }; } } return { value: undefined, done: true }; } }; }这种跳过型迭代器next 一次可能内部循环了好几轮但对消费者来说它只是给我下一个值。这种封装带来的代码整洁度提升只有写过复杂遍历逻辑的人能体会。3.3 提前终止时的 return() 处理协议里 next 是必须的但 return 和 throw 是可选的。什么时候用到 return当消费者提前退出而迭代器还没有遍历完。典型场景是 for...of 里 break或者解构只取前两个值。如果你的迭代器持有外部资源比如打开的文件句柄、数据库游标、网络连接提前退出时不清理资源就泄漏了。实现 return 方法就可以捕获这种提前退出function createFileLineIterator(file) { return { next() { // 假装从文件读一行 }, return() { file.close(); return { value: undefined, done: true }; } }; }我自己的习惯是只要迭代器内部有打开的资源或者需要善后的状态一定写 return不写给 review 的人增加隐患。注意 return 必须返回 { done: true } 这样的对象不然 for...of 的退出逻辑会出问题。实际上在 next 的坑之外还有一个很多人问过的点为什么 Symbol.iterator 的 key 会被贴到对象上作为属性因为 for...of 访问 iterable 的迭代器确实就是从属性上读出来的——可迭代协议就是靠这个属性完成的。所以当别人问对象怎么变成可迭代的答案就是给它装一个 Symbol.iterator 属性属性值是一个返回迭代器的函数没有黑魔法。手写迭代器逻辑清晰、完全可控但代码量不小尤其是状态一多闭包都不够用。所以 ES6 还给了另一把更顺手的工具生成器函数。下一节展开看它如何把迭代器代码大幅精简。4. 生成器函数写迭代器的最佳姿势生成器函数Generator Function是我认为 ES6 里性价比最高的特性之一。它的语法直观到有点反直觉一个普通函数声明加个星号函数体里用 yield 暂停执行调用它不直接执行函数体而是返回一个迭代器。对这个迭代器调用 next函数体会一直执行到下一个 yield把 yield 后面的表达式作为 value 返回然后挂起。4.1 生成器函数从哪里来从语言设计角度看生成器就是给手写迭代器提供的一个语法糖工厂你描述我想要按什么规则产出值生成器把晦涩的 next、闭包、状态切换全部包起来。你不需要自己维护 index不需要自己组装 { value, done } 对象引擎帮你做。打个比方手写迭代器就像手动挡开车每个操作都自己控制生成器就像自动挡你只要告诉它到这儿停一下剩下的换挡交给机器。性能上手动挡理论上更精确但开发效率和可读性自动挡明显胜出。拿上一节的 userGroup 例子改造一下感受差异const userGroup { users: [ { id: 1, name: A }, { id: 2, name: B }, { id: 3, name: C } ], *[Symbol.iterator]() { for (const user of this.users) { yield user.name; } } }; for (const name of userGroup) { console.log(name); // A B C }同样的业务代码少了三分之二而且 next 和对象字面量不用写了。这个星号写在方括号前面是合法语法表示这是一个生成器方法它作为 Symbol.iterator 的返回值正好符合迭代器协议。生成器函数返回的对象天然是迭代器也天然是可迭代对象。4.2 从 yield 行为看生成器执行机制生成器的核心操作是 yield。yield 一词直译是让步——函数执行到这里把控制权交出去等 next 再进来就接着走。关键细节在于 yield 两侧的数据流向next 调用传入的参数会被上一个 yield 表达式的结果接收yield 后面表达式的结果会被 next 返回。我举个例子function* counter() { const a yield first yield value; console.log(got from next:, a); const b yield second yield value; console.log(got from next:, b); return finished; } const it counter(); console.log(it.next()); // { value: first yield value, done: false } console.log(it.next(hello)); // 函数内部打印 got from next: hello然后返回 { value: second yield value, done: false } console.log(it.next(world)); // 打印 got from next: world返回 { value: finished, done: true }这个双向通信能力是生成器区别于普通迭代器的一个重要特性。普通迭代器的 next 是无参的生成器的 next 则可以往里传数据。这种特性在消费者需要给生产者反馈的场景非常有用——比如一个按需分页的迭代器消费者可以告诉它我这一页只看这么多下一页跳过去。还有一个点值得强调return 语句在生成器里值是可以通过 next 拿到的但 done 为 true 的时候value 通常是收尾值。常规业务不取这个值但如果你写协议代码要认真看。4.3 yield* 委托与递归场景如果只是把数组循环一遍生成器还不算惊艳。真正惊艳的是 yield*它可以把另一个可迭代对象整个地委托给当前生成器。遇到 yield* 表达式引擎会自动遍历后面的可迭代对象把里面的每个值逐个 yield 出来直到遍历完才继续执行当前生成器的后续代码。function* wrappedIterable(listOfLists) { for (const subList of listOfLists) { yield* subList; } } const result []; for (const value of wrappedIterable([[1, 2], [3], [4, 5]])) { result.push(value); } console.log(result); // [1, 2, 3, 4, 5]yield* 到递归场景里价值更大。比如遍历一棵树用普通迭代器写递归很痛苦因为 next 的调用机制会把调用栈打散。但生成器配合 yield*可以写出接近同步代码的递归遍历function* walkTree(node) { yield node.value; if (node.children) { for (const child of node.children) { yield* walkTree(child); } } }这个函数在语义上就是先输出当前节点再递归输出子树但因为改成了生成器外部可以按需地逐个取值。我在做目录树组件时用这个模式处理按需加载子节点的需求遍历逻辑从原来的命令式循环变成了声明式描述后面维护的人看代码一眼就能懂。生成器把迭代器的实现成本压到了最低所以现代 JavaScript 里几乎所有需要自定义迭代的地方我都会用生成器而不是手写对象。但迭代器本身还有更广的覆盖面哪些内置对象已经自带它们怎么被消费下一节聊。5. 内置可迭代对象盘点与各种消费方式ES6 引入迭代器之后语言内置了一大批可迭代对象。这个盘点很重要因为日常代码里你不知不觉就在用它们只是没意识到哦原来它走的就是 [Symbol.iterator] 这条路。5.1 哪些内置对象默认可迭代直接说结论默认实现了可迭代协议的对象包括ArrayStringMapSetTypedArray比如 Uint8Arrayarguments 对象DOM 的 NodeList现代浏览器生成器对象普通对象不在其中。这个设计不是疏忽是刻意的数组、字符串、Map、Set 都有明确且公认的遍历顺序普通对象的键顺序在不同语义下是有争议的虽然有插入序规则但不是所有遍历都默认按这个语义。需要遍历普通对象时标准做法是 Object.keys / Object.values / Object.entries 返回数组再进行迭代。每个内置对象的迭代行为也各不相同记一个表更容易对象每次 next 返回的 value说明Array数组元素会遍历稀疏数组的洞值为 undefinedString字符串的一个字符按码点code point遍历不是按 UTF-16 码元Map[key, value] 数组按插入顺序Set元素值按插入顺序arguments实参值类数组但真的是可迭代对象NodeListDOM 节点现代浏览器都支持表格里有一个细节值得单独唠一唠就是稀疏数组差异传统 for 循环按下标访问稀疏数组的洞读到的值是 undefinedforEach 会跳过洞for...of 则会把洞位置同样产出 undefined。这个差异在数据分析场景会让人很困惑我是真踩过。5.2 字符串迭代的码点特性字符串是我工作中最容易踩坑的可迭代对象之一。ES6 之前str[i] 访问的是 UTF-16 码元遇到表情符号这类代理对字符会碎成两半。ES6 字符串迭代是按码点code point进行的也就是说 for...of 一个字符串能正确产出完整的四字节字符。const s abc; console.log(s.length); // 5因为 占两个 UTF-16 码元 for (const ch of s) { console.log(ch); // a b c 没问题 } console.log(s[3], s[4]); // 高代理位和低代理位碎开了这个特性在处理用户输入、富文本、国际化文本时有实际意义。用 for...of 遍历来统计用户看到的字符数比 length 准确。但注意按码点遍历和按字素遍历还不一样组合字符比如一个字符加一个变音符号仍可能被拆开真要完整处理用户感知的字符得用 Intl.Segmenter。5.3 解构、展开运算、Array.from 等消费场景迭代器不仅驱动 for...of还驱动了好几个看起来八竿子打不着的语法和 API。理解这一点很多为什么这个功能能用在 Set 上的疑惑会瞬间消失。展开运算符和解构赋值在遇到可迭代对象时会调用它的 Symbol.iterator。比如const myMap new Map([[name, A], [age, 30]]); const arr [...myMap]; console.log(arr); // [[name, A], [age, 30]] const [key, ...rest] [10, 20, 30, 40]; console.log(key); // 10 console.log(rest); // [20, 30, 40]展开 Set、展开字符串也有对应的行为展开字符串会得到码点数组这对把带 emoji 的字符串转成数组特别有用[...str] 比 str.split() 更安全。Array.from 也接受可迭代对象和类数组对象它会一口气把整个迭代器里的值全部装入数组function sumArgs() { return Array.from(arguments).reduce((sum, n) sum n, 0); } console.log(sumArgs(1, 2, 3)); // 6还有 Promise.all 和 Promise.race 接受的是可迭代对象这在接收动态 Promise 列表时很常见。为什么要强调消费场景因为你在写自己的迭代器时只要实现了协议这些语法和 API 通通能用零成本。你做的自定义数据结构不会只是为了让 for...of 能跑它还能被展开、被解构、被 Array.from 接管。这是协议统一的巨大红利。6. 迭代器在真实项目里的落地场景、坑位与取舍讲到这儿协议和语法都清楚了。但会写和会在项目里用对之间还隔着一段距离。这段距离主要是两类东西一是值得上迭代器的真实场景二是会让代码悄悄出 bug 的边缘情况。6.1 无限序列与惰性求值的正确打开方式数组天生是有限的你造不出无限长度的数组真要硬造会爆内存。但迭代器可以无限因为它是惰性的你不调用 next它就不产生下一个值。这给概念上无限的数据流提供了一个安全的描述方式。经典的斐波那契序列用生成器写出来几乎和数学定义一样直观function* fibonacci() { let a 0, b 1; while (true) { yield a; [a, b] [b, a b]; } } const gen fibonacci(); // 取前 8 个 console.log(Array.from({ length: 8 }, () gen.next().value)); // [0, 1, 1, 2, 3, 5, 8, 13] // 或者用 for...of 手动 break let count 0; for (const n of fibonacci()) { if (count 10) break; console.log(n); }这种方式的大优势是内存稳定不管概念上序列多长内存里永远只保存当前协程状态和当前值。我之前处理一个时间序列数据流上游会一直推数据但页面只想展示最近 N 条就是靠一个封装好的迭代器过滤加截断内存占用保持在常量级。注意无限生成器配合 for...of 时如果忘了 break 或者循环条件恒真会变成死循环这是无限序列与循环语法天生的兼容性风险写的时候务必留一个明确的退出条件。6.2 性能迭代器和生成器并不是零开销的聊到性能很多人会问用 for...of 替代传统 for 循环会不会慢很多实测下来for...of 遍历数组通常比 for 下标循环慢那么一点生成器则更慢一些——每次 next 都是一次函数调用yield 挂起恢复也有额外成本。这个差距在几百万次循环的规模下能明显感受到平常几百个元素的列表根本无感。我的取舍逻辑很简单业务数据遍历用 for...of / 迭代器因为可读性和统一性好热路径、高频执行、对性能敏感的数值计算老老实实写 for 循环。别拿迭代器去优化性能它是为表达力和抽象服务的不是性能银弹。还有一个隐藏开销容易忽略生成器对象是活的协程不能像普通函数一样被垃圾回收得那么利索。如果一个生成器长期挂在全局作用域并且一直 yield 不断执行完它所占的状态就一直活着。用完及时让它结束或者调用 return 关闭能帮助引擎早点回收。6.3 一个真实的分页数据集案例我最近写一个报表导出功能后端接口是分页的一次只能拉 1000 条总共可能有几十万条。早期版本的做法是把所有页全部拉完拼成一个大数组再交给导出模块。结果前端内存暴涨接口一个接一个串行也慢得不行。后来我把分页拉取封装成生成器导出模块不需要知道分页细节它只需要逐个消费记录async function* paginateAll(fetcher, pageSize 1000) { let page 1; while (true) { const { records, hasMore } await fetcher(page, pageSize); if (records.length 0 || !hasMore) { return; } yield* records; page; } } // 消费方 const generator paginateAll(fetchPage); let total 0; for await (const record of generator) { // 每拿到一条就写入导出流 total; }这个改造的收益内存从攒完所有页变成只保留当前页和当前记录配合 for await...of 语法每页数据拉完函数在 yield 处挂起下一页的请求是在消费方请求下一个值时才发起。这种按需拉取模式在报表、消息推送、数据同步代码里非常常见。如果你还在用一次性拉完再处理的模式处理大列表强烈建议试一下这种写法。这里用到 for await...of 是异步迭代器的场景属于迭代器家族在异步世界的延伸原理和同步一致但在项目中很实用就一起提了。6.4 我踩过的几个坑最后把这几年实际踩过的坑盘点一下都是真实发生的第一以为迭代器可以被重新遍历。迭代器是游标不是副本——同一个迭代器 next 到底就结束了想再来一遍必须重新拿。尤其是生成器一次生成的对象只能完整遍历一次。如果团队里有同事把生成器当作数组到处传很快就会出现第一次 for...of 有数据第二个循环没数据的 bug。解决方案是要么传生成器函数而不是生成器结果要么每次消费方都用 Symbol.iterator 现取。第二遍历时修改原集合。迭代器是有状态的在 next 过程中往数组 push、从 Set 删除元素行为没有统一保证不同引擎实现可能不同。最常见的是 for...of 数组时在里面 splice 删除当前项你会发现索引直接跳过去了漏掉后面一个元素。正确的做法是先收集要删除的、遍历结束后再统一处理或者用数组的 filter 从数据源层面解决。第三生成器里用 try/finally 不写 return。finally 在生成器被 return() 关闭时会执行但不显式调用 return 或等它走完finally 不一定执行。如果你在生成器里打开了资源比如数据库连接又用 break 提前跳出消费资源清理完全依赖 finally 的执行时机那就要小心了。第四对象不是可迭代对象但很多人默认它是。本来想让 for...of 遍历一个普通对象结果报错别忘了转成 Object.entries(obj) 再迭代。这个错我已经在 code review 里拦过好几次了。踩坑之后我想明白一件事迭代器不是一个需要多炫的技能它更像一套基础协议用得好的人不会觉得惊艳但代码会明显更清爽。项目里凡是有逐项产出、按需消费、可能提前终止这些特征的逻辑我都会下意识想想这里是不是可以用迭代器。大多数时候答案是肯定的然后我会选择生成器。以一个个人体会收个尾迭代器这玩意儿不太适合一口气背完它更适合在日常生活里慢慢渗透——先意识到 for...of 可以遍历哪些东西再意识到背后的协议统一最后才是自己动手造迭代器。等你哪一天写代码时不再关心这个集合类型支持哪种遍历而是直接扔给 for...of 跑你就算是真正吃透它了。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →