Iterator & generator
Iterator
iterator 迭代器是使用户可以对某个数据结构进行遍历的对象,使用该接口无需关心对象的内部实现细节
这里的数据结构在JavaScript中包括数组、String、Set、Map、arguments 对象、NodeList 集合。这些对象内置了迭代器。
在JavaScript中,迭代器是一个具体的对象,这个对象要符合迭代器协议(实现 next 函数)
next 函数
这是一个函数,返回一个应当拥有以下两个属性的对象
done:如果迭代器可以产生序列中的下一个值,则返回false,如果已经迭代完毕,没有后续,那就是truevalue:迭代器返回的任何JavaScript值
const arr = ["a", "b", "c"];
const iterator = arr[Symbol.iterator](); // js的迭代器存在于对象的 [Symbol.iterator] 属性上
console.log(iterator.next()); // { value: 'a', done: false }
console.log(iterator.next()); // { value: 'b', done: false }
console.log(iterator.next()); // { value: 'c', done: false }
console.log(iterator.next()); // { value: undefined, done: true }
实现 Symbol.iterator
想要实现上例中原生可迭代对象的迭代器还是比较简单的,只要记录 next 被调用的次数就可以,我们这里用一个闭包来实现
function myIterator() {
let index = 0;
return {
next: () => {
return index < this.length
? { value: this[index++], done: false }
: { value: undefined, done: true };
},
};
}
// 测试用例
Array.prototype[Symbol.myIterator] = myIterator;
const iterator = ["a", "b", "c"][Symbol.myIterator]();
console.log(iterator.next()); // { value: 'a', done: false }
console.log(iterator.next()); // { value: 'b', done: false }
console.log(iterator.next()); // { value: 'c', done: false }
console.log(iterator.next()); // { value: undefined, done: true }
应用场景
上文提到了可迭代对象,实现了 [Symbol.iterator] 的对象就可以被称为可迭代对象。而这些可迭代对象可以使用一些语法糖,如 for...of、...展开语法等
对应的,非可迭代对象使用这些语法糖就会报错
const obj = { // 默认对象不是可迭代的
values: ["a", "b", "c"]
}
for (const value of obj) {
console.log(value); // TypeError: obj is not iterable
}
console.log(...obj) // TypeError: Found non-callable @@iterator
我们可以添上自定义内置的迭代器
const obj = {
values: ["a", "b", "c"],
[Symbol.iterator]: function () { // 自定义可迭代对象
let index = 0;
return {
next: () => {
return index < this.values.length
? { value: this.values[index++], done: false }
: { value: undefined, done: true };
},
};
},
};
for (const value of obj) {
console.log(value); // a b c
}
console.log(...obj); // a b c
要注意的是,语法糖的停止条件都是 done 的值,如果 done 为 true 就会停止,不然会一直搜索下去: 在 for...of 中会陷入死循环(一直输出 undefined),...语法 则是会在当前语句卡住不输出任何值。
其它常用场景
- 解构语法
const [a, b] = iterableObj - 创建其他对象
new Set(iterableObj) - 一些API
Array.from(iterableObj)、Promise.all(iterableObj)...
自定义相关类
既然已经知道了怎么写内置迭代器和迭代器的应用场景,那么实现一个跟 Array 一样可迭代的类已经很轻松了
class Person {
constructor(...args) {
this.names = args;
}
[Symbol.iterator]() {
let index = 0;
return {
next: () => {
return index < this.names.length
? { value: this.names[index++], done: false }
: { value: undefined, done: true };
},
};
}
}
// 测试用例
let group = new Person("neo", "john", "amy");
console.log(...group); // neo john amy
Generator
generator 生成器是一种特殊的迭代器,用于控制函数什么时候暂停与执行。在开发中并不多见,但是它与 Promise 构成的语法糖 async / await 则是特别高频的被使用。
虽然说生成器是一个特殊的迭代器,但是它并不是直接通过类似于 [Symbol.iterator] 这种函数返回的,而是通过生成器函数返回的
生成器函数
关于生成器函数只要关注这四点
- 通过
function *声明 - 通过
yield关键字控制函数执行流程 - 返回值是个
generator - 返回的
generator通过next()来操作下一步的流程
举个例子
function* foo() {
console.log("start");
yield;
console.log("1");
yield;
console.log("2");
yield;
console.log("finish");
}
// 测试用例
const generator = foo();
generator.next(); // start
generator.next(); // 1
generator.next(); // 2
generator.next(); // finish
yield
yield 是个关键字,可以返回一个值,也可以接收值
基本使用
先来看看返回值
function* foo() {
// 可以像 return 一样返回值
yield 1;
yield 2;
}
const generator = foo();
const res1 = generator.next(); // { value: 1, done: false }
const res2 = generator.next(); // { value: 2, done: false }
const res3 = generator.next(); // { value: undefined, done: true }
console.log(res1.value, res2.value, res3.value); // 1 2 undefined
再来看看接收值
function* foo() {
console.log("start");
const value1 = yield;
console.log(value1); // bbb
console.log(yield); // ccc
console.log("finish");
}
const generator = foo();
generator.next("aaa");
generator.next("bbb");
generator.next("ccc");
当然,也可以组合使用
function* foo() {
console.log("start");
const value1 = yield 1;
console.log(value1); // bbb
console.log(yield 2); // ccc
console.log("finish");
}
const generator = foo();
const res1 = generator.next("aaa").value;
const res2 = generator.next("bbb").value;
const res3 = generator.next("ccc").value;
console.log(res1, res2, res3); // 1 2 undefined
暂停执行时机
看完几个例子,第一个疑惑可能是,我在 next第一个传入的不是 aaa 吗,怎么没有被打印出来?
因为第一个 next 只是运行了第一个 yield 前面的代码片段,也没有 yield 可以接收第一个传入的参数。
那么随之而来第二个问题,第一个代码片段,是在哪里停止的,是含有 yield 的当前一行还是 yield 的上一行呢?
这里可以用一个例子来试试看
function* foo() {
console.log("start");
console.log(yield 1);
console.log("finish");
}
const generator = foo();
const res1 = generator.next("aaa").value;
console.log(res1);
// generator.next("bbb");
/* output
start
1
*/
这里的 res1 是有值的,而且还是 yield 返回的值,所以可以初步确定代码是执行到有 yield 的这一行。但是此时我们 console.log(yield 1) 并没有执行(甚至没有输出 undefined),那么我们可以猜想,yield 这里的语句可以被拆成两步
- 返回一个值
- 接收一个值并执行当前行语句
用代码表示console.log(yield 1)的两步如下
return 1 // 步骤一(与实际的 return 不同,在generator中使用return会将生成器的状态done设为true,终止生成器的活动)
console.log(yield) // 步骤二(这里的yield代指通过启动第二个代码片段的next传入的参数)
我们启动最开始的例子里面的下一个next来验证一下
function* foo() {
console.log("start");
console.log(yield 1);
console.log("finish");
}
const generator = foo();
const res1 = generator.next("aaa").value;
console.log(res1);
generator.next("bbb");
/* output
start
1
bbb
finish
*/
输出是符合预期的,至此证明了至少在实现的层面,可以将含有yield这一行的代码理解为两个步骤的结合
返回一个可迭代对象
通过 yield 返回一个可迭代对象,可以分段触发迭代器。是下面代码的语法糖
function* foo(arr) {
for (const item of arr) {
yield item;
}
}
const generator = foo([1, 2, 3]);
const res = [];
for (let i = 0; i < 4; i++) {
res.push(generator.next().value);
}
console.log(res); // [ 1, 2, 3, undefined ]
返回可迭代对象写法 yield*
function* foo(arr) {
yield* arr; // yield 后面记得带 *
}
const generator = foo([1, 2, 3]);
const res = [];
for (let i = 0; i < 4; i++) {
res.push(generator.next().value);
}
console.log(res); // [ 1, 2, 3, undefined ]
小结
最后,其实我们可以用生成器来简单实现自定义迭代器的功能
class Person {
constructor(...args) {
this.names = args;
}
[Symbol.iterator] = function* foo() {
yield* this.names;
};
}
let group = new Person("neo", "john", "amy");
console.log(...group); // neo john amy
后续
...正在施工中