迭代器与生成器
什么是迭代器?
概念(维基百科): 迭代器(iterator),是使用户可在容器对象(container,例如链表或数组)上遍访的对象[1][2][3],设计人员使用此接口无需关心容器对象的内存分配的实现细节。
JS 中的迭代器
- 其本质就是一个对象,符合迭代器协议(iterator protocol)
- 迭代器协议
- 其对象返回一个 next 函数
- 调用 next 函数返回一个对象,其对象中包含两个属性
done
(完成),它的值为布尔类型,也就是true/false
。- 如果这个迭代器没有迭代完成即返回
{done:false}
- 当这个迭代器完成了即返回
{done:true}
- 如果这个迭代器没有迭代完成即返回
value
(值),它可以返回 js 中的任何值,TS 中表示可为:value:any
类型
迭代器的基本实现
思考以下代码:
let index = 0;
const bears = ["ice", "panda", "grizzly"];
let iterator = {
next() {
if (index < bears.length) {
return { done: false, value: bears[index++] };
}
return { done: true, value: undefined };
},
};
console.log(iterator.next()); //{ done: false, value: 'ice' }
console.log(iterator.next()); //{ done: false, value: 'panda' }
console.log(iterator.next()); //{ done: false, value: 'grizzly' }
console.log(iterator.next()); //{ done: true, value: undefined }
- 是一个对象,实现了
next
方法,next
方法返回了一个对象,有done
属性和value
属性,且key
的值类型也为boolean
或any
,符合迭代器协议,是一个妥妥的迭代器没跑了。 - 弊端
- 违背了高内聚思想,明明
index
和iterator
对象是属于一个整体,我却使用了全局变量,从 V8 引擎的 GC,可达性(也就是标记清除)来看,如果bears = null
,不手动设置为 null 很有可能会造成内存泄漏,并且内聚性低。 - 假如我要创建一百个迭代器对象呢? 那我就自己定义一百遍吗?肯定错误的,我们要把它封装起来,这样内聚性又高,又能进行复用,一举两得,一石二鸟,真的是
very beautiful
,very 优雅。
- 违背了高内聚思想,明明
迭代器的封装实现
思考一下代码:
const bears = ["ice", "panda", "grizzly"];
function createArrIterator(arr) {
let index = 0;
let _iterator = {
next() {
if (index < arr.length) {
return { done: false, value: arr[index++] };
}
return { done: true, value: undefined };
},
};
return _iterator;
}
let iter = createArrIterator(bears);
console.log(iter.next());
console.log(iter.next());
console.log(iter.next());
console.log(iter.next());
内聚性非常高,尽最大可能进行了复用,减少冗余代码
什么是可迭代对象?
迭代器对象和可迭代对象是一个不同的东西,虽然它们存在关联,而且面试的时候经常面这些概念,废话不多说,我们直接进入主题。
- 首先就是一个对象,且符合可迭代对象协议(iterable protocol)
- 可迭代对象协议
- 实现了[Symbol.iterator]为 key 的方法,且这个方法返回了一个迭代器对象
- 绕了一大圈终于把概念搞明白了,那可迭代对象有什么好处呢? 有什么应用场景呢?
- for of 的时候,其本质就是调用的这个函数,也就是[Symbol.iterator]为 key 的方法
原生可迭代对象(JS 内置)
- String
- Array
- Set
- NodeList 类数组对象
- Arguments 类数组对象
- Map
部分 for of 演示
let str = "The Three Bears";
const bears = ["ice", "panda", "grizzly"];
for (let text of str) {
console.log(text); // 字符串每个遍历打印
}
for (let bear of bears) {
console.log(bear); // ice panda grizzly
}
查看内置的[Symbol.iterator]方法
上面给大家举例了很多可迭代对象,那它们必定是符合可迭代对象协议的,思考以下代码
const bears = ["ice", "panda", "grizzly"];
// 数组的Symbol.iterator方法
const iter = bears[Symbol.iterator]();
console.log(iter.next());
console.log(iter.next());
console.log(iter.next());
console.log(iter.next());
const nickName = "ice";
//字符串的Symbol.iterator方法
const strIter = nickName[Symbol.iterator]();
console.log(strIter.next());
console.log(strIter.next());
console.log(strIter.next());
console.log(strIter.next());
可迭代对象的实现
let info = {
bears: ["ice", "panda", "grizzly"],
[Symbol.iterator]: function () {
let index = 0;
let _iterator = {
//这里一定要箭头函数,或者手动保存上层作用域的this
next: () => {
if (index < this.bears.length) {
return { done: false, value: this.bears[index++] };
}
return { done: true, value: undefined };
},
};
return _iterator;
},
};
let iter = info[Symbol.iterator]();
console.log(iter.next());
console.log(iter.next());
console.log(iter.next());
console.log(iter.next());
//符合可迭代对象协议 就可以利用 for of 遍历
for (let bear of info) {
console.log(bear);
}
//ice panda grizzly
- 符合可迭代对象协议,是一个对象,有
[Symbol.iterator]
方法,并且这个方法返回了一个迭代器对象。 - 当我利用 for of 遍历,就会自动的调用这个方法。
可迭代对象的应用
- for of
- 展开语法
- 解构语法
- promise.all(iterable)
- promise.race(iterable)
- Array.from(iterable)
- ...
自定义类迭代实现
class myInfo {
constructor(name, age, friends) {
this.name = name;
this.age = age;
this.friends = friends;
}
[Symbol.iterator]() {
let index = 0;
let _iterator = {
next: () => {
const friends = this.friends;
if (index < friends.length) {
return { done: false, value: friends[index++] };
}
return { done: true, value: undefined };
},
};
return _iterator;
}
}
const info = new myInfo("ice", 22, ["panda", "grizzly"]);
for (let bear of info) {
console.log(bear);
}
//panda
//grizzly
- 此案例只是简单的对
friends
进行了迭代,你也可以迭代你想要的一切东西... - 记住此案例,后续我们会对这个案例进行重构,优雅的会让你不能用言语来形容。
生成器函数
生成器是 ES6 新增的一种可以对函数控制的方案,能灵活的控制函数的暂停执行,继续执行等。
生成器函数和普通函数的不同
- 定义: 普通函数
function
定义,生成器函数function*
,要在后面加*
- 生成器函数可以通过
yield
来控制函数的执行 - 生成器函数返回一个生成器(generator),生成器是一个特殊的迭代器
生成器函数基本实现
function* bar() {
console.log("fn run");
}
bar();
我们会发现,这个函数竟然没有执行。我们前面说过,它是一个生成器函数,它的返回值是一个生成器,同时也是一个特殊的迭代器,所以跟普通函数相比,好像暂停了,那如何让他执行呢?接下来我们进一步探讨。
生成器函数单次执行
function* bar() {
console.log("fn run");
}
const generator = bar();
console.log(generator.next());
//fn run
//{ value: undefined, done: true }
返回了一个生成器,我们调用 next 方法就可以让函数执行,并且 next 方法是有返回值的,我们上面讲迭代器的时候有探讨过,而 value 没有返回值那就是 undefined。那上面说的 yield 关键字在哪,到底是如何控制函数的呢?是如何用的呢?
生成器函数多次执行
function* bar() {
console.log("fn run start");
yield 100;
console.log("fn run...");
yield 200;
console.log("fn run end");
return 300;
}
const generator = bar();
// 1. 执行到第一个yield,暂停之后,并且把yield的返回值 传入到value中
console.log(generator.next());
// 2. 执行到第一个yield,暂停之后,并且把yield的返回值 传入到value中
console.log(generator.next());
// 3. 执行剩余代码
console.log(generator.next());
// 打印结果:
// fn run start
// {done:false, value: 100}
// fn run...
// {done:false, value: 200}
// fn run end
// {done:true, value: 300}
现在我们恍然大悟,每当调用 next 方法的时候,代码就会开始执行,执行到yield x
,后就会暂停,等待下一次调用 next 继续往下执行,周而复始,没有了yield
关键字,进行最后一次 next 调用返回done:true
。
生成器函数的分段传参
我有一个需求,既然生成器能控制函数分段执行,我要你实现一个分段传参。
思考以下代码:
function* bar(nickName) {
const str1 = yield nickName;
const str2 = yield str1 + nickName;
return str2 + str1 + nickName;
}
const generator = bar("ice");
console.log(generator.next());
console.log(generator.next("panda "));
console.log(generator.next("grizzly "));
console.log(generator.next());
// { value: 'ice', done: false }
// { value: 'panda ice', done: false }
// { value: 'grizzly panda ice', done: true }
// { value: undefined, done: true }
如果没有接触过这样的代码会比较奇怪
- 当我调用 next 函数的时候,yield 的左侧是可以接受参数的,也并不是所有的 next 方法的实参都能传递到生成器函数内部
- yield 左侧接收的,是第二次调用 next 传入的实参,那第一次传入的就没有 yield 关键字接收,所有只有当我调用 bar 函数的时候传入。
- 最后一次 next 调用,传入的参数我也调用不了,因为没有 yield 关键字可以接收了。
- 很多开发者会疑惑,这样写有什么用呢? 可读性还差,但是在处理异步数据的时候就非常有用了,后续会在 promise 中文章中介绍。
生成器代替迭代器
前面我们讲到,生成器是一个特殊的迭代器,那生成器必定是可以代替迭代器对象的,思考以下代码。
let bears = ["ice", "panda", "grizzly"];
function* createArrIterator(bears) {
for (let bear of bears) {
yield bear;
}
}
const generator = createArrIterator(bears);
console.log(generator.next());
console.log(generator.next());
console.log(generator.next());
console.log(generator.next());
其实这里还有一种语法糖的写法 yield*
- yield* 依次迭代这个可迭代对象,相当于遍历拿出每一项 yield item(伪代码)
思考以下代码:
let bears = ["ice", "panda", "grizzly"];
function* createArrIterator(bears) {
yield* bears;
}
const generator = createArrIterator(bears);
console.log(generator.next());
console.log(generator.next());
console.log(generator.next());
console.log(generator.next());
依次迭代这个可迭代对象,返回每个 item 值
可迭代对象的终极封装
class myInfo {
constructor(name, age, friends) {
this.name = name;
this.age = age;
this.friends = friends;
}
*[Symbol.iterator]() {
yield* this.friends;
}
}
const info = new myInfo("ice", 22, ["panda", "grizzly"]);
for (let bear of info) {
console.log(bear);
}
//panda
//grizzly
回顾以下可迭代对象协议
- 是一个对象并且有[Symbol.iterator]方法
- 这个方法返回一个迭代器对象 生成器函数返回一个生成器,是一个特殊的迭代器
总结
迭代器对象
- 本质就是一个对象,要符合迭代器协议
- 有自己对应的 next 方法,next 方法则返回一组数据
{done:boolean, value:any}
可迭代对象
- 本质就是对象,要符合可迭代对象协议
- 有
[Symbol.iterator]
方法,并且调用这个方法返回一个迭代器
生成器函数
- 可以控制函数的暂停执行和继续执行
- 通过
function* bar() {}
这种形式定义 - 不会立马执行,而是返回一个生成器,生成器是一个特殊的迭代器对象
yield
关键字可以控制函数分段执行- 调用返回生成器的 next 方法进行执行
结语
坚持自律简单二字,却贯穿了我的前半生,我希望我能坚持做一件事,每天都能有所进步。