8-1.为什么需要迭代器
我们之前需要遍历数据的时候,通常使用 for
循环。
但 for
循环其实是利用索引及其递增访问对应的数据。
如果我们只是想要对应的 value
,而不想关心 key
的时候,就可以利用迭代器。
目前,数组、Set
集合、Map
集合、字符串都可以使用迭代器。
8-2.何为迭代器
迭代器其实是一个对象。
- 本身有
next
方法。 - 调用
next
方法后,返回值是一个对象。对象有两个属性,value
和done
。 - 其中
value
对应迭代数据的每一项。done
表示是否迭代完毕。迭代完毕时,value
为undefined
,此时done
为true
。
我们可以模拟实现一个迭代器:
function getIterator (list) {
var index = 0
return {
next () {
var done = index >= list.length
var value = done ? undefined : list[index++]
return {
value,
done
}
}
}
}
var list = ['Tom', 'Jerry']
var iterator = getIterator(list)
iterator.next() // {value: 'Tom', done: false}
iterator.next() // {value: 'Jerry', done: false}
iterator.next() // {value: undefined, done: true}
8-3.默认迭代器
在 Es6
中,所有的集合对象(数组、 Set
集合、 Map
集合)和字符串都是可迭代对象。
这是因为 Es6
为这些集合对象和字符串都提供了一个方法 Symbol.iterator
。
Symbol.iterator
本身是 Symbol
,它对应的值是一个函数,返回值是迭代器。
// 1.数组
var arr = ['i', 'am', 'an', 'array']
var getArrayIterator = arr[Symbol.iterator]
console.log(getArrayIterator) // ƒ values() { [native code] }
// 这里不能使用 getArrayIterator(arr)
var arrayIterator = arr[Symbol.iterator]()
arrayIterator.next()
arrayIterator.next()
arrayIterator.next()
arrayIterator.next()
// 2.Set 集合
var set = new Set(['i', 'am', 'a', 'set'])
var getSetIterator = set[Symbol.iterator]
console.log(getSetIterator) // ƒ values() { [native code] }
var setIterator = set[Symbol.iterator]()
setIterator.next()
setIterator.next()
setIterator.next()
setIterator.next()
// 3.Map 集合
var map = new Map([['id', 19], ['name', 'yxp']])
var getMapIterator = map[Symbol.iterator]
console.log(getMapIterator) // ƒ entries() { [native code] }
var mapIterator = map[Symbol.iterator]()
mapIterator.next()
mapIterator.next()
// 4.字符串
var str = 'i am the king of the world'
var getStrIterator = str[Symbol.iterator]
console.log(getStrIterator) // ƒ [Symbol.iterator]() { [native code] }
var strIterator = str[Symbol.iterator]()
strIterator.next()
strIterator.next()
在 Es6
中提供了 for...of
这个利用迭代器的新的遍历方式。该方式能遍历所有具有 Symbol.iterator
属性的数据。
利用 Symbol.iterator
,我们可以将不可迭代数据转化为可迭代数据。比如对象:
var obj = {
id: 19,
name: 'yxp'
}
// 对象本身是不可迭代数据 Uncaught TypeError: obj is not iterable
for (const value of obj) {
console.log(value)
}
// 利用 Symbol.iterator
var iterateObj = {
id: 19,
name: 'yxp',
[Symbol.iterator] () {
const list = []
for (const key in this) {
// 由于 Symbol.iterator 是 Symbol。所以Object.prototype.hasOwnProperty()并不会获取该属性
if (this.hasOwnProperty(key)) {
list.push(key)
}
}
let index = 0
return {
next () {
const done = index >= list.length
const value = done ? undefined : list[index++]
return {
value,
done
}
}
}
}
}
for (const value of iterateObj) {
console.log(value)
}
8-4.内置迭代器
在 Es6
中,这3种类型的集合对象:数组、 Map
集合与 Set
集合,都内建了以下三种迭代器:
entries()
返回一个迭代器,其值为多个键值对。values()
返回一个迭代器,其值为集合的值。keys()
返回一个迭代器,其值为集合中的所有键名。
// 以数组为例
var list = ['i', 'am', 'a', 'list']
console.log(list.entries().next()) // {value: Array(2), done: false}
console.log(list.values().next()) // {value: 'i', done: false}
console.log(list.keys().next()) // {value: 0, done: false}
另外值得一提的是,字符串、 伪数组(NodeList
、 arguments
)也都能使用 for...of
方法,因为它们支持 Symbol.iterator
属性。但并不支持上述迭代器:
function fn () {
for (const value of arguments) {
console.log(value) // 逐次打印 1 2 3
}
console.log(arguments[Symbol.iterator]) // ƒ values() { [native code] }
console.log(arguments.entries) // undefined
console.log(arguments.values) // undefined
console.log(arguments.keys) // undefined
}
fn(1, 2, 3)
8-5.展开运算符
展开运算符可用于任意可迭代对象。
展开运算符可以操作可迭代对象,并根据默认迭代器来选取要引用的值,从迭代器读取所有值,然后按照返回顺序将它们依次返回。
// 1.数组
var list = [1, 2, 3]
console.log([...list]) // [1, 2, 3]
// 2.Set
var set = new Set(['a', 'set'])
console.log([...set]) // ['a', 'set']
// 3.Map
var map = new Map([['id', 19], ['name', 'yxp']])
console.log([...map]) // [['id', 19], ['name', 'yxp']]
// 4.字符串
var str = 'i am string'
console.log([...str]) // ['i', ' ', 'a', 'm', ' ', 's', 't', 'r', 'i', 'n', 'g']
// 5.伪数组(NodeList arguments)
function fn () {
console.log([...arguments]) // [1, 2, 3]
}
fn(1, 2, 3)
8-6.生成器
我们在前几章中有模拟实现迭代器,在 ES6
中提供了专门的生成器来生成迭代器。
生成器是一种返回迭代器的函数,通过 function
关键字后的 *
符号来表示。函数中会用到新的关键字 yield
(意为生成)。
function* generator () {
yield 'Hello world!'
console.log('console', yield 'alibaba')
yield 'I love ' + 'my life'
}
var iterator = generator()
console.log(iterator) // generator {<suspended>}
console.log(iterator.next()) // {value: 'Hello world!', done: false}
console.log(iterator.next()) // {value: 'alibaba', done: false}
// console undefined
console.log(iterator.next()) // {value: 'I love my life', done: false}
console.log(iterator.next()) // {value: undefined, done: true}
可以发现:
generator
生成器函数执行之后,会返回迭代器iterator
。yield
后的值会对应next
方法执行后的value
属性。yield
关键字后面可以接任何值或表达式。但在函数内的执行结果是undefined
,并不会有返回值。执行完
yield
后,该yield
后的语句不会继续执行,直到再次调用迭代器的next()
方法才会继续执行。该特性在下一章的高级迭代器功能中有更加广泛的应用。
TIP
yield
只能在生成器的函数作用域内使用,不能在其他函数作用域下使用。
我们使用 for
循环和 forEach
来作比较:
// 1.for循环
function* firstGenerator(list) {
for (let i = 0; i < list.length; i++) {
yield list[i]
}
}
var firstIterator = firstGenerator(['Hello', 'World'])
console.log(firstIterator.next()) // {value: 'Hello', done: false}
console.log(firstIterator.next()) // {value: 'World', done: false}
console.log(firstIterator.next()) // {value: undefined, done: true}
// 2.forEach
function* secondGenerator(list) {
// 由于 yield 在箭头的作用域内,所以执行时会报错
list.forEach(item => {
yield item
})
}
var secondIterator = secondGenerator(['Hello', 'World'])
console.log(secondIterator.next()) // Uncaught SyntaxError: Unexpected identifier
console.log(secondIterator.next())
console.log(secondIterator.next())
此外,由于生成器函数本身也是函数,所以也可以使用函数表达式或者作为对象方法使用:
// 1.函数表达式
var generator = function* () {
yield 'Hello world'
}
var iterator = generator()
console.log(iterator.next()) // {value: 'Hello world', done: false}
// 2.作为对象方法使用
var obj = {
*generator () {
yield 'Hello world'
}
}
var iterator = obj.generator()
console.log(iterator.next()) // {value: 'Hello world', done: false}
8-7.高级迭代器功能
在之前,我们已经阐述了迭代器和生成器的基本使用。
两者结合,我们来看下高级迭代器功能。
8-7-1.迭代器传参
// 1.正常执行
function* generator () {
var value = yield 1
var result = yield value + 2
yield result
}
var iterator = generator()
console.log(iterator.next()) // {value: 1, done: false}
console.log(iterator.next()) // {value: NaN, done: false} 因为value是undefined,undefined进行加法运算时,结果是NaN
console.log(iterator.next()) // {value: undefined, done: false}
// 2,传参
function* generator () {
var value = yield 1
var result = yield value + 2
yield result
}
var iterator = generator()
console.log(iterator.next(100)) // {value: 1, done: false}
console.log(iterator.next(100)) // {value: 102, done: false}
console.log(iterator.next(100)) // {value: 100, done: false}
// 3.next传参其实影响的是上一次的yield语句的返回值
function* generator () {
var value = yield 1
var result = yield value + 2
yield 2
yield result
}
var iterator = generator()
console.log(iterator.next(100)) // {value: 1, done: false}
console.log(iterator.next(100)) // {value: 102, done: false}
console.log(iterator.next(200)) // {value: 2, done: false} 可以从这里看出来,next的传参影响到了var result这里的语句。
console.log(iterator.next(100)) // {value: 200, done: false}
总结:
- 当
yield
后接变量与表达式时,可进行next
函数传参操作。 - 每一次的
next
函数被调用,只会执行对应yield
右侧语句,左侧语句并不会执行。 next
传参其实影响的是上一次的yield
语句的返回值。
8-7-2.迭代器抛错
// 利用try...catch捕获throw抛出的错误
function* generator () {
var value
try {
value = yield 1
} catch (err) {
value = 2
console.warn(err)
}
var result = yield value + 2
yield result
}
var iterator = generator()
// ①
console.log(iterator.next(100)) // {value: 1, done: false}
// ②
console.log(iterator.throw(new Error('error'))) // {value: 4, done: false}
// ③
console.log(iterator.next(100)) // {value: 100, done: false}
逐步分析(console
语句):
- 执行
①
时,代码执行var value; yield 1
。 - 执行
②
时,在执行yield value + 2
,会先执行try...catch
块。由于throw
抛出错误,这时try...catch
块捕获,代码执行catch
块代码,value
被赋值为2
。同时打印Error
。另外throw
可以看做额外的一次的next
调用,执行yield value + 2
。 - 执行
③
时,由于next
传入的参数是100
,所以value
会是100
。
现在我们修改下 throw
的调用时机:
// throw抛出的错误必须在执行yield之前 才能捕获到
function* generator () {
var value
try {
value = yield 1
} catch (err) {
value = 2
console.warn(err)
}
var result = yield value + 2
yield result
}
var iterator = generator()
// ①
console.log(iterator.throw(new Error('error'))) // Uncaught Error: error
// ②
console.log(iterator.next(100)) // not work
// ③
console.log(iterator.next(100)) // not work
执行之后,我们会发现 ①
直接报错,原因就是错误未捕获,Uncaught Error: error
。后面的 ②
和 ③
不会再执行。
其实这个例子的执行结果,我们在上例中是可以推断出来的:
在执行 ①
时,执行的会是 var value; yield 1
这两步,由于这两步没有 try...catch
进行捕获,在 throw
语句触发的时候,就直接报错阻断代码执行了。
总结:
throw
可用于抛出错误。throw
可以看做会额外执行一次next
方法。- 在与
throw
对应的yield
代码执行过程中,我们可以用try...catch
捕获错误。
8-7-3.生成器的return
由于生成器也是函数,因此也可以通过 return
语句提前退出函数执行。
当使用 return
时,属性 done
会被设置为 true
。
如果 return
语句后面有值,属性 value
会被设置成对应的值。
如下例:
function* generator () {
yield 1
return 'hello world'
yield 2
}
var iterator = generator()
console.log(iterator.next()) // {value: 1, done: false}
console.log(iterator.next()) // {value: 'hello world', done: true}
console.log(iterator.next()) // {value: undefined, done: true}
8-7-4.委托生成器
在某些情况下,我们可能需要将多个迭代器合而为一。这时我们就可以使用委托生成器。
要额外注意的一点是,在定义委托生成器时,需要在 yield
后面添加 *
。
function* numberGenerator () {
yield 1
yield 2
}
function* colorGenerator () {
yield 'red'
yield 'green'
yield 'blue'
}
function* combinedGenerator () {
// 这里的yield必须添加* 否则next方法不能正确迭代
yield* numberGenerator()
yield* colorGenerator()
}
var combinedIterator = combinedGenerator()
console.log(combinedIterator.next()) // {value: 1, done: false}
console.log(combinedIterator.next()) // {value: 2, done: false}
console.log(combinedIterator.next()) // {value: 'red', done: false}
console.log(combinedIterator.next()) // {value: 'green', done: false}
console.log(combinedIterator.next()) // {value: 'blue', done: false}
另外还可以结合 return
使用:
function* subGenerator () {
yield 1
return 3
}
function* generator () {
var result = yield* subGenerator()
console.log(result)
for (let i = 1; i <= result; i++) {
yield i
}
}
var iterator = generator()
console.log(iterator.next()) // {value: 1, done: false}
// 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}
我们可以发现,子生成器中的 return
语句的值,并没有打印 {value: 3, done: true}
,而是将值传递给了 result
变量。
我们记住这个特点就可以。
8-8.任务执行
我们已经知道,生成器中的 yield
语句是在迭代器的 next
函数调用后才会执行的。
但在实际的业务中,我们并不会根据时机或条件逐步调用 next
函数。
对应 async await
更加高级的处理任务的功能,我们来用迭代器与生成器来实现相同的功能。
首先分析下,我们要实现的功能:
// ajax
var getUserNameById = function (data, success, error ) {
$.ajax({
url: '',
method: 'get',
data,
success,
error
})
}
// 生成器 我们的业务功能会写在这里
function* generator () {
// 1.同步任务
var value = yield 1
console.log(value) // 我们根据前几节 会知道这里是undefined 但我们现在在上下文环境 希望它打印的是1
var result = yield value + 2
console.log(result)
// 2.异步任务
try {
var res = yield getUserNameById({
id: 19
})
// 对应success的处理逻辑
} catch (err) {
// 对应error的处理逻辑
}
}
// 我们要写一套代码 自动执行上面生成器 并实现下列功能:
/*
1.yield 语句自动依次执行。
2.yield 左侧语句的变量,对应上一步yield执行后的value
3.异步任务按照上面的书写方式执行
4.处理异步任务的成功逻辑与失败逻辑
*/
我们循序渐进的来实现这套函数。
8-8-1.简单任务执行
function* generator () {
console.log(1)
yield
console.log(2)
yield
console.log(3)
}
// 不封装函数的话 想要打印的话 需要调用3次next函数
var iterator = generator()
iterator.next()
iterator.next()
iterator.next()
现在我们封装一套函数,使得上面的 generator
内的代码能够自动执行,而无需我们再手动的调用 next
函数。
// 生成器
function* generator () {
console.log(1)
yield
console.log(2)
yield
console.log(3)
}
// 辅助函数
function exec (generatorFn) {
var iterator = generatorFn()
var result = iterator.next()
function step () {
console.log(result)
if (!result.done) {
result = iterator.next()
step()
}
}
step()
}
exec(generator)
8-8-2.任务执行传参
上节已经实现了同步任务的自动执行,但是如果我们想要实现下面的生成器功能,就需要对辅助函数加以改造、添加传参功能。
// 生成器
function* generator () {
var value = yield 1
// !!! 如果想要result为3,那么则需要把上一步yield中的得到的value传入next方法
var result = yield value + 2
yield result
}
// 1.辅助函数
function exec (generatorFn) {
var iterator = generatorFn()
var result = iterator.next()
function step () {
console.log(result)
if (!result.done) {
result = iterator.next()
step()
}
}
step()
}
exec(generator)
/*
根据前几节的学习,我们会知道,这里依次打印:
{value: 1, done: false}
{value: NaN, done: false}
{value: undefined, done: false}
{value: undefined, done: true}
*/
// 2.可传参的辅助函数
function exexWithParams (generatorFn) {
var iterator = generatorFn()
var result = iterator.next()
function step () {
console.log(result)
if (!result.done) {
// 这里把yield得到的value 传入下一步的next方法中
result = iterator.next(result.value)
step()
}
}
step()
}
exexWithParams(generator)
/*
依次打印:
{value: 1, done: false}
{value: 3, done: false}
{value: 3, done: false}
{value: undefined, done: true}
*/
8-8-3.异步任务执行
var getImage = function (data) {
return function (success, error) {
$.ajax({
url: 'https://img3.doubanio.com/img/files/file-1633834533-0.jpg',
method: 'get',
data,
success,
error,
xhrFields: {
responseType: 'blob'
}
})
}
}
function* generator () {
// 1.同步任务
var value = yield 1
var result = yield value + 2
// 2.异步任务
try {
var res = yield getImage()
// 对应success的处理逻辑
const objectURL = URL.createObjectURL(new Blob([res], { type: 'image/jpeg' }))
console.log(objectURL)
} catch (err) {
// 对应error的处理逻辑
console.error(err)
}
}
function exec (generatorFn) {
var iterator = generatorFn()
var result = iterator.next()
function step () {
console.log(result)
if (!result.done) {
// 判断value的类型 异步任务这里会是function
if (typeof result.value === 'function') {
var success = function (res) {
result = iterator.next(res)
step()
}
var error = function (err) {
// 注意这里的iterator.throw 其实是在 yield getImage() 的下一个yield执行时抛错
result = iterator.throw(err)
// 这里是否调用step的区别在于 异步任务抛错后 后续代码是否还要执行
// step()
}
result.value(success, error)
} else {
result = iterator.next(result.value)
step()
}
// 因为上面的error本身是异步的 所以step函数本身不能写在这里
// step()
}
}
step()
}
exec(generator)