Js 中 迭代器 与 生成器
上篇讲了关于手写 Promise ,从类的设计, 构造函数设计, then 方法实现, catch 方法借助 then 方法实现,then 方法的核心优化, 包括多次调用, 链式调用等, 最后再补充了一些常用的类方法, 如 all,allSettled, any 等实现, 整体过程还是比较有难度的, 体现在状态的变更, 代码执行顺序的控制等, 也算是 js 一个重要知识点吧.本篇则开始学习 js 中的迭代器和生成器, 只有先搞明白这俩兄弟, 才会对后续将 async ... await + Promise 有深刻认知.
认识迭代器
迭代器 (iterator) 是使用户能在容器对象上, 安全, 统一地访问容器元素的方法机制协议, 而无需关系容器内部实现细节
或者说迭代器是一种对象, 它提供了一种按顺序访问集合 (数组, 列表, 链表, 集合, 映射等) 中的每个元素的方法, 而无需让开发者了解其底层实现 (抽象接口). 它通常实现一个 next() 方法, 犯病一个包含两个属性的对象:
{value: 当前值, done: 是否遍历完成 }它本质上是一种设计模式, 在语言中通过接口和协议规范, 在运行时表现为一个具有状态和行为的对象. 可以将它类比为一个地铁的闸机系统:
[*]设计模式: "一人一票, 顺序通过"
[*]编程规范:所有地铁站必须刷卡
[*]接口定义:必须实现 validate(cart) 和 open() 方法
[*]也是对象:每个地铁站都有一台闸机
[*]数据结构:不存数据 (乘客),只控制访问顺序
迭代器的好处或者说应用场景在于:
[*]**统一接口: **不论遍历字符串, 数组, Map, 自定义结构等, 都可以用 for...of 或 .next() 来遍历
[*]**延迟计算: **迭代器可以"按需" 生成值, 而非一次性生成所有数据 (生成器)
[*]内存提效: 适用于处理大数据流或者无限序列 (斐波那契数列等)
[*]解耦结构:数据结构和遍历元素拆分, 让遍历元素部分由迭代器实现
js 中的迭代器
**在 js 中, 迭代器也是一个具体的对象, 这个对象需要符合迭代器协议: **
[*]迭代器协议, 定义了产生一系列值的标准方式
[*]在 js 中标准就是一个特定的 next() 方法
这个 next() 方法需要满足如下的要求:
[*]是一个无参或者仅一个参数的函数, 返回一个应当拥有如下两个属性的对象
[*]done (boolean)
[*]value
// 迭代器 认识, 也是一个对象
const names = ['youge', 'cj', 'yaya']
// 创建一个迭代器对象, 来访问 names 数组
let index = 0
const namesIterator = {
next: function() {
if (index < names.length) {
return { done: false, value: names }
} else {
return { done: true, value: undefined }
}
}
}
// 通过 迭代器的 next() 方法不断去访问数组元素
console.log(namesIterator.next())
console.log(namesIterator.next())
console.log(namesIterator.next())
console.log(namesIterator.next())
console.log(namesIterator.next())
// { done: false, value: 'youge' }
// { done: false, value: 'cj' }
// { done: false, value: 'yaya' }
// { done: true, value: undefined }
// { done: true, value: undefined }继续来封装一个生成迭代器的函数.
// 生成迭代器的函数, 返回一个对象, 包含 next()
// 数组迭代器
function createArrayIterator(arr) {
let index = 0
return {
next: function() {
if (index < arr.length) {
return { done: false, value: arr}
} else {
return { done: true, value: undefined }
}
}
}
}
const names = ['youge', 'cj', 'yaya']
const nums =
const namesIterator = createArrayIterator(names)
console.log(namesIterator.next())
console.log(namesIterator.next())
console.log(namesIterator.next())
console.log(namesIterator.next())
// { done: false, value: 'youge' }
// { done: false, value: 'cj' }
// { done: false, value: 'yaya' }
// { done: true, value: undefined }
// { done: true, value: undefined }
// nums数组 的迭代器
const numsIterator = createArrayIterator(nums)
console.log(numsIterator.next())
console.log(numsIterator.next())
console.log(numsIterator.next()) // { done: false, value: 30 }
console.log(numsIterator.next()) // { done: true, value: undefined }
// 创建一个无限的迭代器
function createNumIterator() {
let index = 0
return {
next: function() {
return { done: false, value: index++ }
}
}
}
const numIterator = createNumIterator()
console.log(numIterator.next())// 0
console.log(numIterator.next())// 1
console.log(numIterator.next())// 2
console.log(numIterator.next())// 3
console.log(numIterator.next())// 4可迭代对象
上面认识迭代器的时候, 这个代码分为了 3个部分:
[*]创建将要被迭代的数组 (全局)
[*]创建一个对象, 里面实现了一个叫 next() 的方法来获取数组数据 (全局)
[*]创建一个全局变量 index 来控制 对象 里面的 next 方法, 来访问数组对象
现在要来思考的是, 如何将上面这有紧密联系但是有互相分开的3部分组合到一起, 即统一放在同一个对象中.
// 组合到一起
const newObj {
names;
index;
obj.next();
}这个能组合到一起的对象, 就称为 可迭代对象
[*]该对象实现了 iterable protocol可迭代协议
[*]该协议必须实现 @@iterator方法,对应在代码中实现 方法即可
const iterableObje= { : function() { return 迭代器 } }迭代器
是一个对象, 它实现了 迭代器协议, 表现为该对象有一个 .next() 方法, 并返回 { value, done }的结构
可迭代对象
也是一个对象, 它实现了 可迭代协议, 表现为该对象有一个 Symbol.ierator 方法, 该方法返回一个 实现了迭代器协议的对象
// 可迭代对象 与 迭代器
const arr =
console.log(arr) // 是个函数, arr 是可迭代对象
// 数组也是对象, 内置实现了可迭代协议, 可以调用 Symbol.iterator
// 该方法返回一个迭代器, 可以用它来进行 .next() 获取元素
const iter = arr()
console.log(iter.next())
console.log(iter.next())
console.log(iter.next())
console.log(iter.next())
// { value: 1, done: false }
// { value: 2, done: false }
// { value: 3, done: false }
// { value: undefined, done: true }对比项可迭代对象(Iterable)迭代器(Iterator)本质对象对象核心特征有 () 方法有 .next() 方法协议实现 可迭代协议实现 迭代器协议作用能被 for...of、...、yield* 等消费负责实际遍历,返回 {value, done}是否可遍历✅ 可以被遍历✅ 可以被遍历(因为迭代器自己也是可迭代的)典型例子Array, String, Map, Set, argumentsarray() 的返回值厘清概念之后, 现对之前的数组, 迭代器, 迭代获取数组元素这个过程进行封装, 即将 nams 数组变为可迭代对象.
// 可迭代对象
const iterableObj = {
names: ['youge', 'cj', 'yaya'],
// Symbol.iterator 用 [] 包起来是让其作为字面量对象的 "计算属性"
: function() {
let index = 0
return {
next: () => { // 箭头函数无绑定this, 因此 next() 中的 this 会找到父级的 names
if (index < this.names.length) {
return { done: false, value: this.names}
} else {
return { done: true, value: undefined}
}
}
}
}
}
// 此时 iterableObj 对象是一个 可迭代对象
console.log(iterableObj) // 函数
// 第一次调用
const iterator = iterableObj()
console.log(iterator.next())
console.log(iterator.next())
console.log(iterator.next())
console.log(iterator.next())
// 第二次调用
const iterator2 = iterableObj()
console.log(iterator2.next())
// for ... of 可以遍历, 可迭代对象
for (const item of iterableObj) {
console.log(item) // youge, cj, yaya
}注意点:
[*]Symbol.iterator 用 "[ ]" 包起来是让其作为字面量对象的 "计算属性"
[*]利用箭头函数无绑定 this, 因此 next() 中的 this 会找到父级的 names
[*]for ... of 可以遍历, 可迭代对象
可见在 js 中的这个 this 的理解是非常重要的, 这里就用到了隐式调用原则嘛.
然后也解决了困扰我很久的问题: js 普通对象为啥不能用 for..of 遍历?因为普通对象没有实现可迭代协议呀.
// 普通对象不可用 for...of 迭代
const obj = {
name: 'youge',
age: 18
}
for (const item of obj) {
console.log(item)
}
// TypeError: obj is not iterable可以理解这个 for...of 也是一个语法糖, 它做的事情就是用迭代器不断调用 .next() 获取元素, 直到 done:true 的时候, 则停止.
内置迭代器对象
像我们常用的 String, Array, Map, Set, arguments,NodeList 等原生对象, 都已实现了可迭代协议, 通过它们所产生的对象, 都是一个可迭代对象.
// 内置了`代器的对象
// 字符串
const str = "ab c"
for (const s of str) {
console.log(s) // a, b, " "
}
// 数组
const arr =
const arrIter = arr()
console.log(arrIter.next()) // { value: 1, done: false }
console.log(arrIter.next()) // { value: 2, done: false }
console.log(arrIter.next()) // { value: undefined, done: true}
// set / map
const set = new Set()
set.add(10)
set.add(20)
console.log(set) //
for (const item of set) {
console.log(item) // 10, 20
}
// map
const map = new Map()
map.set('name', 'youge')
map.set(() => {}, 'cjj')
for (const item of map) {
console.log(item)
// [ 'name', 'youge' ]
// [ , 'cjj' ]
}
// 函数中的 arguments
function foo() {
for (const arg of arguments) {
console.log(arg) // 1, 2, 3
}
}
foo(1, 2, 3)这些内置对象, 都是可迭代对象, 原因就是它们都实现了 Symbol.iterable 方法, 它会返回迭代器, 内部实现元素的遍历啦.
可迭代对象的应用场景
[*]js 语法提升:for ...of; 展开表达式; yield;解构赋值等
[*]创建对象时:new Mapt();new Set(), new WeakMap()等
[*]方法调用时:Promise.all(iterable);Promise.all(iterable); Array.from(iterable) 等
// 可迭代对象应用
// 1. for...of 语法支持
const arr =
for (const item of arr) {
console.log(item) // 1, 2, 3
}
// 2. 展开运算符
const iterableObj = {
names: ['youge', 'cj', 'yaya'],
: function() {
let index = 0
return {
next: () => {
if (index < this.names.length) {
return { done: false, value: this.names }
} else {
return { done: true, value: undefined }
}
}
}
}
}
const names = ['bob', 'jack']
const newNames = [...names, ...iterableObj]
// [ 'bob', 'jack', 'youge', 'cj', 'yaya' ]
console.log(newNames)
// ES9 新增普通对象特性, 虽不可迭代, 但要求支持展开运算符
// 猜测实现原理 for (const entry obj.entries ) {} 组装 key, value 就搞定了
const obj = { name: 'youge', age: 18 }
console.log({...obj}) // { name: 'youge', age: 18 }
// 3. 解构语法
const = names
console.log(name1, name2) // bob, jack
const { name, age } = obj
console.log(name, age) // youge, 18
// 4. 创建其他对象时
const set1 = new Set(iterableObj)
const set2 = new Set(arr)
// 可迭代对象, 转为数组, 如 arguments
const arr1 = Array.from(iterableObj)
// 5. Promise.all(), 传普通值会自动 Promise.resolve(普通值)
Promise.all(iterableObj).then(res => {
console.log("res: ", res) // res:[ 'youge', 'cj', 'yaya' ]
})自定义类的可迭代实现
上述通过 Array, Set, Map, String 等类, 创建出来的对象都是可迭代对象.
现在要来实现一个自己的类, 使其创建出来的对象也是可迭代的, 即在类中实现 方法即可
用一个案例:创建一个 Classroom 的类:
[*]每个教室有自己的编号, 楼层, 当前教室的学生
[*]教室可以新进来学生 (push)
[*]创建的教室对象是可迭代对象
// 自定义类 - 实现可迭代功能
class Classroom {
constructor(id, floor, students) {
this.id = id
this.floor = floor
this.students = students
}
entry(newStudent) {
this.students.push(newStudent)
}
}
const classroom = new Classroom("301", "3楼", ['bob', 'jack'])
classroom.entry('cj')
// 需求: 将教室对象都迭代出来
for (const item of classroom) {
console.log(item)
}当前是不能使用 for...of 语法的, 因为我们这个自定义的类, 没有实现可迭代协议. 则我们在对象方法上去实现这个 Symbol.iterator 的方法即可.
// 自定义类 - 实现可迭代功能
class Classroom {
constructor(id, floor, students) {
this.id = id
this.floor = floor
this.students = students
}
entry(newStudent) {
this.students.push(newStudent)
}
// 让对象可迭代, 添加 方法
() {
let index = 0
return {
next: () =>{
if (index < this.students.length) {
return { done: false, value: this.students }
} else {
return { done: true, value: undefined }
}
}
}
}
}
const classroom = new Classroom("301", "3楼", ['bob', 'jack'])
classroom.entry('cj')
// 需求: 将教室内学生都迭代出来
for (const item of classroom) {
console.log(item) // bob, jack, cj
}生成器
生成器 (Generator) 是一种特殊的函数,它可以在函数执行中暂停, 之后再从暂停的地方恢复执行.
它返回一个生成器对象, 该对象既是迭代器, 也是可迭代对象
它提供了一种暂停和恢复函数执行的能力, 是实现惰性求值, 异步编程, 迭代器 等高级功能的核心工具.
// 普通函数的执行, 不能暂停
function foo() {
console.log(100)
console.log(200)
console.log(300)
}
foo()
// 需求: 在 打印 200 时先暂停, 晚点再往后执行普通函数不能控制 "中途暂停", 此功能在当前知识体系下, 是不能实现的.
生成器函数
生成器函数也是一个函数, 但和普通函数有区别:
[*]需要在 function 的后面加一个符号 *
[*]可以通过 yield 关键字来控制函数执行流程
[*]返回值是一个 生成器对象,它是一种特殊的迭代器
// 生成器函数
function* foo() {
console.log('函数开始执行...')
console.log('第一段代码: ', 100)
yield
console.log("第二代代码: ", 200)
yield
console.log("第三段代码: ", 300)
yield
console.log('函数执行结束')
}
// 调用 生成器函数时, 会返回一个生成器对象, 特殊迭代器
const generator = foo()
// 执行第一段代码, 第一个 yield
generator.next()
// 执行第二段代码
generator.next()
// 执行第三段代码
generator.next()
generator.next() // 函数执行结束即通过 yield 来控制函数的执行顺序, 然后调用生成器函数, 返回的是一个生成器对象, 它是一个特殊的迭代器, 因此可以用它来调用 .next() 方法, 对应于 yield 的分段代码.
// 生成器函数-执行流程
// 当遇到 yield时, 暂停函数执行
// 当遇到 return 时, 生成器停止执行
function* foo() {
console.log('函数开始执行...')
console.log('第一段代码: ', 100)
yield 111
console.log("第二代代码: ", 200)
yield
console.log("第三段代码: ", 300)
yield 333
console.log('函数执行结束')
return 'over'
}
// 调用 生成器函数时, 会返回一个生成器对象, 特殊迭代器
const generator = foo()
// 生成器对象 的 next() 也有返回值
console.log(generator.next())
console.log(generator.next() )
console.log(generator.next() )
console.log(generator.next() )// 函数执行结束
// 函数开始执行...
// 第一段代码:100
// { value: 111, done: false }
// 第二代代码:200
// { value: undefined, done: false }
// 第三段代码:300
// { value: 333, done: false }
// 函数执行结束
// { value: 'over', done: true }通过 yield 可以有返回值, 对应到 { value, done} 中.最后可以进行 return, 值作为最后的 value, done 的值变为 true.
生成器传递参数- next()
在调用 next() 函数时, 传递参数, 则此参数会作为上一个 yield 语句的返回值, 然后在下一个yield 间进行运算
也就是说, 我们是为本次的代码块提供了一个参数值进来.
const generator = foo(5)
// 生成器对象 的 next() 也有返回值
// next() 也可以传递参数
console.log(generator.next())
// 给第二个 next() 传参
console.log(generator.next(10) )给第二段代码传参,则先需要在 第一个段代码的 yield 的地方进行定义和接收,然后在第二个 yield 后边处理
function* foo() {
console.log('第一段代码: ', 100)
const n = yield 100 * num// 这里接收
console.log("第二代代码: ", 200)
const count = yield 200 * n // 这里调用
}同样的做法, 可以给第一个 (很少见) 或者第三个都进行这样的处理:
// 生成器-next() 传递参数
function* foo(num) {
console.log('函数开始执行...')
console.log('第一段代码: ', 100)
const n = yield 100 * num
console.log("第二代代码: ", 200)
const count = yield 200 * n
console.log("第三段代码: ", 300)
yield 300 * count
console.log('函数执行结束')
return 'over'
}
const generator = foo(5)
// 生成器对象 的 next() 也有返回值
// next() 也可以传递参数
console.log(generator.next())
console.log(generator.next(10) )
console.log(generator.next(20) )
console.log(generator.next() )函数开始执行...
第一段代码:100
{ value: 500, done: false }
第二代代码:200
{ value: 2000, done: false }
第三段代码:300
{ value: 6000, done: false }
函数执行结束
{ value: 'over', done: true }生成器传递参数- return()
还有一种传参方式是通过 return() 函数, 它会直接终止生成器, 在当前代码块执行之前.
但这样的话就表示生成器函数的结束, 之后调用 next 则不会再继续生成值了.
// 生成器函数-return 函数
function* foo(num) {
console.log('函数开始执行...')
console.log('第一段代码: ', 100)
const n = yield 100 * num
console.log("第二代代码: ", 200)
const count = yield 200 * n
console.log("第三段代码: ", 300)
yield 300 * count
console.log('函数执行结束')
return 'over'
}
const generator = foo(5)
console.log(generator.next())
// 第二段代码, 执行 return
console.log(generator.return(222))
console.log(generator.next(20) )
console.log(generator.next())函数开始执行...
第一段代码:100
{ value: 500, done: false }
{ value: 222, done: true }
{ value: undefined, done: true }
{ value: undefined, done: true }则在第二段的时候, 调用 "generator.return(222)"相当于是在第一段代码执行结束后进行 return
function* foo(num) {
console.log('函数开始执行...')
console.log('第一段代码: ', 100)
const n = yield 100 * num
// 相当于在这里终止了生成器
return 222
// 因为 return 了, 则后面的不会再进行 yield 新值了
console.log("第二代代码: ", 200)
const count = yield 200 * n
// ....
}生成器抛出异常 - throw()
除了可以给生成器函数内部传参外, 也可以给生成器函数内部抛出异常.
[*]抛异常后, 可以在生成器函数中捕获异常
[*]但在 catch 语句中则不能再 yield 新值了, 但在 catch 语句外用 yield 中断语句执行可以
// 生成器函数-throw() 捕捉异常
function* foo() {
console.log('函数开始执行...')
console.log('第一段代码: ', 100)
try {
yield 100
} catch (err) {
console.log('捕捉到异常: ', err)
}
console.log("第二代代码: ", 200)
const count = yield 200
console.log("第三段代码: ", 300)
yield 300 * count
console.log('函数执行结束')
return 'over'
}
const generator = foo()
console.log(generator.next())
// 第二段代码, 执行 throw, 则会在第一段代码执行完后就异常了
console.log(generator.throw('err'))
// 处理完异常后, 后续逻辑则正常运行
console.log(generator.next(20))
console.log(generator.next())函数开始执行...
第一段代码:100
{ value: 100, done: false }
捕捉到异常:err
第二代代码:200
{ value: 200, done: false }
第三段代码:300
{ value: 6000, done: false }
函数执行结束
{ value: 'over', done: true }生成器 替代 迭代器使用
生成器是一种特殊的迭代器, 则在一些情况下可以是使用生成器来代替迭代器的.
// 生成器 代替 迭代器
// 迭代器
function arrayIterator(arr) {
let index = 0
return {
next: function() {
if (index < arr.length) {
return {done: false, value: arr}
} else {
return {done: true, value: undefined}
}
}
}
}
const names = ['youge', 'cj', 'yaya']
const namesIterator = arrayIterator(names)
console.log(namesIterator.next())
console.log(namesIterator.next())
console.log(namesIterator.next())
// 也可以用生成器替代
function* arrayGenerator(arr) {
for (const item of arr) {
yield item// {value: xxx, done: false}
}
}
const namesGenerator = arrayGenerator(names)
console.log(namesGenerator.next())
console.log(namesGenerator.next())我们还可以使用 yield* 来生成一个可迭代对象.
这时候相当于是一个 yield语法糖, 会以此迭代这个可迭代对象, 每次迭代其中的一个值.
// 生成器 代替 迭代器 yield* 语法糖
function* arrayGenerator(arr) {
// yield* 可迭代对象
yield*arr
}
const names = ['youge', 'cj', 'yaya']
const namesGenerator = arrayGenerator(names)
console.log(namesGenerator.next())
console.log(namesGenerator.next())
console.log(namesGenerator.next())
console.log(namesGenerator.next()){ value: 'youge', done: false }
{ value: 'cj', done: false }
{ value: 'yaya', done: false }
{ value: undefined, done: true }来个小案例, 创建一个函数, 这个函数可以迭代一个范围内的数字
// 用生成器代替迭代器
function* createRangeIterator(start, end) {
let index = start
while (index < end) {
yield index++
}
}
const rangeIterator = createRangeIterator(1, 3)
console.log(rangeIterator.next())
console.log(rangeIterator.next())
console.log(rangeIterator.next())
// { value: 1, done: false }
// { value: 2, done: false }
// { value: undefined, done: true }
// { value: undefined, done: true }同样的, 对于之前自定义类的迭代器, 也是可以改写为生成器的.
// 自定义类 - 实现可迭代功能
class Classroom {
constructor(id, floor, students) {
this.id = id
this.floor = floor
this.students = students
}
entry(newStudent) {
this.students.push(newStudent)
}
// 让对象可迭代, 添加 方法
() {
let index = 0
return {
next: () =>{
if (index < this.students.length) {
return { done: false, value: this.students }
} else {
return { done: true, value: undefined }
}
}
}
}
}现在可以改成这样的:
// 自定义类 - 迭代逻辑, 用生成器替换
class Classroom {
constructor(id, floor, students) {
this.id = id
this.floor = floor
this.students = students
}
entry(newStudent) {
this.students.push(newStudent)
}
// 用生成器来实现 方法
*() {
yield* this.students// 妙呀!
}
}// 测试一下
const classroom = new Classroom("301", "3楼", ['bob', 'jack'])
classroom.entry('cj')
// 需求: 将教室对象都迭代出来
for (const item of classroom) {
console.log(item)
}bob
jack
cj
cj@m在实际应用中, 能用生成器解决问题, 尽量用它, 就不用自己写迭代器的一通逻辑, 直接用这种大道至简的语法糖:
yield* 可迭代对象// 1行顶10行至此, 关于迭代器和生成器的基本使用部分就到这了. 整体操作难度不大, 但是概念特别多, 比如说迭代器, 迭代器对象, 可迭代对象, 生成器, 生成器函数, 生成器是特殊迭代器, 生成器可以在某些情况下替代迭代器, for ...of 的语法糖, yield* 可迭代对象等. 就还是细节很多, 理解为主, 能用就行.
来源:豆瓜网用户自行投稿发布,如果侵权,请联系站长删除
页:
[1]