迭代器
迭代器(iterator),使用户在容器对象(container,例如链表或数组)上遍访的对象,使用该接口无需关心对象的内部实现细节
- 其行为像数据库中的光标,迭代器最早出现在1974年设计的CLU编程语言中
- 在各种编程语言的实现中,迭代器的实现方式各不相同,但是基本都有迭代器,比如Java、Python等
从迭代器的定义我们可以看出来,迭代器是帮助我们对某个数据结构进行遍历的对象
在JavaScript中,迭代器也是一个具体的对象,这个对象需要符合迭代器协议(iterator protocol):
- 迭代器协议定义了产生一系列值(无论是有限还是无限个)的标准方式
- 在JavaScript中这个标准就是一个特定的next方法
- 这个next需要满足如下要求:
- 一个无参数或者一个参数的函数,返回一个应当拥有以下两个属性的对象
- done(boolean):
-
如果迭代器可以产生序列中的下一个值,则为 false。(这等价于没有指定 done 这个属性。)
-
如果迭代器已将序列迭代完毕,则为 true。这种情况下,value 是可选的,如果它依然存在,即为迭代结束之后的默认返回值(一般情况下,对应的默认值为undefined,因此此时value就可以省略)
- value:
- 迭代器返回的任何 JavaScript 值。done 为 true 时可省略
- value:
-
- done(boolean):
- 一个无参数或者一个参数的函数,返回一个应当拥有以下两个属性的对象
const user = {
friends: ['Alex', 'Klaus', 'Steven']
}
function genIterator(arr) {
let index = 0
return {
next() {
if (index < arr.length) {
return {
done: false,
value: arr[index++]
}
} else {
return { done: true }
}
}
}
}
// 此时iterator就是一个迭代器
// 一个专门用于迭代数组的迭代器
const iterator = genIterator(user.friends)
console.log(iterator.next()) // => { done: false, value: 'Alex' }
console.log(iterator.next()) // => { done: false, value: 'Klaus' }
console.log(iterator.next()) // => { done: false, value: 'Steven' }
console.log(iterator.next()) // => { done: true }
可迭代对象
对于上述的示例代码而言,可以发现的是,迭代器和对应需要迭代的对象是分离的,并不内聚,所以我们需要对上述代码进行改进
const user = {
friends: ['Alex', 'Klaus', 'Steven'],
// Symbol.iterator作为计算属性名,作为一个特殊的函数
// 这个函数用于在对象中返回对应的迭代器
[Symbol.iterator]() {
let index = 0
// 迭代器函数中的this为迭代器函数所在的那个对象
// 因为其内部调用方式类似 user[Symbol.iterator]()
console.log(this === user) // => true
return {
// 使用箭头函数 修正next函数中的this指向
// 内部调用方式 类似 [Symbol.iterator]().next()
next:() => {
if (index < this.friends.length) {
return {
done: false,
value: this.friends[index++]
}
} else {
return { done: true }
}
}
}
}
}
// 因为user对象实现了迭代器函数,那么就可以认为user对象是一个可迭代对象
// 任何一个可迭代对象都可以通过for-of进行迭代
// for-of在迭代过程中会自动执行对象内部的[Symbol.iterator]方法
for (const name of user) {
console.log(name)
}
/*
=>
Alex
Klaus
Steven
*/
所以如果一个对象需要是可迭代对象,则必须满足如下条件:
- 实现迭代器方法,[Symbol.iterator]
- 迭代器方法必须返回一个用于迭代当前对象的迭代器
JS原生的一些数据结构(如字符串,数组,set,map)等都是可迭代对象,默认就实现了迭代器方法
const arr = [1, 2, 3]
console.log(arr[Symbol.iterator]()) // => Object [Array Iterator] {}
const iterator = arr[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 }
所以当一个对象实现了iterable protocol协议时,它就是一个可迭代对象
可迭代对象和迭代器是不同的概念
如果一个对象实现了 @@iterator 方法,我们就认为这个对象实现了iterable protocol协议,是个可迭代对象
在代码中我们使用 Symbol.iterator 访问该属性
事实上我们平时创建的很多原生对象已经实现了可迭代协议,会生成一个迭代器对象
例如: String、Array、Map、Set、arguments对象、NodeList集合
示例
const user = {
name: 'Klaus',
age: 23,
height: 1.88,
[Symbol.iterator]() {
const entires = Object.entries(user)
let index = 0
return {
next() {
if (index < entires.length) {
return { done: false, value: entires[index++] }
} else {
return { done: true }
}
}
}
}
}
for (const [key, value] of user) {
console.log(key, value)
}
/*
=>
name Klaus
age 23
height 1.88
*/
应用场景
- JavaScript中语法: for ...of、展开语法(spread syntax)、yield*、解构赋值(Destructuring_assignment) {对象的解构赋值除外}
- 创建一些对象时:new Map([Iterable])、new WeakMap([iterable])、new Set([iterable])、new WeakSet([iterable])
- 一些方法的调用:Promise.all(iterable)、Promise.race(iterable)、Array.from(iterable)
for-in vs for-of
for-in是用于遍历对象上的可枚举属性,包括自身的可枚举属性和位于原型链上的可枚举属性
for-of是用于对对象进行迭代,在遍历的时候,会自动去调用对象上的Symbol.iterator方法
也就是说如果一个对象没有实现Symbol.iterator方法,那么该对象是不可迭代对象,不能使用for-of迭代
const user = {
name: 'Klaus',
address: 'shanghai',
__proto__: {
age: 23
},
[Symbol.iterator]() {
const entries = Object.entries(this)
let index = 0
return {
next() {
if (index < entries.length) {
return { done: false, value: entries[index++] }
} else {
return { done: true }
}
}
}
}
}
// for-in会遍历自身和自身原型对象上的所有可枚举属性
for (const key in user) {
console.log(key)
/*
=>
name
address
age
*/
}
// 默认情况下,原生对象是不可迭代对象
// 但是在本例中,自主实现了Symbol.iterator方法
// 所以本例中的user对象是可迭代对象
for (const key of user) {
console.log(key)
/*
=>
['name', 'Klaus' ]
['address', 'shanghai' ]
*/
}
自定义类的迭代
class Person {
constructor(name, age, height) {
this.name = name
this.age = age
this.height = height
}
// 当一个类的显示原型上存在 可迭代方法([Symbol.iterator])
// 那么这个类所创建出来的所有实例方法都默认是可迭代对象
[Symbol.iterator]() {
const values = Object.values(this)
let index = 0
return {
next: () => {
if (index < values.length) {
return { done: false, value: values[index++] }
} else {
return { done: true }
}
}
}
}
}
const p1 = new Person('Klaus', 23, 1.88)
for (const value of p1) {
console.log(value)
}
/*
=>
Klaus
23
1.88
*/
迭代器的中断
迭代器在某些情况下会在没有完全迭代的情况下中断:
- 比如遍历的过程中通过break、return、throw中断了循环操作
- 比如在解构的时候,没有解构所有的值
那么这个时候我们想要监听中断的话,可以添加return方法
const user = {
name: 'Klaus',
age: 23,
height: 1.88,
[Symbol.iterator]() {
const values = Object.values(user)
let index = 0
return {
next() {
if (index < values.length) {
return { done: false, value: values[index++] }
} else {
return { done: true }
}
},
// 如果没有编写return方法,默认会存在默认的return方法
// 但是如果编写了return方法,就会对默认的return方法进行重写
// return方法也是需要返回含有done属性或value属性的一个对象的
return() {
console.log('迭代器被中断了')
return { done: true }
}
}
}
}
for (const value of user) {
console.log(value)
if (value === 23) {
break
}
}
生成器
生成器是ES6中新增的一种函数控制、使用的方案,
平时我们会编写很多的函数,这些函数终止的条件通常是返回值或者发生了异常
但是生成器可以让我们更加灵活的控制函数什么时候继续执行、暂停执行等
所以 生成器函数本质上是一种特殊的函数
- 首先,生成器函数需要在function的后面加一个符号
* - 其次,生成器函数可以通过yield关键字来控制函数的执行流程
- 最后,生成器函数的返回值是一个Generator(生成器)
- 而生成器本质上是一种特殊的迭代器
// 一个函数function后边加上 星号 就表明当前函数是生成器函数
// *只要在function和函数名之间即可 即 function* foo() 和 function *foo() 都是合法的
// 推荐 function* foo()
// 生成器方法被执行的时候,并不会执行函数内部的代码
// 而是会返回一个生成器对象
function* foo() {
console.log(111)
console.log(222)
yield console.log('aaa')
console.log(333)
console.log(444)
yield
console.log(555)
console.log(666)
}
const generator = foo()
// 当我们执行生成器对象的next方法的时候
// 会自动执行到下一个yield所在位置后停止执行
// yield后边的同行代码 也会被执行
generator.next()
/*
=>
111
222
aaa
*/
console.log('------')
generator.next()
/*
=>
333
444
*/
console.log('------')
generator.next()
/*
=>
555
666
*/
// 函数内部代码执行完毕后,
// 继续执行生成器对象的next方法
// 那么对应的next方法会静默失效
generator.next()
返回值
function* foo() {
console.log(111)
console.log(222)
yield '第一次yield返回值'
console.log(333)
console.log(444)
yield '第二次yield返回值'
console.log(555)
console.log(666)
return 'foo函数执行完毕了'
}
const generator = foo()
// yield关键字 后边的值 会作为next方法返回值的value值
// 如果是因为 yield关键字 暂停的 函数执行 那么next方法的返回值的done属性的值为false
// 如果是因为 return 结束的 函数执行 那么next方法的返回值的done属性的值为true
console.log(generator.next()) // => { value: '第一次yield返回值', done: false }
console.log(generator.next()) // => { value: '第二次yield返回值', done: false }
console.log(generator.next()) // => { value: 'foo函数执行完毕了', done: true }
传参
- 调用next函数的时候,可以给它传递参数,那么这个参数会作为上一个yield语句的返回值
- 也就是说我们是为本次的函数代码块执行提供了一个值
function* foo(res) {
console.log(res)
const res1 = yield '第一次yield返回值'
console.log(res1)
const res2 = yield '第二次yield返回值'
console.log(res2)
return 'foo函数执行完毕了'
}
// 函数执行到第一个yield中使用的变量在函数调用的时候进行传入
const generator = foo('第一次传参')
// 第一次调next方法时候,一般不会传入对应的参数(也无法传入对应的参数)
generator.next()
// 自第二次开始,next方法传入的参数
// 会作为yield表达式的返回值
generator.next('第二次传参')
generator.next('第三次传参')
中断
function* foo(res) {
console.log(res)
const res1 = yield '第一次yield返回值'
console.log(res1)
return 'foo函数执行完毕了'
console.log(res2)
yield 'foo函数执行完毕了'
}
const generator = foo()
// 如果提前return了,那么生成器函数后边的所有yield和return都不会再被执行
console.log(generator.next('第一次传参')) // => { value: '第一次yield返回值', done: false }
console.log(generator.next('第二次传参')) // => { value: 'foo函数执行完毕了', done: true }
console.log(generator.next('第三次传参')) // => { value: undefined, done: true }
生成器提前结束
function* foo() {
console.log('part1')
yield
console.log('part2')
yield
console.log('part3')
}
const generator = foo('arg1')
console.log(generator.next()) // => { value: undefined, done: false }
// 调用生成器的return方法 会立即结束生成器函数的执行
// 返回的对象中 done的值 为 true, value的值为return方法传入的参数
console.log(generator.return('arg2')) // => { value: 'arg2', done: true }
console.log(generator.next('arg3')) // => { value: undefined, done: true }
生成器抛出异常
function* foo() {
console.log('part1')
try {
// 第一个yield的返回值是一个异常,或者说yeild表达式执行产生了异常
// 所以使用try-catch进行异常捕获
yield
console.log('part2')
} catch(e) {
// 在catch语句中不能继续yield新的值,但是可以在catch语句外使用yield继续中断函数的执行
// 在catch中的yield会被自动忽略
console.log('error:' + e)
}
yield
console.log('part3')
}
const generator = foo('arg1')
console.log(generator.next())
// generator.throw方法 用于让generator在内部抛出一个异常
// 该方法的参数是一个异常对象,其会作为上一个yield(在这里就是第一个yield)的异常被抛出
console.log(generator.throw(new Error('this is an error')))
console.log(generator.next('arg3'))
示例
// 有[a, b] 依次生成 a 到b之间的各个数
// 例如 [1, 4] -> 1, 2, 3, 4
function* printNum(start, end) {
for (let i = start; i <= end; i++) {
yield i
}
}
const generator = printNum(3, 5)
console.log(generator.next()) // => { value: 3, done: false }
console.log(generator.next()) // => { value: 4, done: false }
console.log(generator.next()) // => { value: 5, done: false }
console.log(generator.next()) // => { value: undefined, done: true }
生成器 替代 迭代器
const user = {
name: 'Klaus',
age: 24,
height: 1.88,
// 生成器函数,会返回一个生成器
// 生成器本质上是一个特殊的迭代器
*[Symbol.iterator]() {
const entries = Object.entries(user)
for (let i = 0; i < entries.length; i++) {
// 调用next方法后,会将第一个entry返回,并停止当前函数的执行
// 再次调用next方法,返回后一个entry,并再次停止当前函数的执行
// 依次类推
// 最终迭代完成后,返回{ done: true, value: undefined }
yield entries[i]
}
}
}
for(let [key, value] of user) {
console.log(key, value)
}
/*
=>
name Klaus
age 24
height 1.88
*/
yield*
yield* <iterable> 是一种yield的语法糖,只不过会依次迭代这个可迭代对象,每次迭代其中的一个值
class Person {
constructor(name, age, height) {
this.name = name
this.age = age
this.height = height
}
*[Symbol.iterator]() {
yield* Object.values(this)
}
}
const p1 = new Person('Klaus', 23, 1.88)
for (const value of p1) {
console.log(value)
}
/*
=>
Klaus
23
1.88
*/
async/await
异步处理方案
如果我们存在如下需求:
- 我们需要向服务器发送网络请求获取数据,一共需要发送三次请求
- 第二次的请求url依赖于第一次的结果
- 第三次的请求url依赖于第二次的结果
- 依次类推
解决方法一:
function asyncFn(res) {
return new Promise(resolve => {
setTimeout(() => resolve(res), 2000)
})
}
// 这么处理可以解决依次异步请求的问题
// 但是这么做会导致回调函数嵌套回调函数
// 如果请求过多,会导致嵌套层级过多
// 这种情况被称之为回调地狱
asyncFn('第一次调用').then(res => {
console.log(res)
asyncFn('第二次调用').then(res => {
console.log(res)
asyncFn('第三次调用').then(res => console.log(res))
})
})
解决方法二:
function asyncFn(res) {
return new Promise(resolve => {
setTimeout(() => resolve(res), 2000)
})
}
// 利用promise的then方法依旧会返回一个新的promise的特性
// 可以将嵌套调用 转换为 链式调用
asyncFn('第一次调用').then(res => {
console.log(res)
return asyncFn('第二次调用')
}).then(res => {
console.log(res)
return asyncFn('第三次调用')
}).then(res => console.log(res))
解决方法三:
function asyncFn(res) {
return new Promise(resolve => {
setTimeout(() => resolve(res), 2000)
})
}
// 利用生成器函数来处理异步方法
function* fetchData() {
const res1 = yield asyncFn(10)
const res2 = yield asyncFn(20)
const res3 = yield asyncFn(30)
console.log(res1 + res2 + res3) // => 60
}
const generator = fetchData()
// generator.next().value 就是asyncFn 返回的那个promise对象
generator.next().value
.then(res => generator.next(res).value)
.then(res => generator.next(res).value)
.then(res => generator.next(res).value)
.catch(err => console.error(err))
上述代码的生成器执行是重复的逻辑,所以我们可以将上述生成器执行过程,封装成一个自动化执行函数
function asyncFn(res) {
return new Promise(resolve => {
setTimeout(() => resolve(res), 2000)
})
}
// 利用生成器函数来处理异步方法
function* fetchData() {
const res1 = yield asyncFn(10)
const res2 = yield asyncFn(20)
const res3 = yield asyncFn(30)
console.log(res1 + res2 + res3) // => 60
}
const generator = fetchData()
execGenerator(fetchData)
function execGenerator(fn) {
if (typeof fn !== 'function') {
throw new Error('fn is not a function')
}
const generator = fn()
function exec(res) {
const result = generator.next(res)
// 如果result的done值为true表明的是
// 函数已经return了,所以直接return即可
if (result.done) return
result.value.then(res => exec(res))
}
exec()
}
在ES9中,提供了上述写法的语法糖,那就是async/await
所以async/await本质就是promise+generator的语法糖
function asyncFn(res) {
return new Promise(resolve => {
setTimeout(() => resolve(res), 2000)
})
}
// 这段代码就是上述示例的语法糖写法
async function fetchData() {
const res1 = await asyncFn(10)
const res2 = await asyncFn(20)
const res3 = await asyncFn(30)
console.log(res1 + res2 + res3) // => 60
}
fetchData()
异步函数
异步函数是使用async关键字声明的一个函数,其本质是生成器函数和promise组合使用的一种语法糖写法
异步函数的内部代码执行过程和普通的函数是一致的,默认情况下也是会被同步执行
返回值
异步函数有返回值时,和普通函数会有区别:
- 情况一:异步函数也可以有返回值,但是异步函数的返回值相当于被包裹到Promise.resolve中
- 情况二:如果我们的异步函数的返回值是Promise,状态由会由Promise决定
- 情况三:如果我们的异步函数的返回值是一个对象并且实现了thenable,那么会由对象的then方法来决定
async function foo() {
return 123 // Promise.resolve(123)
}
foo().then(res => console.log(res))
async function foo() {
return new Promise((resolve) => {
resolve(123)
})
}
foo().then(res => console.log(res))
异步函数抛出异常,既可以通过try-catch捕获,也可以通过catch方法捕获
async function foo() {
throw new Error('this is a error')
}
foo().then(res => console.log(res))
.catch(res => console.log(res.message))
async function foo() {
throw new Error('this is a error')
}
try {
// 等待foo执行结束,以便于捕获foo执行异常
await foo()
} catch(e) {
console.error(e.message)
}
await
async函数另外一个特殊之处就是可以在它内部使用await关键字,而普通函数中是不可以的
- 通常使用await是后面会跟上一个表达式,这个表达式会返回一个Promise
- 那么await会等到Promise的状态变成fulfilled状态,之后继续执行异步函数
- await后 也可以跟上普通的同步表达式,对应的结果会使用Promise.resolve进行包裹,但是这么做的结果和不加await关键字时效果是一致的
function fun() {
return new Promise(resolve => {
setTimeout(() => resolve('success'), 1000)
})
}
async function foo() {
// await后边 如果是一个Promise
// 会等待到 Promise有结果后,才会继续往后执行代码
// 如果是resolved 则会把结果赋值给res
// 如果是rejected 则会抛出对应的异常
// + 可以在异步函数中使用 try-catch进行捕获
// + 如果异步函数中没有捕获,因为异步函数返回一个promise
// 所以可以在foo().catch() 中进行捕获
// + 如果依旧没有捕获对应的异常,会一层层向上传递
// 最终交给浏览器进行处理,浏览器会将对应的错误输出在console中
const res = await fun()
console.log(res)
}
foo()
async function foo() {
// await 123 === await Promise.resolve(123)
// 但是这么做往往没有什么实际意义,因为其最终执行效果和没有await关键字是一致的
const res = await 123
console.log(res) // => 123
}
foo()
await关键字只能使用在async函数内部,或ES模块的顶层
使用在async函数内部
async function main() {
const asyncMsg = Promise.resolve('hello world!')
console.log(asyncMsg) // => hello world!
}
main()
在ES模块顶层使用
因为顶层await只能使用在ES模块中,所以需要进行特殊设置
方式一: 在package.json中设置type: module
方式二: 将需要开启顶层await的模块的后缀设置mjs
index.mjs
const asyncMsg = await Promise.resolve('hello world!')
console.log(asyncMsg) // "hello world!"