迭代器,Iterator,其代表一种接口,目的是为各种不同的数据结构提供统一的访问机制。
任何数据结构,只要部署了 Iterator 接口,就可以完成遍历操作!(当然要是可遍历的数据结构,像 let、Number 这种不可遍历的自然就不支持迭代器了)
首先来了解三个相关的类型:
next()
方法的,且方法返回迭代结果对象的对象,其可以用于执行迭代。next()
方法返回的,具有属性value
和done
的对象,用于保存每次迭代的结果。具体来说,当我们通过使用next()
方法实现了一个符合迭代协议的对象时,这个对象就被称为迭代器对象。此时,next()
方法会返回一个拥有属性value
和done
的对象,即迭代器结果对象。对于迭代器结果对象中的两个属性:value
对应的是迭代时的next值;而done
则是用于标识是否已经迭代到序列的最后一个值。
至于迭代协议 Iterator protocol ,官方文档中的大致意思是:
首先,迭代协议并不是新的内置实现或语法,而是协议。这些协议可以被任何遵循某些约定的对象来实现。
迭代协议具体分为两个协议:可迭代协议和迭代器协议
可迭代协议允许 JavaScript 对象定义或定制它们的迭代行为。而要成为一个可迭代对象,需要实现**@@iterator
** 方法,即一个名为 [Symbol.iterator] 的方法,该方法没有参数,其返回值为一个符合迭代器协议的对象。
当一个对象要被迭代时(例如被放入一个 for of 循环中时),会自动地调用它的**@@iterator
** 方法,然后通过该方法返回的迭代器对象获取要迭代的值。
迭代器协议定义了产生一系列值(无论是有限个还是无限个)的标准方式,当值为有限个时,所有的值都被迭代完毕后,则会返回一个默认返回值。简单来说,其定义了一个迭代器对象的实现标准,即:
next()
** 方法next()
、return()
和 throw()
)都应返回实现 IteratorResult
接口的对象,即拥有属性value
和done
的对象至于更详细的内容,可以参阅官方文档。
支持 Iterator 接口的数据结构有:
另外,需要注意的是,实际上我们也可以不使用这些数据结构,而是自己定义一个符合上面迭代协议的对象!
根据上面的内容,我们可以发现,当我们迭代一个可迭代对象时,实际上是通过该对象身上的**@@iterator
** 方法获得一个迭代器对象,然后重复地调用该迭代器对象中的**next()
** 方法,而该方法会返回一个迭代器结果对象,其中包含了下一次迭代的值,以及一个标识是否结束的布尔值。重复操作直到该布尔值为 true 时,迭代就停止了。
对于可迭代对象,我们实际上并不用显示地调用**@@iterator
** 方法,而是可以通过 for of、扩展操作符等直接操作!
// 通过 for of 操作可迭代对象
const Iterable = ['1', '2', '3', '234', '555'];
for (let v of Iterable) {
console.log(v) // '1', '2', '3', '234', '555'
}
// 通过扩展操作符操作可迭代对象
data = [1, 2, 3, 4, 5, 6, 7, 8];
console.log(Math.max(...data)); // 8
另外,迭代器也可以用于 ES6 中的解构赋值:
let arr = [10 , 18 , 20];
let [a , b , c] = arr;
console.log(c); // 20
根据前面的内容,要实现一个可迭代对象,首先需要实现**@@iterator
** 方法,其需要返回一个内置了**next()
** 方法的迭代器对象。next()
方法需要能够实现迭代,并返回一个拥有属性value
和done
的迭代器结果对象。
因此,实现如下:
const myIterable = {
name: 'my iterable obj',
data: ['test1', 'iterable', '222', 'test2', 'ababab'],
// 迭代器方法
[Symbol.iterator]() {
// 索引
let index = 0;
// 返回一个迭代器对象
return {
// next()方法
next: () => {
if (index < this.data.length) {
const res = {
value: this.data[index],
done: false,
};
index++;
return res;
} else {
return {
value: undefined,
done: true,
};
}
},
};
},
};
// 通过 for of 遍历该可迭代对象
for (let v of myIterable) {
console.log(v) // 'test1', 'iterable', '222', 'test2', 'ababab'
}
生成器,在官方文档中的描述是这样的:
生成器函数提供了一个强大的选择:它允许你定义一个包含自有迭代算法的函数,同时它可以自动维护自己的状态。
生成器同时也是一种异步编程解决方案。
生成器函数使用**function*
**语法编写。最初调用时,生成器函数不执行任何代码,而是返回一种称为 Generator 的迭代器。
而通过调用生成器的**next()
**方法消耗值时,生成器函数才会执行,并一直运行直到遇到 yield
关键字。即,第一次调用next()
方法时,生成器函数会一直执行到其代码中的第一个 yield
关键字处;第二次调用next()
方法时,生成器函数会从第一个 yield
关键字处执行到第二个 yield
关键字处。
下面是一个例子:
// 定义一个生成器函数
function* fn() {
console.log('生成器函数第一次执行');
yield 'result1';
console.log('生成器函数第二次执行');
yield 'result2';
console.log('生成器函数第三次执行');
yield 'result3';
console.log('生成器函数第四次执行');
}
// 调用生成器函数,得到一个生成器对象
let gen = fn() // 此处生成器函数内部的代码并不会执行!仅仅是得到了一个生成器对象
// 调用生成器对象的next()方法
console.log(gen.next()) // 生成器函数第一次执行 {value: 'result1', done: false}
console.log(gen.next()) // 生成器函数第二次执行 {value: 'result2', done: false}
console.log(gen.next()) // 生成器函数第三次执行 {value: 'result3', done: false}
console.log(gen.next()) // 生成器函数第四次执行 {value: undefined, done: true}
可以看到,第一次调用next()
方法时,生成器函数内部的代码仅执行到第一个 yield
关键字处,并将 yield
关键字后表达式的值作为next()
方法所返回的迭代器结果对象中value
的值!
而第二次调用next()
方法时,则是从第一个 yield
关键字后开始执行,直到遇到第二个 yield
关键字,并同样地将yield
关键字后表达式的值作为next()
方法所返回的迭代器结果对象中value
的值返回出去…重复此操作,直到最后一次调用next()
方法,此时从最后一个 yield
关键字后开始执行,直到函数结束,且此时返回的值中,value
的值为undefined
,而done
的值为true
,说明迭代已经结束!
yield
通过上面的例子,我们可以了解到,执行next()
方法后,生成器函数会将某一个yield
关键字后面的表达式的值作为next()
方法的返回值,并将函数暂停在yield
关键字处。
而上面的例子中没有提及的是,如果我们在调用next()
方法时,传入参数,那么我们传入的参数会变成上一次暂停处的yield
表达式的值!
下面通过一个例子来理解:
// 定义一个生成器函数
function* fn(str) {
console.log(str);
let a = yield 1; // 第一次调用暂停的位置
console.log(a);
let b = yield 2; // 第二次调用暂停的位置
console.log(b);
let c = yield 3; // 第三次调用暂停的位置
console.log(c);
return 100
}
// 获取生成器对象
let gen = fn('测试传参')
// 第一次调用next方法,传入一个参数 'a'
console.log(gen.next('a'))
// 此时是第一次调用,该参数会被丢弃,因为没有上一次暂停处!
// 执行结果为:测试传参 {value: 1, done: false}
// 第二次调用,传入一个参数 'b'
console.log(gen.next('b'))
// 此时有上一次调用暂停的位置:let a = yield 1;
// 此时 'yield 1' 整个表达式的值被赋为我们传入的参数 'b'
// 因此本次的执行结果为:b {value: 2, done: false}
// 第三次调用,传入一个参数 'c'
console.log(gen.next('c'))
// 与上面同理,上一次暂停位置的yield表达式 'yield 2' 的值被赋为 'c'
// 所以本次执行结果为:c {value: 3, done: false}
// 最后一次调用,传入参数 'd'
console.log(gen.next('d'))
// 还是同理,'yield 3' 的值被赋为 'd',另外,生成器函数中有return,所以这里迭代器结果对象中的value有值了
// d {value: 100, done: false}
function fa() {
setTimeout(() => {
let yon = '用户数据';
//传入参数并调用
han.next(yon);
}, 1000);
}
function fb() {
setTimeout(() => {
let din = '订单数据';
han.next(din);
}, 1000);
}
function fc() {
setTimeout(() => {
let shang = '商品数据';
han.next(shang);
}, 1000);
}
function* fn() {
let a = yield fa();
console.log(a); //1s 用户数据
let b = yield fb();
console.log(b); //2s 订单数据
let c = yield fc();
console.log(c); //3s 商品数据
}
let han = fn();
//调用
han.next();
总的来说,生成器是ES6中提供的一种异步编程的解决方法,但通过上面的例子可以看到,单纯地利用生成器来实现异步操作,会导致代码可读性比较低,在实际开发中使用更多的还是 async/await 和 promise 。