八: ES6 迭代器 生成器

前言 该部分为书籍 深入理解ES6 第八章(迭代器与生成器)笔记 循环的问题 var colors = ["red", "green", "blue"]; for (var i = 0, len = colors.length; i < len; i++) { console.log(colors[i]); } 虽然这个循环非常直观, 然而当它被嵌套使用并要追踪多个变量时, 情况就会变得非常复杂. 额外的复杂度会引发错误, 而 for 循环的样板性也增加了自身出错的可能性, 因为相似的代码会被写在多个地方 何为迭代器? ES6 中的迭代器是被设计专用于迭代的对象, 带有特定接口. 其规则为: 所有的迭代器对象都拥有 next() 方法, 会返回一个结果对象, 这个结果对象有两个属性: 对应下一个值的 value, 以及一个布尔类型的 done, 其值为 true 时表示没有更多值可供使用. 迭代器持有一个指向集合位置的内部指针, 每当调用了 next() 方法, 迭代器就会返回相应的 下一个值 // 在 ES5 中创建一个 类迭代器 function createIterator(items) { var i = 0; return { next: function() { var done = (i >= items.length); var value = !done ? items[i++] : undefined; return { done: done, value: value }; } }; } var iterator = createIterator([1, 2, 3]); console.log(iterator.next()); // "{ value: 1, done: false }" console.log(iterator.next()); // "{ value: 2, done: false }" console.log(iterator.next()); // "{ value: 3, done: false }" console.log(iterator.next()); // "{ value: undefined, done: true }" // 之后的所有调用 console.log(iterator.next()); // "{ value: undefined, done: true }" 何为生成器? 生成器是能返回一个迭代器的函数, 生成器函数由放在 function 关键字之后的一个星号(*)来表示, 并能使用新的 yield 关键字. 星号紧跟在 function 关键字之后, 或是在中间留出空格, 都是没问题的 **生成器函数最有意思的方面可能就是它们会在每个 yield 语句后停止执行. ** yield 关键字可以和值或是表达式一起使用 // 在次函数内, for 循环在循环执行时从数组中返回元素给迭代器. 每当遇到 yield, 循环就会停止; 而每当 iterator 上的 next() 方法被调用, 循环就会再次执行到 yield 语句处. function *createIterator(items) { for (let i = 0; i < items.length; i++) { yield items[i]; } } let iterator = createIterator([1, 2, 3]); console.log(iterator.next()); // "{ value: 1, done: false }" console.log(iterator.next()); // "{ value: 2, done: false }" console.log(iterator.next()); // "{ value: 3, done: false }" console.log(iterator.next()); // "{ value: undefined, done: true }" // 之后的所有调用 console.log(iterator.next()); // "{ value: undefined, done: true }" yield 关键字只能用在生成器内部, 用于其他任意位置都是语法错误 , 即使在生成器内部的函数也不行 function *createIterator(items) { items.forEach(function(item) { // 语法错误 yield item + 1; }); } 生成器函数是 ES6 的一个重要特性, 并且因为它就是函数, 就能被用于所有可用函数的位置 1. 生成器函数表达式 **语法: *在 function 关键字与圆括号之间使用一个星号()即可. 注意: 箭头函数不能为生成器 let createIterator = function *(items) { for (let i = 0; i < items.length; i++) { yield items[i]; } }; let iterator = createIterator([1, 2, 3]); console.log(iterator.next()); // "{ value: 1, done: false }" console.log(iterator.next()); // "{ value: 2, done: false }" console.log(iterator.next()); // "{ value: 3, done: false }" console.log(iterator.next()); // "{ value: undefined, done: true }" // 之后的所有调用 console.log(iterator.next()); // "{ value: undefined, done: true }" 2. 生成器对象方法 生成器就是函数, 因此可以被添加到对象中 var o = { createIterator: function *(items) { for (let i = 0; i < items.length; i++) { yield items[i]; } } }; // ES6 的速记法 var o = { *createIterator(items) { for (let i = 0; i < items.length; i++) { yield items[i]; } } }; 可迭代对象与for-of循环 与迭代器紧密相关的是, 可迭代对象(iterable)是包含 Symbol.iterator 属性的对象. 这个 Symbol.iterator 知名符号定义了为指定对象返回迭代器的函数. 在 ES6 中, 所有的集合对象(数组、Set与Map) 以及字符串都是可迭代对象, 因此它们都被指定了默认的迭代器, 可迭代对象被设计用于与 ES 新增的 for-of 循环配合使用 生成器创建的所有迭代器都是可迭代对象, 因为生成器默认就会为 Symbol.iterator 属性赋值 for-of 循环: 完全删除了追踪集合索引的需要, 专注于操作集合内容, 在循环每次执行时会调用可迭代对象的 next() 方法, 并将结果对象 value 值存储在一个变量上, 直到结果对象的 done 属性变成 true let values = [1, 2, 3]; // 这个 for-of 首先调用了 values 数组的 Symobol.iterator 方法, 获取了一个迭代器(发生在 JS 引擎后台). 接下来 iterator.next() 被调用,迭代器结果对象的 value 属性被读出并放入了 num 变量。 num 变量的值开始为 1 ,接下来是 2 ,最后变成 3 。当结果对象的 done 变成 true ,循环就退出了,因此 num 绝不会被赋值为 undefined 。 for (let num of values) { console.log(num); } 1. 访问默认迭代器 可以使用 Symbol.iterator 来访问可迭代对象上的默认迭代器 let values = [1, 2, 3]; let iterator = values[Symbol.iterator](); console.log(iterator.next()); // "{ value: 1, done: false }" console.log(iterator.next()); // "{ value: 2, done: false }" console.log(iterator.next()); // "{ value: 3, done: false }" console.log(iterator.next()); // "{ value: undefined, done: true }" // 或检测是否能进行迭代 function isIterable(object) { return typeof object[Symbol.iterator] === "function"; } 2. 创建可迭代对象 自定义对象默认情况下不是可迭代对象, 但可以通过写入 Symbol.iterator 属性, 让它们成为可迭代对象 let collection = { items: [], *[Symbol.iterator]() { for (let item of this.items) { yield item; } } }; collection.items.push(1); collection.items.push(2); collection.items.push(3); for (let x of collection) { console.log(x); } 内置的迭代器 迭代器是 ES6 的一个重要部分, 语言已经为许多内置类型创建了迭代器. 1. 集合的迭代器 ES6 具有三种集合对象类型: Set、数组和Map. 这三种都拥有如下的迭代器,有助于探索它们的内容: entries():返回一个包含键值对的迭代器 entries() 迭代器会在每次 next() 被调用时返回一个双项数组, 对于数组来说, 第一项是数值索引; 对于 Set, 第一项也是值(因为它的值也会被视为键); 对于 Map, 第一项就是键 let colors = [ "red", "green", "blue" ]; let tracking = new Set([1234, 5678, 9012]); let data = new Map(); data.set("title", "Understanding ES6"); data.set("format", "ebook"); for (let entry of colors.entries()) { console.log(entry); } for (let entry of tracking.entries()) { console.log(entry); } for (let entry of data.entries()) { console.log(entry); } // 此代码输出了如下内容: [0, "red"] [1, "green"] [2, "blue"] [1234, 1234] [5678, 5678] [9012, 9012] ["title", "Understanding ES6"] ["format", "ebook"] values()迭代器 values() 迭代器仅仅能返回存储在集合内的值 let colors = [ "red", "green", "blue" ]; let tracking = new Set([1234, 5678, 9012]); let data = new Map(); data.set("title", "Understanding ES6"); data.set("format", "ebook"); for (let value of colors.values()) { console.log(value); } for (let value of tracking.values()) { console.log(value); } for (let value of data.values()) { console.log(value); } // 此代码输出了如下内容: "red" "green" "blue" 1234 5678 9012 "Understanding ES6" "ebook" keys()迭代器 keys() 迭代器 能返回集合中的每一个键. 对于数组, 返回数值类型的 键, 永不返回数组的其他自由属性; Set 的键与值是相同的, 因此它的 keys() 与 values() 返回了相同的 迭代器; 对于 Map, keys() 迭代器返回了每个不重复的键 let colors = [ "red", "green", "blue" ]; let tracking = new Set([1234, 5678, 9012]); let data = new Map(); data.set("title", "Understanding ES6"); data.set("format", "ebook"); for (let key of colors.keys()) { console.log(key); } for (let key of tracking.keys()) { console.log(key); } for (let key of data.keys()) { console.log(key); } // 本例输出了如下内容: 1 2 1234 5678 9012 "title" "format" 集合类型的默认迭代器 当 for-of 循环没有显式指定迭代器时, 每种集合类型都有一个默认的迭代器供循环使用 values() 方法是数组与 Set 的默认迭代器, 而 entries() 方法则是 Map 的默认迭代器. 2. 字符串的迭代器 从 ES5 发布开始, JS 的字符串就慢慢变得越来越像 数组. 而字符串也是由默认迭代器的 var message = "A B" ; for (let c of message) { console.log(c); } // 此代码输出了如下内容: A (blank) (blank) B 3. NodeList 的迭代器 文档对象模型(DOM)具有一种 NodeList 类型, 用于表示页面文档中元素的集合. NodeList也包含了一个默认迭代器, 其表现方式与数组的默认迭代器一致. var divs = document.getElementsByTagName("div"); for (let div of divs) { console.log(div.id); } 扩展运算符与非数组的可迭代对象 扩展运算符(...)可以被用于将一个 Set 转换为数组 扩展运算符能作用于所有可迭代对象, 并且会使用默认迭代器来判断需要使用哪些值. 所有的值都从迭代器中被读取出来并插入数组, 遵循迭代器返回值的顺序 let map = new Map([ ["name", "Nicholas"], ["age", 25]]), array = [...map]; console.log(array); // [ ["name", "Nicholas"], ["age", 25]] let set = new Set([1, 2, 3, 3, 3, 4, 5]), array = [...set]; console.log(array); // [1,2,3,4,5] 迭代器的高级功能 使用迭代器的基本功能, 并使用生成器来方便地创建迭代器, 可以完成很多工作了, 然而, 在单纯迭代集合的值之外的任务中, 迭代器会显得更加强大 1. 传递参数给迭代器 前面的例子中已经展示了迭代器能够将值传递出来, 通过 next() 方法或者在生成器中使用 yield 都可以. 还能通过 next() 方法向迭代器传递参数 ==> 数据的双向流通 function *createIterator() { // yield 1 => 会将语句的结果值(1)传递出去给第一个 next() 方法的结果对象 // (yield 1) => 表达式会接收到第二个 next(4) 传递进来的参数(4) 并赋值给 first 变量 let first = yield 1; // yield first + 2 => 会将语句的结果值(6)传递出去给第二个 next(4) 方法的结果对象 // (yield first + 2) => 表达式会接收到第三个 next(5) 传递进来的参数(5) 并赋值给 second 变量 let second = yield first + 2; // 4 + 2 yield second + 3; // 5 + 3 } let iterator = createIterator(); console.log(iterator.next()); // "{ value: 1, done: false }" console.log(iterator.next(4)); // "{ value: 6, done: false }" console.log(iterator.next(5)); // "{ value: 8, done: false }" console.log(iterator.next()); // "{ value: undefined, done: true }" 执行机制图: 黄色表示对于 next() 的第一次调用、以及在生成器内部执行的所有代码;水蓝色表示了对next(4) 的调用以及随之执行的代码;而紫色则表示对 next(5) 的调用以及随之执行的代码 传递参数给迭代器.png 2. 在迭代器中抛出错误 能传递给迭代器的不仅是数据, 还可以是错误条件, 迭代器可以通过 throw() 方法, 用于指示迭代器应在回复执行时抛出一个错误 function *createIterator() { let first = yield 1; let second = yield first + 2; // yield 4 + 2 ,然后抛出错误 yield second + 3; // 永不会被执行 } let iterator = createIterator(); console.log(iterator.next()); // "{ value: 1, done: false }" console.log(iterator.next(4)); // "{ value: 6, done: false }" console.log(iterator.throw(new Error("Boom"))); // 从生成器中抛出了错误 执行机制图: 红色表示当 throw() 被调用时所执行的代码,红星说明了错误在生成器内部大约何时被抛出。 在迭代器中抛出错误.png 可以在生成器内部使用一个 try-catch 块来捕捉错误 function *createIterator() { let first = yield 1; let second; try { second = yield first + 2; // yield 4 + 2 ,然后抛出错误 } catch (ex) { second = 6; // 当出错时,给变量另外赋值 } yield second + 3; } let iterator = createIterator(); console.log(iterator.next()); // "{ value: 1, done: false }" console.log(iterator.next(4)); // "{ value: 6, done: false }" // throw() 方法就像 next() 方法一样返回了一个结果对象. 由于错误在生成器内部被捕捉, 代码继续执行到下一个 yield 处并返回了下一个值, 也就是 9 console.log(iterator.throw(new Error("Boom"))); // "{ value: 9, done: false }" console.log(iterator.next()); // "{ value: undefined, done: true }" next() 方法指示迭代器继续执行(可能会带着给定的值), 而 throw() 方法则指示迭代器通过抛出一个错误继续执行. 在调用点之后会发生什么, 根据生成器内部的代码来决定 next() 与 throw() 方法控制着迭代器在使用 yield 时内部的执行 3. 生成器的 Return 语句 生成器也是函数, 同样在它内部使用 return 语句, 这样会使生成器退出执行, 同样也可以指定在 next() 方法最后一次调用时的返回值 在生成器内, return 表明所有的处理已完成, 因此 done 属性会被设为 true, 而如果提供了返回值, 就会被用于 value 字段 function *createIterator() { yield 1; return 42; yield 2; } let iterator = createIterator(); console.log(iterator.next()); // "{ value: 1, done: false }" // 第二次调用时, return 语句后的 42 会被返回在 value 字段中 console.log(iterator.next()); // "{ value: 42, done: true }" console.log(iterator.next()); // "{ value: undefined, done: true }" 扩展运算符与 for-of 循环会忽略 return 语句所指定的任意值. 一旦它们看到 done 的值为 true, 它们就会停止操作而不会读取对应的 value 值 4. 生成器委托 生成器可以用星号()配合 yield 这一特殊形式来委托其他迭代器* function *createNumberIterator() { yield 1; yield 2; } function *createColorIterator() { yield "red"; yield "green"; } // createCombinedIterator() 生成器依次委托了 createNumberIterator() 与 createColorIterator(). function *createCombinedIterator() { yield *createNumberIterator(); yield *createColorIterator(); yield true; } var iterator = createCombinedIterator(); // 每次对 next() 的调用都会委托给合适的生成器, 直到使用 createNumberIterator() 与 createColorIterator() 创建迭代器全部清空为止 console.log(iterator.next()); // "{ value: 1, done: false }" console.log(iterator.next()); // "{ value: 2, done: false }" console.log(iterator.next()); // "{ value: "red", done: false }" console.log(iterator.next()); // "{ value: "green", done: false }" console.log(iterator.next()); // "{ value: true, done: false }" console.log(iterator.next()); // "{ value: undefined, done: true }" 生成器委托也能让你进一步使用生成器的返回值, 这是访问这些返回值的最简单方式 function *createNumberIterator() { yield 1; yield 2; // 这里只有 return 返回值才会被 result 接收, 使用 yield 是无效的 return 3; } function *createRepeatingIterator(count) { for (let i=0; i < count; i++) { yield "repeat"; } } function *createCombinedIterator() { let result = yield *createNumberIterator(); yield *createRepeatingIterator(result); } var iterator = createCombinedIterator(); console.log(iterator.next()); // "{ value: 1, done: false }" console.log(iterator.next()); // "{ value: 2, done: false }" console.log(iterator.next()); // "{ value: "repeat", done: false }" console.log(iterator.next()); // "{ value: "repeat", done: false }" console.log(iterator.next()); // "{ value: "repeat", done: false }" console.log(iterator.next()); // "{ value: undefined, done: true }" 可以直接在字符串上使用 yield *(例如 `yield * "hello"), 字符串的默认迭代器会被使用 异步任务运行 以后在进一步了解

本文章由javascript技术分享原创和收集

发表评论 (审核通过后显示评论):