一. 迭代器和可迭代对象
1. 什么是迭代器?
迭代器(
iterator
),使用户在容器对象(container
,例如链表或数组)上遍访的对象,使用该接口无需关心对象的内部实现细节- 其行为像数据库中的光标,迭代器最早出现在1974年设计的
CLU
编程语言中 - 在各种编程语言的实现中,迭代器的实现方式各不相同,但是基本都有迭代器,比如
Java
、Python
等
- 其行为像数据库中的光标,迭代器最早出现在1974年设计的
从迭代器的定义我们可以看出来,迭代器是 用来对某个数据结构进行遍历的对象
在
js
中,迭代器也是一个具体的对象,这个对象需要符合迭代器协议(iterator protocol
):迭代器协议定义了产生一系列值(无论是有限还是无限个)的标准方式
在
js
中这个标准就是一个特定的next
方法next
方法有如下的要求:一个无参数或一个参数的函数,返回一个应当拥有以下两个属性的对象
done(boolean)
- 如果迭代器可以产生序列中的下一个值,则为
false
(这等价于没有指定done
这个属性) - 如果迭代器已将序列迭代完毕,则为
true
。这种情况下,value
是可选的(不写是等价于undefined
),如果它依然存在,即为迭代结束之后的默认返回值
- 如果迭代器可以产生序列中的下一个值,则为
value
- 迭代器返回的任何
js
值。done
为true
时可省略,省略等价于undefined
jsconst names = ["abc", "cba", "nba"] // 给数组 names 创建一个迭代器(迭代器: names的迭代器) let index = 0 const namesIterator = { next: function() { // done: Boolean // value: 具体值/undefined if (index < names.length) { return { done: false, value: names[index++] } } else { return { done: true } // 等价于 { done: true, value: undefined },所以可省略value } } } namesIterator.next() // {done: false, value: 'abc'} namesIterator.next() // {done: false, value: 'cba'} namesIterator.next() // {done: false, value: 'nba'} namesIterator.next() // {done: true}
- 迭代器返回的任何
封装一个迭代器函数
jsconst names = ["abc", "cba", "nba"] const nums = [100, 24, 55, 66, 86] // 封装一个函数 function createArrayIterator(arr) { let index = 0 return { next: function() { if (index < arr.length) { return { done: false, value: arr[index++] } } else { return { done: true } } } } } const namesIterator = createArrayIterator(names) namesIterator.next() // {done: false, value: 'abc'} namesIterator.next() // {done: false, value: 'cba'} namesIterator.next() // {done: false, value: 'nba'} namesIterator.next() // {done: true} const numsIterator = createArrayIterator(nums) numsIterator.next() // {done: false, value: 100} numsIterator.next() // {done: false, value: 24} numsIterator.next() // {done: false, value: 55} numsIterator.next() // {done: false, value: 66} numsIterator.next() // {done: false, value: 86} numsIterator.next() // {done: true }
2. 什么是可迭代对象?
但是上面的代码整体来说看起来是有点奇怪的:
- 我们获取一个数组的时候,需要自己创建一个
index
变量,再创建一个所谓的迭代器对象 - 事实上我们可以对上面的代码进行进一步的封装,让其变成一个可迭代对象
- 我们获取一个数组的时候,需要自己创建一个
什么又是可迭代对象呢?
- 它和迭代器是不同的概念
- 当一个对象实现了
iterable protocol
协议时,它就是一个可迭代对象 - 这个对象的要求是必须实现
@@iterator
方法,在代码中我们使用Symbol.iterator
访问该属性
当然我们要问一个问题,我们转成这样的一个东西有什么好处呢?
- 当一个对象变成一个可迭代对象的时候,就可以进行某些迭代操作
- 比如
for...of
操作时,其实就会调用它的@@iterator
方法
js// 将 infos 变成一个可迭代对象 /* 1. 必须实现一个特定的函数: [Symbol.iterator] 2. 这个函数需要返回一个迭代器(这个迭代器用于迭代当前的对象) */ // 1.迭代infos中friends const infos = { friends: ["kobe", "james", "curry"], [Symbol.iterator]: function() { let index = 0 const infosIterator = { next: () => { if (index < this.friends.length) { return { done: false, value: this.friends[index++] } } else { return { done: true } } } } return infosIterator } } // 可迭代对象必然具备下面的特点 // 可迭对象可以进行 for...of 操作 for (const item of infos) { console.log(item) // kobe james curry } // 可迭代对象必然有一个 [Symbol.iterator] 函数 // 数组是一个可迭代对象 const students = ["张三", "李四", "王五"] students[Symbol.iterator] // ƒ values() { [native code] } const studentIterator = students[Symbol.iterator]() studentIterator.next() // {value: '张三', done: false} studentIterator.next() // {value: '李四', done: false} studentIterator.next() // {value: '王五', done: false} studentIterator.next() // {value: undefined, done: true} // 2.迭代infos2中的key-value const infos2 = { name: "later", age: 18, height: 1.88, [Symbol.iterator]: function() { const keys = Object.keys(this) // 获取对象自身中所有可枚举属性的key(symbol除外) const values = Object.values(this) // 获取对象自身所有可枚举属性的value(symbol除外) const entries = Object.entries(this) // 获取对象自身所有可枚举属性的key-value let index = 0 const iterator = { next: function() { if (index < entries.length) { return { done: false, value: entries[index++] } } else { return { done: true } } } } return iterator } } // 可迭对象可以进行for...of操作 for (const item of infos2) { const [key, value] = item console.log(key, value) } // name later // age 18 // height 1.88
二. 原生的迭代器对象
1. 原生迭代器对象
事实上我们平时创建的很多原生对象已经实现了可迭代协议,会生成一个迭代器对象的:
String
、Array
、Map
、Set
、arguments
对象、NodeList
集合
js// 1.数组 const names = ["abc", "cba", "nba"] for (const name of names) { console.log(name) } names[Symbol.iterator]() // Array Iterator {} names[Symbol.iterator] == Array.prototype[Symbol.iterator] // true // 2.Set const set = new Set(["abc", "cba", "nba"]) for (const item of set) { console.log(item) } const SetIterator = set[Symbol.iterator]() SetIterator.next() // {value: 'abc', done: false} SetIterator.next() // {value: 'cba', done: false} SetIterator.next() // {value: 'nba', done: false} SetIterator.next() // {value: undefined, done: true} set[Symbol.iterator]() // SetIterator {'abc', 'cba', 'nba'} set[Symbol.iterator] == Set.prototype[Symbol.iterator] // true // 3.arguments function foo() { for (const arg of arguments) { console.log(arg) } console.log(arguments[Symbol.iterator]()) // Array Iterator {} console.log(arguments[Symbol.iterator] == Array.prototype[Symbol.iterator]) // true } foo(123, 321, 111, 222)
2. 可迭代对象的应用
那么这些东西可以被用在哪里呢?
js
中语法:jsfor...of 展开语法(spread syntax) yield*(后面章节讲解) 解构赋值(Destructuring_assignment)
创建一些对象时:
jsnew Map([Iterable]) new WeakMap([iterable]) new Set([iterable]) new WeakSet([iterable])
一些方法的调用:
jsPromise.all(iterable) Promise.race(iterable) Array.from(iterable)
jsconst arr = [1, 2, 3] const info = { name: "later", age: 18, height: 1.88, [Symbol.iterator]: function() { const values = Object.values(this) let index = 0 const iterator = { next: function() { if (index < values.length) { return { done: false, value: values[index++] } } else { return { done: true } } } } return iterator } } function foo(arg1, arg2, arg3) { console.log(arg1, arg2, arg3) } // 1.用在特定的语法上:展开语法 foo(...arr) // 1 2 3 foo(...info) // later 18 1.88 // 2.一些类的构造方法中, 也是传入的可迭代对象:new Set(iterable) const set1 = new Set(["aaa", "bbb", "ccc"]) const set2 = new Set("abc") console.log(set2) // Set(3) {'a', 'b', 'c'} const set3 = new Set(info) console.log(set3) // Set(3) {'later', 18, 1.88} // 3.一些常用的方法,如:Promise.all(iterable) const p1 = Promise.resolve("aaaa") const p2 = Promise.resolve("aaaa") const p3 = Promise.resolve("aaaa") const pSet = new Set() pSet.add(p1) pSet.add(p2) pSet.add(p3) Promise.all(pSet).then(res => { console.log("res:", res) // res: ['aaaa', 'aaaa', 'aaaa'] }) function bar() { // 将arguments转成Array类型 const arr = Array.from(arguments) console.log(arr) } bar(111, 222, 333) // [111, 222, 333]
三. 自定义类的迭代器
1. 自定义类的迭代实现
在前面我们看到
Array
、Set
、String
、Map
等类创建出来的对象都是可迭代对象:- 在面向对象开发中,我们可以通过
class
定义一个自己的类,这个类可以创建很多的对象 - 如果我们也希望自己的类创建出来的对象默认是可迭代的,那么在设计类的时候我们就可以添加上
@@iterator
方法
- 在面向对象开发中,我们可以通过
案例:
- 创建一个
classroom
的类 - 教室中有自己的位置、名称、当前教室的学生
- 这个教室可以进来新学生(push)
- 创建的教室对象是可迭代对象
jsclass Room { constructor(position, name, students) { this.position = position this.name = name this.students = students } pushStudent(student) { this.students.push(student) } // 实例方法定义在class内部中,内聚性好于定义在class的原型上 [Symbol.iterator]() { let index = 0 return { next: () => { const values = Object.values(this) if (index < values.length) { return { value: values[index++], done: false } } else { return { value: undefined, done: true } } } } } } const room1 = new Room({x: 1, y: 2}, '教室1', ['小红', '小明']) room1.pushStudent('小李') for (const item of room1) { console.log(item) // {x: 1, y: 2} 、教室1、['小红', '小明', '小李'] }
- 创建一个
2. 迭代器的中断
迭代器在某些情况下会在没有完全迭代的情况下中断:
- 比如遍历的过程中通过
break
、return
、throw
中断了循环操作 - 比如在解构的时候,没有解构所有的值
- 比如遍历的过程中通过
那么这个时候我们想要监听中断的话,可以添加
return
方法:jsclass Room { constructor(position, name, students) { this.position = position this.name = name this.students = students } // 实例方法定义在class内部中,内聚性好于定义在class的原型上 [Symbol.iterator]() { let index = 0 return { next: () => { const values = Object.values(this) if (index < values.length) { return { value: values[index++], done: false } } else { return { value: undefined, done: true } } }, return: () => { console.log(this) // Room {position: {…}, name: '教室1', students: ['小红', '小明']} return { done: true, value: undefined } //这里如果不return一个对象的话,会报错: Iterator result undefined is not an object } } } } const room1 = new Room({x: 1, y: 2}, '教室1', ['小红', '小明']) for (const item of room1) { if (item == '教室1') { break } console.log(item) // {x: 1, y: 2} }
四. 生成器的理解和作用
1. 什么是生成器函数,什么是生成器?
生成器是
ES6
中新增的一种函数控制、使用的方案,它可以让我们更加灵活的控制函数什么时候继续执行、暂停执行等- 平时我们会编写很多的函数,这些函数终止的条件通常是返回值或者发生了异常
生成器函数也是一个函数,但是和普通的函数有一些区别:
首先,生成器函数需要在
function
的后面加一个符号:*
function*
这种声明方式**(function
关键字后跟一个星号)会定义一个生成器函数**(generator function
),调用该函数会返回一个Generator
对象js// 下面几种写法都是ok的 function* generatorFunc() {} function * generatorFunc() {} function *generatorFunc() {}
其次,生成器函数可以通过
yield
关键字来控制函数的执行流程最后,生成器函数的返回值是一个
Generator
(生成器)- 生成器是一种 特殊类型的迭代器
jsfunction* generatorFunc() {} console.log( generatorFunc() ) // generatorFunc {<suspended>}
GeneratorFunction
构造器生成新的生成器函数对象在
js
中,生成器函数实际上都是GeneratorFunction
的实例对象注意:
GeneratorFunction
并不是一个全局对象。它可以通过下面的代码获取jsconsole.log( Object.getPrototypeOf(function*(){}) ) // GeneratorFunction {prototype: Generator, Symbol(Symbol.toStringTag): 'GeneratorFunction', constructor: ƒ}
2. 生成器函数的执行
我们发现下面的生成器函数
foo
的执行体压根没有执行,它只是返回了一个生成器对象yield
关键字使生成器函数执行暂停,yield
关键字后面的表达式的值返回给生成器的调用者,作为value
的值它可以被认为是一个基于生成器的版本的
return
关键字yield
关键字返回一个IteratorResult
对象,它有两个属性value
和done
value
属性是对yield
表达式求值的结果,而done
是false
,表示生成器函数尚未完全完成一旦遇到
yield
表达式,生成器的代码将被暂停运行,直到生成器的next()
方法被调用那么我们如何可以让它执行函数中的东西呢?调用
next
即可,但是遇到yield
会暂停执行,除非继续调用该生成器的next
方法jsfunction* foo() { console.log('111') yield console.log('222') } foo() // 不会执行函数体代码,而是返回一个生成器对象 const fooGenerator = foo() fooGenerator // foo {<suspended>} fooGenerator.__proto__ === foo.prototype // true foo.prototype // Generator {} foo.__proto__ // GeneratorFunction {prototype: Generator, Symbol(Symbol.toStringTag): 'GeneratorFunction', constructor: ƒ} fooGenerator.next() // 111 fooGenerator.next() // 222
我们之前学习迭代器时,知道迭代器的
next
是会有返回值的但是我们很多时候不希望
next
返回的是一个undefined
,这个时候我们可以通过yield
来返回结果jsfunction* foo() { console.log('执行函数体代码') } const fooGenerator = foo() console.log(fooGenerator.next()) // {value: undefined, done: true} function* bar() { console.log('执行函数体代码') yield '222' } const barGenerator = bar() console.log(barGenerator.next()) // {value: '222', done: false}
注意:
在调用
next
时,yield
上面和后面的代码会执行,而前面的不会jsfunction* bar() { console.log('执行函数体代码') yield console.log('first') } const barGenerator = bar() barGenerator.next() // 执行函数体代码 // first
3. 生成器传递参数 - next 函数
函数既然可以暂停来分段执行,那么函数应该是可以传递参数的,我们是否可以给每个分段来传递参数呢?
- 答案是可以的
- 我们在调用
next
函数的时候,可以给它传递参数,这个参数会作为上一个yield
语句的返回值 - 注意:也就是说我们是为本次的函数代码块执行提供了一个值
jsfunction* foo(arg1) { console.log(arg1) const arg2 = yield console.log(arg2) const arg3 = yield console.log(arg3) } const fooGenerator = foo('参数1') // 第一个next方法只能通过函数传参的方式了 fooGenerator.next() // 参数1 fooGenerator.next('参数2') // 参数2 fooGenerator.next('参数3') // 参数3
如果生成器函数提前
return
返回,后续再调用next
方法返回{value: 'undefined', done: true}
,因为函数已执行完jsfunction* foo() { yield '111' return '222' yield '333' } const fooGenerator = foo() fooGenerator.next() // {value: '111', done: false} fooGenerator.next() // {value: '222', done: true} fooGenerator.next() // {value: 'undefined', done: true}
4. 生成器提前结束 - return 函数
还有一个可以给生成器函数传递参数的方法是通过
return
函数:return
传值后这个生成器函数就会结束,之后调用next
不会继续生成值了jsfunction* foo() { console.log('111') yield '111' console.log('222') yield '222' console.log('333') yield '333' } const fooGenerator = foo() fooGenerator.next() fooGenerator.return('000') fooGenerator.next() // output: // 111 // {value: '111', done: false} // {value: '000', done: true} // {value: undefined, done: true}
5. 生成器抛出异常 - throw 函数
除了给生成器函数内部传递参数之外,也可以给生成器函数内部抛出异常:
throw
方法用来向生成器抛出异常,并恢复生成器的执行,返回带有done
及value
两个属性的对象抛出异常后我们可以在生成器函数中捕获异常
但是在
catch
语句中不能继续yield
新的值了,但是可以在catch
语句外使用yield
继续中断函数的执行
jsfunction* foo() { console.log('a----') try { yield '111' } catch(err) { console.log('err: ', err) } console.log('b----') yield '222' console.log('c----') yield '333' } const fooGenerator = foo() fooGenerator.next() fooGenerator.throw('000') fooGenerator.next() fooGenerator.next() // outout: // a---- // {value: '111', done: false} // err: 000 // b---- // {value: '222', done: false} // c---- // {value: '333', done: false} // {value: undefined, done: true}
6. 应用题
/**
console.log(1)
function fn(time){
setInterval (() => {
console.log(2)
},time)
}
fn(4)
console.log(3)
实现 输出 1 2 3
只能修改 setInterval 的前后代码 和 内部箭头函数的代码
*/
console.log(1)
function fn(t) {
let timer = null
function* foo() {
timer = setInterval(yield () => {
if (timer) return clearInterval(timer)
console.log(2)
}, t)
}
const generatorFn = foo()
let {value, done} = generatorFn.next() // {value: [Function (anonymous)], done: false}
// value 就是 setInterval中 yield后面的表达式的值
value()
// next方法 传入的参数会作为上一个 yield语句的返回值
// 所以这里需要传入 setInterval中yield后面的函数,因为 setInterval第一个参数只能是callback
generatorFn.next(value)
}
fn(4)
console.log(3)
// output:
// 1
// 2
// 3
五. 自定义生成器方案
1. 生成器替代迭代器
我们发现生成器是一种特殊类型的迭代器,那么在某些情况下我们可以使用生成器来替代迭代器:
js// 1.对之前的代码进行重构(用生成器函数) const names = ["abc", "cba", "nba"] function* createArrayIterator(arr) { for (let i = 0; i < arr.length; i++) { yield arr[i] } // yield arr[0] // yield arr[1] // yield arr[2] // return undefined } const namesIterator = createArrayIterator(names) namesIterator.next() // {value: 'abc', done: false} namesIterator.next() // {value: 'cba', done: false} namesIterator.next() // {value: 'nba', done: false} namesIterator.next() // {value: undefined, done: true} // 2.生成器函数, 可以生成某个范围的值 function* createRangeGenerator(start, end) { for (let i = start; i < end; i++) { yield i } } const rangeGen = createRangeGenerator(3, 9) rangeGen.next() // {value: 3, done: false} rangeGen.next() // {value: 4, done: false} rangeGen.next() // {value: 5, done: false} rangeGen.next() // {value: 6, done: false} rangeGen.next() // {value: 7, done: false} rangeGen.next() // {value: 8, done: false} rangeGen.next() // {value: undefined, done: true}
事实上我们还可以使用
yield*
来迭代一个可迭代对象:- 这个时候相当于是一种
yield
的语法糖,只不过会依次迭代这个可迭代对象,每次迭代其中的一个值
jsconst names = ["abc", "cba", "nba"] function* createArrayIterator(arr) { yield* arr } const namesIterator = createArrayIterator(names) namesIterator.next() // {value: 'abc', done: false} namesIterator.next() // {value: 'cba', done: false} namesIterator.next() // {value: 'nba', done: false} namesIterator.next() // {value: undefined, done: true}
- 这个时候相当于是一种
2. 自定义类迭代 - 生成器实现
在之前的自定义类迭代中,我们也可以换成生成器:
jsclass Room { constructor(position, name, students) { this.position = position this.name = name this.students = students } pushStudent(student) { this.students.push(student) } // 实例方法定义在class内部中,内聚性好于定义在class的原型上 // [Symbol.iterator]() { // let index = 0 // return { // next: () => { // const values = Object.values(this) // if (index < values.length) { // return { value: values[index++], done: false } // } else { // return { value: undefined, done: true } // } // } // } // } *[Symbol.iterator]() { yield* Object.values(this) } } const room1 = new Room({x: 1, y: 2}, '教室1', ['小红', '小明']) room1.pushStudent('小李') for (const item of room1) { console.log(item) // {x: 1, y: 2} 、教室1、['小红', '小明', '小李'] }
六. 异步处理方案解析
1. 异步处理方案
学完了我们前面的
Promise
、生成器等,我们目前来看一下异步代码的最终处理方案案例需求:
- 我们需要向服务器发送网络请求获取数据,一共需要发送三次请求
- 第二次的请求
url
依赖于第一次的结果 - 第三次的请求
url
依赖于第二次的结果
jsfunction request(url) { return new Promise((resolve, reject) => { setTimeout(() => { resolve(url) }, 1000) }) } // 方式一:层层嵌套(回调地狱) function getData() { request('aaa').then(res => { console.log('第一次res: ', res) // 第一次res: aaa request(res + '_bbb').then(res => { console.log('第二次res: ', res) // 第二次res: aaa_bbb request(res + '_ccc').then(res => { console.log('第三次res: ', res) // 第三次res: aaa_bbb_ccc }) }) }) } getData() // 方式二:Promise链式调用(解决回调地狱) function getData2() { // 这种方式还是不行,本质上还是在嵌套回调 // request('111').then(res => { // console.log('第一次调用: ', res) // return res // }).then(res => { // request(res + '_222').then(res => { // console.log('第二次调用: ', res) // }) // }) request('111').then(res => { console.log('第一次调用:', res) // 第一次调用: 111 return request(res + '_222') }).then(res => { console.log('第二次调用:', res) // 第二次调用: 111_222 return request(res + '_333') }).then(res => { console.log('第三次调用:', res) // 第三次调用: 111_222_333 }) } getData2()
2. Generator 方案
但是上面的代码其实看起来也是阅读性比较差的,有没有办法可以继续来对上面的代码进行优化呢?
jsfunction request(url) { return new Promise((resolve, reject) => { setTimeout(() => { resolve(url) }, 1000) }) } // 方式三:生成器函数 + 生成器 function* getData() { const res1 = yield request('111') console.log('res1: ', res1) const res2 = yield request('222') console.log('res2: ', res2) const res3 = yield request('333') console.log('res3: ', res3) } const generator = getData() // console.log(generator.next().value) // Promise {<pending>} generator.next().value.then(res1 => { generator.next(res1).value.then(res2 => { generator.next(res2).value.then(res3 => { generator.next(res3) }) }) }) // 方式四:async + await async function getData() { const res1 = await request('111') const res2 = await request('222') const res3 = await request('333') }
3. 自动执行 generator 函数
目前我们的写法有两个问题:
- 第一,我们不能确定到底需要调用几层的
Promise
关系 - 第二,如果还有其他需要这样执行的函数,我们应该如何操作呢?
- 难道又往后继续添加代码吗?
- 第一,我们不能确定到底需要调用几层的
所以,我们可以封装一个工具函数
execGenerator
自动执行生成器函数:jsfunction request(url) { return new Promise((resolve, reject) => { setTimeout(() => { resolve(url) }, 1000) }) } function* getData() { const res1 = yield request('111') console.log('res1: ', res1) const res2 = yield request('222') console.log('res2: ', res2) const res3 = yield request('333') console.log('res3: ', res3) } function execGenerator(func) { const generator = func() function exec(res) { const result = generator.next(res) console.log(result) // {value: Promise, done: false} if (result.done) return result.value result.value.then(res => { exec(res) }) } exec() } execGenerator(getData)
4. 总结
es6
之前异步代码都是写的嵌套回调函数的方式es6
之后,有了promise
,开始使用promise
的方式写Promise
链式调用的代码- 然后发现可以使用生成器写成方式三中的代码,但是需要写一个自动执行
generator
函数的函数 - 于是
async
和await
关键字就出来了,底层原理主要就是generator + promise
写法的语法糖 - 也有一点区别,就比如自动执行生成器函数中
yield
需要搭配promise
使用 - 而
await
做了一些边界值判断,允许使用普通值