JS 迭代器和生成器
1.迭代器
迭代器,Iterator,其代表一种接口,目的是为各种不同的数据结构提供统一的访问机制。
任何数据结构,只要部署了 Iterator 接口,就可以完成遍历操作!(当然要是可遍历的数据结构,像 let、Number 这种不可遍历的自然就不支持迭代器了)
首先来了解三个相关的类型:
- 可迭代对象 — 类似Array、NodeList等数据结构,当然我们也可以自己定义一个可迭代对象
- 迭代器对象 — 是指任何具备
next()方法的,且方法返回迭代结果对象的对象,其可以用于执行迭代。 - 迭代器结果对象 — 指由
next()方法返回的,具有属性value和done的对象,用于保存每次迭代的结果。
具体来说,当我们通过使用next()方法实现了一个符合迭代协议的对象时,这个对象就被称为迭代器对象。此时,next()方法会返回一个拥有属性value和done的对象,即迭代器结果对象。对于迭代器结果对象中的两个属性:value对应的是迭代时的next值;而done则是用于标识是否已经迭代到序列的最后一个值。
1.1 迭代协议
至于迭代协议 Iterator protocol ,官方文档中的大致意思是:
首先,迭代协议并不是新的内置实现或语法,而是协议。这些协议可以被任何遵循某些约定的对象来实现。
迭代协议具体分为两个协议:可迭代协议和迭代器协议
可迭代协议允许 JavaScript 对象定义或定制它们的迭代行为。而要成为一个可迭代对象,需要实现**@@iterator** 方法,即一个名为 [Symbol.iterator] 的方法,该方法没有参数,其返回值为一个符合迭代器协议的对象。
当一个对象要被迭代时(例如被放入一个 for of 循环中时),会自动地调用它的**@@iterator** 方法,然后通过该方法返回的迭代器对象获取要迭代的值。
迭代器协议定义了产生一系列值(无论是有限个还是无限个)的标准方式,当值为有限个时,所有的值都被迭代完毕后,则会返回一个默认返回值。简单来说,其定义了一个迭代器对象的实现标准,即:
- 该对象必须实现了一个**
next()** 方法 - 该对象中所有迭代器协议的方法(
next()、return()和throw())都应返回实现IteratorResult接口的对象,即拥有属性value和done的对象
至于更详细的内容,可以参阅官方文档。
1.2 哪些数据结构支持迭代器?
支持 Iterator 接口的数据结构有:
- String
- Array
- Map
- Set
- TypedArray
- 函数中的 arguments 对象
- DOM 中的 NodeList 对象
另外,需要注意的是,实际上我们也可以不使用这些数据结构,而是自己定义一个符合上面迭代协议的对象!
1.3 迭代器原理
根据上面的内容,我们可以发现,当我们迭代一个可迭代对象时,实际上是通过该对象身上的**@@iterator** 方法获得一个迭代器对象,然后重复地调用该迭代器对象中的**next()** 方法,而该方法会返回一个迭代器结果对象,其中包含了下一次迭代的值,以及一个标识是否结束的布尔值。重复操作直到该布尔值为 true 时,迭代就停止了。
1.4 迭代器的基本应用
对于可迭代对象,我们实际上并不用显示地调用**@@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
1.5 自定义的可迭代对象
根据前面的内容,要实现一个可迭代对象,首先需要实现**@@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'
}
2.生成器
2.1 生成器的基本概念和基本原理
生成器,在官方文档中的描述是这样的:
生成器函数提供了一个强大的选择:它允许你定义一个包含自有迭代算法的函数,同时它可以自动维护自己的状态。
生成器同时也是一种异步编程解决方案。
生成器函数使用**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,说明迭代已经结束!
2.2 生成器函数中的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}
2.3 生成器的应用 — 模拟异步问题
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 。
5319

被折叠的 条评论
为什么被折叠?



