ARTICLE DETAIL

资讯详情

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

现代 JavaScript 教程:深入理解可迭代对象(Iterable)与 Symbol.iterator

现代 JavaScript 教程:深入理解可迭代对象(Iterable)与 Symbol.iterator 文档教程前端【免费下载链接】zh.javascript.info现代 JavaScript 教程The Modern JavaScript Tutorial以最新的 ECMAScript 规范为基准通过简单但足够详细的内容为你讲解从基础到高阶的 JavaScript 相关知识。项目地址https://gitcode.com/gh_mirrors/zh/zh.javascript.info点击查看免费下载导读在 JavaScript 中for..of循环不仅能遍历数组还能遍历字符串、Map、Set以及任何实现了Symbol.iterator方法的对象。本文基于《现代 JavaScript 教程》06-iterable 章节从零构建一个可迭代的range对象开始系统讲解迭代协议Symbol.iterator与next()、字符串迭代与代理对surrogate pair处理、可迭代与类数组的区别以及Array.from的完整用法。读完本文你将掌握如何为自定义对象接入for..of理解Map/Set等内建集合的迭代机制并能在实战中正确选用迭代协议与数组转换方案。什么是可迭代对象可迭代Iterable对象是数组的泛化任何对象都可以被定制为可在for..of循环中使用的对象。数组是可迭代的但不仅仅是数组——很多内建对象也都是可迭代的例如字符串、Map、Set等。如果从严格意义上讲对象不是数组而是表示某物的集合列表、集合那么for..of就是一个能够遍历它的很好的语法。下面我们来一步步看如何让任意对象接入for..of。Symbol.iterator让任意对象可迭代自定义对象接入 for..of 的四个步骤通过自己创建一个对象我们就可以轻松地掌握可迭代的概念。例如我们有一个并非数组、但看上去很适合使用for..of循环的对象——一个代表数字区间的range对象let range { from: 1, to: 5 }; // 我们希望 for..of 这样运行 // for(let num of range) ... num1,2,3,4,5为了让range对象可迭代也就是让for..of可以运行我们需要为对象添加一个名为Symbol.iterator的方法——一个专门用于使对象可迭代的内建 symbol。Symbol.iterator是 JavaScript 众多“系统 symbol”之一在 对象基础知识 / Symbol 章节 的“系统 symbol”部分有完整列举它与Symbol.hasInstance、Symbol.toPrimitive等同属 ECMAScript 规范中的“众所周知的 symbolwell-known symbols”。for..of循环的工作机制可以拆解为四步当for..of循环启动时它会调用Symbol.iterator方法如果没找到就会报错。这个方法必须返回一个迭代器iterator——一个有next方法的对象。从此开始for..of仅适用于这个被返回的对象。当for..of循环希望取得下一个数值它就调用这个对象的next()方法。next()方法返回的结果的格式必须是{done: Boolean, value: any}当donetrue时表示循环结束否则value就是下一个值。这是带有注释的range的完整实现let range { from: 1, to: 5 }; // 1. for..of 调用首先会调用这个 range[Symbol.iterator] function() { // ……它返回迭代器对象iterator object // 2. 接下来for..of 仅与下面的迭代器对象一起工作要求它提供下一个值 return { current: this.from, last: this.to, // 3. next() 在 for..of 的每一轮循环迭代中被调用 next() { // 4. 它将会返回 {done:.., value :...} 格式的对象 if (this.current this.last) { return { done: false, value: this.current }; } else { return { done: true }; } } }; }; // 现在它可以运行了 for (let num of range) { alert(num); // 1, 然后是 2, 3, 4, 5 }关注点分离迭代器对象与被迭代对象请注意可迭代对象的核心功能关注点分离。range自身没有next()方法。相反是通过调用range[Symbol.iterator]()创建了另一个对象即所谓的“迭代器”对象并且它的next会为迭代生成值。因此迭代器对象和与其进行迭代的对象是分开的。从技术上说我们可以将它们合并并使用range自身作为迭代器来简化代码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 }; } } }; for (let num of range) { alert(num); // 1, 然后是 2, 3, 4, 5 }现在range[Symbol.iterator]()返回的是range对象自身它包括了必需的next()方法并通过this.current记忆了当前的迭代进程。这样更短。但缺点是现在不可能同时在对象上运行两个for..of循环了它们将共享迭代状态因为只有一个迭代器即对象本身。不过两个并行的for..of是很罕见的即使在异步情况下。无穷迭代器iterator无穷迭代器也是可能的。例如将range设置为range.to Infinity这时range则成为了无穷迭代器。或者我们可以创建一个可迭代对象它生成一个无穷伪随机数序列。next没有什么限制它可以返回越来越多的值这是正常的。当然迭代这种对象的for..of循环将不会停止但我们可以通过使用break来停止它。深入理解迭代协议在语言中的广泛应用迭代协议并非只服务于for..of。从仓库源码可以看到这一机制被语言中的多个特性复用generators 章节 直接引用了上述range实现指出可以通过提供一个 generator 函数作为Symbol.iterator来用 generator 进行迭代例如*[Symbol.iterator]() { for(let value this.from; value this.to; value) yield value; }——之所以能工作正是因为 generator 恰好具备.next()方法并以{value: ..., done: true/false}形式返回值。generator 被加入 JavaScript 语言时正是考虑了 iterator 的需求。Map and Set 章节 中map.keys()、map.values()、map.entries()返回的都是“可迭代对象”for..of可以直接遍历map默认等价于map.entries()并且Object.fromEntries(map)之所以可以省掉.entries()正是因为它“期望得到一个可迭代对象作为参数而不一定是数组”。扩展运算符与剩余参数章节 中...generateSequence()能将可迭代的 generator 对象转换为数组。可见掌握了迭代协议就等于掌握了for..of、展开运算符、Map/Set遍历、generator 迭代等一系列语言特性的共同底层。字符串是可迭代的数组和字符串是使用最广泛的内建可迭代对象。对于一个字符串for..of遍历它的每个字符for (let char of test) { // 触发 4 次每个字符一次 alert( char ); // t, then e, then s, then t }对于代理对surrogate pairs它也能正常工作这里的代理对指的是 UTF-16 的扩展字符let str ; for (let char of str) { alert( char ); // 然后是 }为什么这一点值得强调在 字符串章节的“内部Unicode / 代理对”小节 中有详细说明所有常用字符都是一个 2 字节的代码但 2 字节只允许 65536 种组合不足以表示每个可能的符号因此稀有符号emoji、罕见数学符号、罕见象形文字等会以“代理对”——一对 2 字节的符号——来编码。这意味着.length显示为2.length也是2直接按下标取[1]会得到代理对中的一块“垃圾信息”。而字符串迭代器是按“码点”感知代理对的所以for..of能正确地一个符号一个符号地输出。字符串的Symbol.iterator是内建实现的这正是它比简单的下标遍历更“安全”的原因。显式调用迭代器为了更深层地了解底层知识让我们来看看如何显式地使用迭代器。我们将会采用与for..of完全相同的方式遍历字符串但使用的是直接调用。这段代码创建了一个字符串迭代器并“手动”从中获取值let str Hello; // 和 for..of 做相同的事 // for (let char of str) alert(char); let iterator str[Symbol.iterator](); while (true) { let result iterator.next(); if (result.done) break; alert(result.value); // 一个接一个地输出字符 }很少需要我们这样做但是比for..of给了我们更多的控制权。例如我们可以拆分迭代过程迭代一部分然后停止做一些其他处理然后再恢复迭代。可迭代iterable和类数组array-like这两个官方术语看起来差不多但其实大不相同。请确保你能够充分理解它们的含义以免造成混淆。Iterable如上所述是实现了Symbol.iterator方法的对象。Array-like是有索引和length属性的对象所以它们看起来很像数组。当我们将 JavaScript 用于编写在浏览器或任何其他环境中的实际任务时我们可能会遇到可迭代对象或类数组对象或两者兼有。例如字符串既是可迭代的for..of对它们有效又是类数组的它们有数值索引和length属性。但是一个可迭代对象也许不是类数组对象反之亦然类数组对象可能不可迭代。例如上面例子中的range是可迭代的但并非类数组对象因为它没有索引属性也没有length属性。下面这个对象则是类数组的但是不可迭代let arrayLike { // 有索引和 length 属性 类数组对象 0: Hello, 1: World, length: 2 }; // Error (no Symbol.iterator) for (let item of arrayLike) {}可迭代对象和类数组对象通常都不是数组它们没有push和pop等方法。如果我们有一个这样的对象并想像数组那样操作它那就非常不方便。例如我们想使用数组方法操作range应该如何实现呢Array.from把可迭代对象和类数组转成真正的数组有一个全局方法Array.from可以接受一个可迭代或类数组的值并从中获取一个“真正的”数组。然后我们就可以对其调用数组方法了。例如let arrayLike { 0: Hello, 1: World, length: 2 }; let arr Array.from(arrayLike); // (*) alert(arr.pop()); // Worldpop 方法有效在(*)行的Array.from方法接受对象检查它是一个可迭代对象或类数组对象然后创建一个新数组并将该对象的所有元素复制到这个新数组。如果是可迭代对象也是同样// 假设 range 来自上文的例子中 let arr Array.from(range); alert(arr); // 1,2,3,4,5 数组的 toString 转化方法生效完整语法与映射函数Array.from的完整语法允许我们提供一个可选的“映射mapping”函数Array.from(obj[, mapFn, thisArg])可选的第二个参数mapFn可以是一个函数该函数会在对象中的元素被添加到数组前被应用于每个元素此外thisArg允许我们为该函数设置this。例如// 假设 range 来自上文例子中 // 求每个数的平方 let arr Array.from(range, num num * num); alert(arr); // 1,4,9,16,25基于 Array.from 的代理感知字符串处理现在我们用Array.from将一个字符串转换为单个字符的数组let str ; // 将 str 拆分为字符数组 let chars Array.from(str); alert(chars[0]); // alert(chars[1]); // alert(chars.length); // 2与str.split方法不同它依赖于字符串的可迭代特性。因此就像for..of一样可以正确地处理代理对即 UTF-16 扩展字符。技术上来讲它和下面这段代码做的是相同的事let str ; let chars []; // Array.from 内部执行相同的循环 for (let char of str) { chars.push(char); } alert(chars);……但Array.from精简很多。我们甚至可以基于Array.from创建代理感知surrogate-aware的slice方法也就是能够处理 UTF-16 扩展字符的slice方法function slice(str, start, end) { return Array.from(str).slice(start, end).join(); } let str ; alert( slice(str, 1, 3) ); // // 原生方法不支持识别代理对即 UTF-16 扩展字符 alert( str.slice(1, 3) ); // 乱码两个不同 UTF-16 扩展字符碎片拼接的结果注意字符串内建的slice方法按 UTF-16 码元code unit切分遇到代理对会把符号从中间截断产生乱码而基于Array.from的实现先按码点拆成真正符号的数组再切片拼接因此能正确处理 emoji 等扩展字符。这与 字符串章节 中str.codePointAt/String.fromCodePoint的说明互相印证处理代理对需要码点code point级别的手段而字符串迭代器恰好提供了这一能力。Array.from 在集合转换中的实战价值Array.from在仓库其他章节中也频繁出现作为“可迭代对象 → 真正数组”的通用桥梁。例如在 Map and Set 章节 中Object.fromEntries与Array.from互补Object.entries(obj)将普通对象转为[key, value]数组这正是Map构造函数需要的格式而Object.fromEntries接收可迭代的键值对并还原为普通对象。理解Array.from的可迭代分支有助于你推演new Map(iterable)、Object.fromEntries(iterable)等一系列基于迭代协议的 API 的数据流。总结可以应用for..of的对象被称为可迭代的。技术上来说可迭代对象必须实现Symbol.iterator方法。obj[Symbol.iterator]()的结果被称为迭代器iterator由它处理进一步的迭代过程。一个迭代器必须有next()方法它返回一个{done: Boolean, value: any}对象这里done:true表明迭代结束否则value就是下一个值。Symbol.iterator方法会被for..of自动调用但我们也可以直接调用它。内建的可迭代对象例如字符串和数组都实现了Symbol.iterator。字符串迭代器能够识别代理对surrogate pair即 UTF-16 扩展字符。有索引属性和length属性的对象被称为类数组对象。这种对象可能还具有其他属性和方法但是没有数组的内建方法。如果我们仔细研究一下规范——就会发现大多数内建方法都假设它们需要处理的是可迭代对象或者类数组对象而不是“真正的”数组因为这样抽象度更高。Array.from(obj[, mapFn, thisArg])将可迭代对象或类数组对象obj转化为真正的数组Array然后我们就可以对它应用数组的方法。可选参数mapFn和thisArg允许我们将函数应用到每个元素。延伸阅读仓库内相关章节Symbol 章节系统 symbol 与 Symbol.iterator 的定位字符串章节代理对与码点Map and Set 章节keys/values/entries 的可迭代返回与 Object.fromEntriesGenerators 章节用 generator 实现 Symbol.iterator赞分享文档教程前端【免费下载链接】zh.javascript.info现代 JavaScript 教程The Modern JavaScript Tutorial以最新的 ECMAScript 规范为基准通过简单但足够详细的内容为你讲解从基础到高阶的 JavaScript 相关知识。项目地址https://gitcode.com/gh_mirrors/zh/zh.javascript.info点击查看免费下载相关推荐JavaScript 可迭代对象(Iterable)深度解析JavaScript 可迭代对象 Iterable 深度解析 什么是可迭代对象 可迭代对象是 JavaScript 中一个重要的概念它扩展了数组的特性使得文档教程前端深入理解 JavaScript 可迭代对象IterablesSymbol.iterator、for..of 与 Array.from 实战指南深入理解 JavaScript 可迭代对象Iterables Symbol.iterator 、 for..of 与 Array.from 实战指南 本文文档/教程前端wxBot微信机器人多账号管理终极方案三步实现高效自动化运营wxBot微信机器人多账号管理终极方案三步实现高效自动化运营 你是否在为管理多个微信账号而烦恼想要实现自动化回复、批量消息发送却苦于技术门槛太高wxBo即时通讯AI 应用创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表