迭代器(iterator)
什么是迭代器:
迭代器是一个对象,可以对另外一个对象进行遍历,称之为迭代器,其类似于光标,使用的时候不需要关心其内部的实现方法。
可迭代协议
一个迭代器需要满足可迭代协议,其内容包括
1. 必须包含一个next方法
2. next方法的返回值需要包含
(1) value: 迭代器返回的值,在done为false时有效
(2) done: 迭代器是否迭代完成,默认可省略,当迭代器迭代结束时,为true
迭代器对象的使用
通过调用其next方法可以逐步执行遍历,next方法可以传递进一个参数或者不传入参数
e.g. 对一个数组实现迭代器:
const names = ['abc', 'def', 'ghi', 'xxx', 21312312, { test: 1 }]
// 创建迭代器
const namesIterator = {
index: 0,
next: function () {
if (this.index < names.length) {
return {
value: names[this.index++],
done: false,
}
} else {
return { done: true }
}
}
}
console.log(namesIterator.next())
console.log(namesIterator.next())
console.log(namesIterator.next())
console.log(namesIterator.next())
console.log(namesIterator.next())
console.log(namesIterator.next())
console.log(namesIterator.next())
通过执行.next() 方法可以执行迭代,观察 done和value的关系
e.g. 优化一下,我们可以写一个工厂方法,传递进去一个数组,返回对应的迭代器对象
const names = ['abc', 'def', 'ghi', 'xxx', 21312312, { test: 1 }]
// 迭代器工厂
function createArrayIterator(arr) {
let index = 0;
return {
next: function () {
if (index < arr.length) {
return {
name: arr[index++],
done: false,
}
} else {
return { done: true }
}
}
}
}
// 创建迭代器
const namesIterator = createArrayIterator(names)
console.log(namesIterator.next())
console.log(namesIterator.next())
console.log(namesIterator.next())
console.log(namesIterator.next())
console.log(namesIterator.next())
console.log(namesIterator.next())
console.log(namesIterator.next())
可迭代对象
可迭代对象和迭代器是不同的概念,当一个对象实现了[Symbol.iterator]方法,这个对象就是可迭代对象,注意 [Symbol.iterator]需要实现为一个工厂方法,其返回值需要是一个迭代器。
const infos = {
data: [
'iterator',
'generator',
'test',
100,
{ test: 100 }
[1, 2, 3]
],
[Symbol.iterator]: function () {
let index = 0
return {
next: () => {
if (index < this.data.length) {
return {
value: this.data[index++],
done: false,
}
} else {
return { done: true, }
}
}
}
}
}
infos对象实现了 [Symbol.iterator] 成为一个可迭代对象,[Symbol.iterator]对应一个工厂方法,其返回一个迭代器,用来遍历infos中的data属性
请注意这里next方法的this指向问题,如果使用function(){} 其内部的this指向迭代器本身(因为调用的时候一定是 迭代器.next() )所以需要使用箭头函数,让this继承工厂方法的this,而工厂方法是由infos对象[Symbol.iterator]调用创建,所以this指向infos。
可迭代对象的应用场景
1. Javascipt语法: for...of , 展开语法,yield* 委托,解构赋值
2. 一些对象的构造方法传入的参数是可迭代对象 如: new Map(iterable). new Set(iterable)
3. 一些方法调用 如: Promise.all(iterable) Promise.race(iterable) Array.from(iterable)
我们经常使用 Array.from 将伪数组arguments转换成数组,arguments也是个可迭代对象,所以可以直接传入Array.from(iterable)中
原生可迭代对象
String Array Map Set 伪数组: arguments Nodelist
注意: 普通对象Object 不是可迭代对象,对一个对象 for of 或者使用展开运算符在非赋值的情况下展开对象会报错 如 foo(...obj)
用在赋值中的展开对象是特殊处理,和可迭代无关,如:
{
a:100,
...obj
}
自定义类迭代器
想创建一个对象就默认带iterator,可以将迭代器工厂放到其原型链上(作为类方法)
类中没有function关键字,可直接写成[Symbol.iterabor](){}的方式
class Person {
constructor(name, age, height, friends) {
this.name = name
this.age = age
this.height = height
this.friends = friends
}
// 定义类迭代器工厂方法,注意是放在原型中的
[Symbol.iterator]() {
const entries = Object.entries(this)
let index = 0
return {
next() {
if (index < entries?.length) {
return {
value: entries[index++],
done: false,
}
} else {
return {
done: true
}
}
}
}
}
}
const p1 = new Person('TOM', 18, 1.80, ['jack', 'lucy', 'jasmine'])
const p2 = new Person('Jack', 17, 1.90, ['tom', 'lucy', 'jasmine'])
for(let p of p1){
const [key,value] = p
console.log(key,value)
}
for(let p of p2){
const [key,value] = p
console.log(key,value)
}
迭代器中断
迭代器在以下情况下会中断
1. 循环遍历时,使用break return等中断循环
2. 解构赋值时没有结构所有的值
可以通过在迭代器中设置return方法作为中断的回调,return方法也需要满足可迭代协议 返回(done: true)
对上述例子做修改如下:
class Person {
constructor(name, age, height, friends) {
this.name = name
this.age = age
this.height = height
this.friends = friends
}
[Symbol.iterator]() {
const entries = Object.entries(this)
let index = 0
return {
next() {
if (index < entries?.length) {
return {
value: entries[index++],
done: false,
}
} else {
return {
done: true
}
}
},
// 设置迭代器中断的回调
return(){
console.log('iterator中断')
return {done:true}
}
}
}
}
const p1 = new Person('TOM', 18, 1.80, ['jack', 'lucy', 'jasmine'])
const p2 = new Person('Jack', 17, 1.90, ['tom', 'lucy', 'jasmine'])
for(let p of p1){
const [key,value] = p
console.log(key,value)
}
for(let p of p2){
const [key,value] = p
if(value === 1.90){
break
}
console.log(key,value)
}
在循环中强制中断迭代,会触发return函数,输出"iterator中断"并且返回 { done: true }
生成器 (generator)
ES6中新增特性,是特殊的迭代器,是一种函数控制和使用的方案,可以灵活的控制函数的暂停和执行
生成器函数
- 声明的时候需要在function后加* function*
- 需要通过yield函数来控制函数的执行和暂停
- 生成器函数调用后返回一个生成器Generator 生成器本身是特殊的迭代器
- 生成器的执行需要对生成器对象Generator调用next()方法
function* foo(){
yield console.log("step1")
yield console.log("step2")
yield console.log("step3")
}
// 生成器对象
const fooGenerator = foo()
console.log(fooGenerator.next())
console.log(fooGenerator.next())
console.log(fooGenerator.next())
通过生成器对象,我们可以一步一步的执行生成器函数,每次调用.next 会在对应的yield标记处停下,并且处理参数传递和返回值。
请区分 生成器函数 和 生成器
生成器函数是用 function* 声明 并且用yield标记暂停的函数
生成器是生成器函数调用返回的特殊的迭代器 通过生成器.next可以逐步执行生成器函数
生成器函数的调用并不立刻执行函数
参数和返回值
1. Next函数的返回值为下一个yield右边表达式返回的值
2. Next函数可以传递参数,作为当前yield左边LHS操作的赋值 (结合上面说的,迭代器协议 next函数可以不传递参数或者传递一个参数)
3. next第一次传递的参数没用作用 会被忽略,一般情况下通过生成器函数的参数来传递第一步所需的值
举例说明:
function* foo() {
console.log('init')
const p1 = yield 'return 1'
console.log("step1", p1)
const p2 = yield 'return 2'
console.log("step2", p2)
const p3 = yield 'return 3'
console.log("step3", p3)
return 'end'
}
// 生成器对象
const fooGenerator = foo()
console.log(fooGenerator.next(1))
console.log(fooGenerator.next(2))
console.log(fooGenerator.next(3))
console.log(fooGenerator.next(4))
运行过程如下:
结果如下:
生成器终止
生成器是特殊的迭代器,除了next方法, 还实现了return方法和throw方法
使用生成器.return() 方法可以提前结束生成器函数的运行
使用生成器.throw(err message) 可以向生成器函数内部抛出一个异常,同样可以提前结束生成器函数的运行,但是需要注意用throw的时候需要用try catch处理异常
用生成器代替迭代器
使用生成器函数可以自动生成一个特殊的迭代器(生成器)借此我们可以简化迭代器
e.g. 数组迭代器,使用生成器函数生成的迭代器,在每次运行next的时候都会运行奥yield标志处暂停,并且返回yield后的值,直到下一次next运行,使用生成器的方式可以更好的“生成”迭代器
const names = ['abc', 'def', 'ghi', 'xxx', 21312312, { test: 1 }]
// 迭代器工厂
function* createArrayGenerator(arr) {
for(let i=0;i<arr.length;i++){
yield arr[i]
}
}
const arrayGenerator = createArrayGenerator(names)
console.log(arrayGenerator.next()?.value)
console.log(arrayGenerator.next()?.value)
console.log(arrayGenerator.next()?.value)
e.g. 如果要写在类中,可以直接用 *[Symbol.iterator](){}的形式:
class Person {
constructor(name, age, height, friends) {
this.name = name
this.age = age
this.height = height
this.friends = friends
}
*[Symbol.iterator]() {
const entries = Object.entries(this)
for (let e of entries) {
yield e
}
}
}
const p1 = new Person('TOM', 18, 1.80, ['jack', 'lucy', 'jasmine'])
const p2 = new Person('Jack', 17, 1.90, ['tom', 'lucy', 'jasmine'])
for (let p of p1) {
const [key, value] = p
console.log(key, value)
}
for (let p of p2) {
const [key, value] = p
console.log(key, value)
}
e.g.范围生成器 一种惰性求值
function* createRangeGenerator(start,end){
for(let i = start;i<end;i++){
yield i
}
}
const gen = createRangeGenerator(1,10)
console.log(gen.next())
惰性求值:迭代器是一种惰性求值的机制,只有在需要下一个元素时才会取值,这样可以节省内存,提高性能
yield委托
yield* 后面根一个可迭代对象,其可以传递给next一个可迭代对象,也就是yield会遍历每一个元素,并且返回
使用yield委托,可以更简化上面的生成器
const names = ['abc', 'def', 'ghi', 'xxx', 21312312, { test: 1 }]
// yield*
function* createArrayGenerator(arr) {
console.log(yield* arr) //yield* 返回undefined
}
class Person {
constructor(name, age, height, friends) {
this.name = name
this.age = age
this.height = height
this.friends = friends
}
*[Symbol.iterator]() {
yield* Object.entries(this)
}
}
生成器的异步处理
生成器也可以被用来解决异步的问题,配合Promise可以让我们更优雅的书写异步代码
回调函数+生成器
写一个函数用来延迟2s获取数据,这是一个异步的函数,这个函数传入2个参数,成功的callback和失败的callback
function fetchData(successCallback, failedCallback) {
const data = [
'迭代器 需要满足迭代协议',
'对象默认不可以迭代 Arrary Map Set等等 可以迭代',
'其中 可迭代对象需要实现迭代器',
'即 obj[Symbol.iterator] = 工厂函数 返回一个可迭代对象',
'可迭代对象需要包含 next函数,next函数需要返回对象 {value , done }',
'可迭代对象可以用 for of 遍历',
'区分 for in 会遍历原型链上enumable Object.keys 不会遍历原型链'
]
setTimeout(() => {
successCallback(data)
// failedCallback('inernal server error!')
}, 2000);
}
写一个函数用来调用改函数获取数据并且以alert的形式展示
function* queryData() {
try {
const data = yield fetchData((data) => {
console.log(gen.next(data))
}, (errMsg) => {
gen.throw(errMsg)
})
const str = data?.join('')
return str
} catch (e) {
alert(e)
}
}
var gen = queryData()
gen.next()
当生成器在第一个next开始执行之后,会卡在 yield fetch... 的位置,当fetch异步执行结束之后,再调用gen.next() 继续执行,如果执行失败则抛出异常,此时data才会接到异步获取的值 或者trycatche才能处理异常,这样就做到了让函数 “等待” 异步事件结束返回之后才继续执行。但是这种方式不够优雅,看着很凌乱,因为生成器函数是否继续执行是由fetch方法控制的,我们需要使用Promise的方式反转控制。
Promise+生成器
将上述fetch方法改成Promise形式
function fetchData() {
const data = [
'迭代器 需要满足迭代协议',
'对象默认不可以迭代 Arrary Map Set等等 可以迭代',
'其中 可迭代对象需要实现迭代器',
'即 obj[Symbol.iterator] = 工厂函数 返回一个可迭代对象',
'可迭代对象需要包含 next函数,next函数需要返回对象 {value , done }',
'可迭代对象可以用 for of 遍历',
'区分 for in 会遍历原型链上enumable Object.keys 不会遍历原型链'
]
return new Promise((resolve, reject) => {
setTimeout(() => {
// resolve(data)
reject('500 internal server error')
}, 2000);
})
}
写一个函数调用fetch并且展示
function* queryData() {
try {
const data = yield fetchData()
const resStr = data.join('')
return resStr
}
catch (e) {
alert(e)
}
}
需要写一个 runGenerator方法来控制生成器函数的流程
function runGenerator(genFunc) {
const args = [].slice.call(arguments, 1)
// 创建生成器
const generator = genFunc.apply(this, arguments)
return Promise.resolve((function handleNext(value) {
const genItem = generator.next(value)
if (genItem.done) {
return genItem.value
} else {
return Promise.resolve(genItem.value).then(
handleNext,
(e) => generator.throw(e)
)
}
})())
}
这个函数完成了生成器的创建,等待promise返回后继续调用next,判断生成器函数是否结束的流程,最终函数的返回结果是个promise。
通过使用promise的方式,实现了生成器函数的流程控制不取决于fetch(第三方库)实现了控制反转,让代码也更优雅,类似于async await 的写法。
async&await
async & await 本质上就是生成器函数+Promise的写法,通过babel我们可以看到async await转换之后的写法
async function fetchData(){
const data = await fetch()
consoel.log(data)
}
转换后如下: asyncToGenerator正式上面的 runGenerator的写法,asyncGeneratorStep被单独拆分,实际就是上面的hanleNext写法,只不过我们使用async await时,不需要自己写这个控制函数,使用webpack等工具会帮我们生成。
function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) {
try {
var info = gen[key](arg);
var value = info.value;
} catch (error) {
reject(error);
return;
}
if (info.done) {
resolve(value);
} else {
Promise.resolve(value).then(_next, _throw);
}
}
function _asyncToGenerator(fn) {
return function () {
var self = this,
args = arguments;
return new Promise(function (resolve, reject) {
var gen = fn.apply(self, args);
function _next(value) {
asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value);
}
function _throw(err) {
asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err);
}
_next(undefined);
});
};
}
function fetchData() {
return _fetchData.apply(this, arguments);
}
function _fetchData() {
_fetchData = _asyncToGenerator(
/*#__PURE__*/ _regeneratorRuntime().mark(function _callee() {
var data;
return _regeneratorRuntime().wrap(function _callee$(_context) {
while (1)
switch ((_context.prev = _context.next)) {
case 0:
_context.next = 2;
return fetch();
case 2:
data = _context.sent;
consoel.log(data);
case 4:
case "end":
return _context.stop();
}
}, _callee);
})
);
return _fetchData.apply(this, arguments);
}