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)