迭代器
什么是迭代器?
迭代器(iterator),是确使用户可在容器对象(container,例如链表或数组)上遍访的对象,使用该接口无需关心对象的内部实现细节。它提供了一种获取一系列值(无论是有限还是无限个)的标准方式。
其行为像数据库中的光标,迭代器最早出现在1974年设计的CLU编程语言中;
在各种编程语言的实现中,迭代器的实现方式各不相同,但是基本都有迭代器,比如Java、Python等;
说人话就是:迭代器是一个工具对象,可以帮助我们对某个数据结构进行遍历。
JavaScript 中的迭代器
在JavaScript中,迭代器也是一个具体的对象,这个对象需要符合迭代器协议规范(iterator protocol):
迭代器要求有一个获取值的标准方式,在 js 中这个标准方式就是迭代器对象中一个特定的 next 方法。
next 方法有如下的要求:
- 是一个无参数或者一个参数的函数,返回一个应当拥有以下两个属性的对象:
- done(boolean)
如果迭代器可以产生序列中的下一个值,则为 false。(这等价于没有指定 done 这个属性。)
如果迭代器已将序列迭代完毕,则为 true。这种情况下,value 是可选的,如果它依然存在,即为迭代结束之后的默认返回值。
- value
迭代器返回的任何 JavaScript 值。done 为 true 时可省略。
说人话就是:next 函数一个一个遍历容器,并返回一个对象。对象中有两个属性,done:表示该容器中是否还有值,如果没有值了为 true,表示遍历可以结束了;value:表示当前遍历出来的值,如果容器中没有值了,value 就是 undefined,此时 done 就是 true。
以一个数组为例:
// 待遍历的数组
const arr = ['nba', 'cba', 'mba']
// 数组迭代器迭代上面的数组
let index = 0
const iteratorArr = {
next: function () {
if (index < arr.length) {
return { done: false, value: arr[index++] }
} else {
return { done: true, value: undefined }
}
}
}
console.log(iteratorArr.next());
console.log(iteratorArr.next());
console.log(iteratorArr.next());
console.log(iteratorArr.next());
console.log(iteratorArr.next());
// { done: false, value: 'nba' }
// { done: false, value: 'cba' }
// { done: false, value: 'mba' }
// { done: true, value: undefined }
// { done: true, value: undefined }
什么是可迭代对象?
它和迭代器是不同的概念;
当一个对象实现了iterable protocol 可迭代协议时,它就是一个可迭代对象;
这个对象要求必须实现迭代器方法,并且要求使用 Symbol.iterator 作为迭代器方法的 key;
什么是可迭代协议?
可迭代协议就像一个接口,要想能被迭代,就得实现这个接口,也就是得按接口的规矩办事。很多容器就实现了这个接口,所以我们可以遍历出容器中的元素。换句话说,很多容器对象都是可迭代对象,并且是否实现可迭代协议,也是一个容器能否被遍历取值的必要条件。(难怪 Java 中这些集合都实现了 iterator 接口)
一个可迭代容器示例:
// 一个写死的容器(可迭代对象)
const iterableObj = {
// 假设容器中已经存了这些数据
arr: ["abc", "cba", "nba"],
// 迭代器方法也是写死的,只能遍历上面的测试数据
[Symbol.iterator]: function() {
let index = 0
return {
next: () => { // 小细节:迭代器对象中要想访问到测试数据数组,得用箭头函数,
if (index < this.arr.length) { // 因为 this 可以跳过迭代器对象作用域指向容器
return { done: false, value: this.arr[index++] }
} else {
return { done: true, value: undefined }
}
}
}
}
}
// 获取该容器的迭代器
const iteratorArr = iterableObj[Symbol.iterator]()
console.log(iteratorArr.next());
console.log(iteratorArr.next());
console.log(iteratorArr.next());
console.log(iteratorArr.next());
// { done: false, value: 'abc' }
// { done: false, value: 'cba' }
// { done: false, value: 'nba' }
// { done: true, value: undefined }
当一个容器实现可迭代协议成为一个可迭代容器后,我们就可以利用这个容器可被迭代的能力,添加一些语法糖,来帮助使用容器的人更好的遍历容器取值。
比如 for … of 就是一个方便我们对可迭代对象进行迭代的语法糖。
// 一个写死的容器(可迭代对象)
const iterableObj = {
// 假设容器中已经存了这些数据
arr: ["abc", "cba", "nba"],
// 迭代器方法也是写死的,只能遍历上面的测试数据
[Symbol.iterator]: function() {
let index = 0
return {
next: () => { // 小细节:迭代器对象中要想访问到测试数据数组,得用箭头函数,
if (index < this.arr.length) { // 因为 this 可以跳过迭代器对象作用域指向容器
return { done: false, value: this.arr[index++] }
} else {
return { done: true, value: undefined }
}
}
}
}
}
for (const item of iterableObj) {
console.log(item);
}
// 很方便的取出了值,而不用我们去 value 里面扣,这个糖真不错
// abc
// cba
// nba
原生迭代器对象
JavaScript 中原生的容器都实现了可迭代协议,所以它们都是可迭代的,可迭代对象就是这些容器的实例。
- String、Array、Map、Set、arguments对象、NodeList集合 ```javascript // String for (const iterator of “hello”) { console.log(iterator); // h e l l o }
// array const arr = [‘abc’, ‘cba’, ‘nba’] for (const iterator of arr) { console.log(iterator); // abc cba nba }
// Map const map = new Map() map.set(‘key1’, ‘a’) map.set(‘key2’, ‘b’) map.set(‘key3’, ‘c’) for (const iterator of map) { console.log(iterator) // [ ‘key1’, ‘a’ ] [ ‘key2’, ‘b’ ] [ ‘key3’, ‘c’ ] }
// Set const set = new Set() set.add(‘a’) set.add(‘b’) set.add(‘c’) for (const iterator of set) { console.log(iterator); // a b c }
// 函数中arguments也是一个可迭代对象 function foo(x, y, z) { for (const arg of arguments) { console.log(arg) } } foo(10, 20, 30) // 10 20 30
<a name="Qp6eW"></a>
## 可迭代对象的应用体现
可迭代对象的应用体现在很多方面:
- JavaScript中语法:for ...of、展开语法(spread syntax)、yield*(后面讲)、解构赋值(Destructuring_assignment);
- 创建一些对象时:new Map([Iterable])、new WeakMap([iterable])、new Set([iterable])、new WeakSet([iterable]);
- 一些方法的调用:Promise.all(iterable)、Promise.race(iterable)、Array.from(iterable);
```javascript
// 1.for of场景
// 2.展开语法(spread syntax)
const iterableObj = {
names: ["abc", "cba", "nba"],
[Symbol.iterator]: function() {
let index = 0
return {
next: () => {
if (index < this.names.length) {
return { done: false, value: this.names[index++] }
} else {
return { done: true, value: undefined }
}
}
}
}
}
const arr = [1, 2, 3]
const newNames = [...arr, ...iterableObj]
console.log(newNames) // [ 1, 2, 3, 'abc', 'cba', 'nba' ]
// 普通对象为什么也能用展开语法?
const obj = { name: 'zs', age: 20}
const objarr = {...obj}
console.log(objarr); // { name: 'zs', age: 20 }
// 因为这是 ES9(ES2018) 作了特殊处理,新增的一个特性: 内部实现用的不是迭代器。
// ES9 之前是不能用展开语法的
// 3.解构语法
const [ name1, name2 ] = arr
// 普通对象也能解构,也是 ES9 特意的处理,让解构支持普通对象,内部实现也不是迭代器
const { name, age } = obj
console.log(name, age); // zs 20
// 4.创建一些其他对象时,它会要求你传入一个可迭代对象,要不然报错
// 这时我们就能知道什么能传什么不能传,更方便的阅读 API
const set1 = new Set(iterableObj) // 自定义的容器实例
const set2 = new Set(arr) // 数组实例
const set3 = new Set('asdf') // String 实例
// 数字就不是可迭代对象,它的构造函数中没有迭代器
// const set4 = new Set(123) // number 123 is not iterable (cannot read property Symbol(Symbol.iterator))
// 5.Promise.all
// 开始我们只是传 promise 的数组,其实只要可迭代对象都行
Promise.all(iterableObj).then(res => {
console.log(res)
})
// 相当于:Promise.all( Promise.resolve(iterableObj) ).then()
手动实现一个简易可迭代容器
上面已经实现了一个写死的可迭代容器,我们可以稍微完善一下。
- 要想可迭代,这个容器就得实现可迭代协议。
- 容器中数据的存储结构,就用一个数组,并且存储结构挂载在容器属性上。
一些容器基本的方法,如添加方法,精确查找方法,删除方法就也免了。实例化的时候就添加数据。 ```javascript // 创建一个教室容器类, 创建出来的对象都是可迭代教室对象 class Classroom { constructor(address, name, students) { this.address = address this.name = name this.students = students }
Symbol.iterator { let index = 0 return { next: () => {
if (index < this.students.length) {
return { done: false, value: this.students[index++] }
} else {
return { done: true, value: undefined }
}
} } } }
const classroom = new Classroom(“3幢5楼205”, “计算机教室”, [“james”, “kobe”, “curry”])
// 迭代成功 for (const item of classroom) { console.log(item); // james kobe curry }
<a name="eclxI"></a>
## 迭代器的终止
迭代器一般情况下会迭代完所有元素,但也可再某些条件下中断:
- 比如遍历的过程中通过 break、continue、return、throw 中断了循环操作;
- 比如在解构的时候,没有解构所有的值;
那么这个时候我们想要监听中断的话,可以在迭代器中添加 return 方法:
```javascript
// 创建一个教室容器类, 创建出来的对象都是可迭代教室对象
class Classroom {
constructor(address, name, students) {
this.address = address
this.name = name
this.students = students
}
[Symbol.iterator]() {
let index = 0
return { // 迭代器
next: () => {
if (index < this.students.length) {
return { done: false, value: this.students[index++] }
} else {
return { done: true, value: undefined }
}
},
// 迭代中断后的处理
return: () => {
console.log("迭代器提前终止了~")
// 也需要返回一个对象,包含这两个属性,done 为 true
return { done: true, value: undefined }
}
}
}
}
const classroom = new Classroom("3幢5楼205", "计算机教室", ["james", "kobe", "curry" ])
for (const item of classroom) {
console.log(item); // james kobe
if(item == 'kobe') break; // 迭代器提前终止了~
}
生成器
什么是生成器?
生成器是ES6中新增的一种函数控制、使用的方案,它可以让我们更加灵活的控制函数什么时候继续执行、暂停执
行等。
平时我们会编写很多的函数,这些函数终止的条件通常是返回值或者发生了异常。
生成器函数也是一个函数,但是和普通的函数有一些区别:
- 首先,生成器函数需要在function的后面加一个符号:
*
- 其次,生成器函数可以通过
yield
关键字来控制函数的执行流程: 最后,生成器函数的返回值是一个 Generator(生成器)
生成器函数执行 yield
直接调用生成器函数是不会执行的,它只是返回了一个生成器对象,需要通过生成器对象的 next 方法才能执行函数中的代码。
执行到 yield 会中断函数执行,再次调用 next 方法就会继续往下执行。生成器事实上是一种特殊的迭代器,yield 好像把函数切成了几段。 ```javascript function* foo() { console.log(“函数开始执行~”)const value1 = 100 console.log(“第一段代码:”, value1) // 中断函数执行 yield
const value2 = 200 console.log(“第二段代码:”, value2) yield
const value3 = 300 console.log(“第三段代码:”, value3) yield
console.log(“函数执行结束~”) }
// 直接调用生成器函数是不会执行的 foo() // 调用生成器函数时, 会给我们返回一个生成器对象 const generator = foo() // 需要通过生成器对象的 next 方法执行,执行到 yield 会中断函数执行 generator.next() // 需要再次调用 next 方法 console.log(“——————-“) generator.next()
// 函数开始执行~
// 第一段代码: 100
// ——————-
// 第二段代码: 200
// generator.next()
// console.log(“—————“)
// generator.next()
生成器作为特殊的迭代器,next 函数也会返回一个拥有 done 和 value 属性的对象。
**事实上生成器函数中 的 yield 就是一个特殊的 return 。**<br />next 函数中 return 会返回 done 和 value 属性的对象(没写 return 语句,就是默认 return undefined),其中 return 后面的值,就是 value 的值;done 的值为 true,表示函数迭代完了,也就是终止函数。
结合前面终止迭代器的 return 函数,生成器中也有,它就相当于在 next 中添加了一个 return
```javascript
function* foo1() {
return ;
}
console.log(foo().next()) // { value: undefined, done: true }
function* foo2() {
return 123;
}
console.log(foo().next()) // { value: 123, done: true }
function* foo3() {
}
console.log(foo().return(123)) // { value: 123, done: true }
而 yield 也会返回拥有 done 和 value 属性的对象,yield 后面的值,也会成为 value 的值。最大的区别是 done,yield 的 done 为 false,表示没迭代完,所以 yield 是中断函数执行,还可以恢复。
function* foo() {
console.log("函数开始执行~")
const value1 = 100
console.log("第一段代码:", value1)
// 中断函数执行
yield
const value2 = 200
console.log("第二段代码:", value2)
yield 123
console.log("函数执行结束~")
}
const generator = foo()
console.log(generator.next())
// 函数开始执行~
// 第一段代码: 100
// { value: undefined, done: false }
console.log(generator.next())
// 第二段代码: 200
// { value: 123, done: false }
生成器传递参数 - next 函数
函数既然可以暂停来分段执行,那么函数应该是可以传递参数的,我们是否可以给每个分段来传递参数呢?
答案是可以的;我们在调用 next 函数的时候,可以给它传递参数, 并且这个值会作为上一个 yield 语句的返回值,这样 next 本次要执行的代码块就能拿到这个传进来的值了。
也就是说 next 函数能为本次执行的函数代码块传参。
function* foo(num) {
console.log("函数开始执行~")
const value1 = 100
console.log("第一段代码:", value1)
// 中断函数执行
const n = yield // next 的参数作为上一个 yield 的返回值
const value2 = 200 + num + n // num 是整个函数的参数,n 是第二个 next 函数传进来的参数
console.log("第二段代码:", value2)
console.log("函数执行结束~")
}
const generator = foo(1)
generator.next()
generator.next(10)
// 函数开始执行~
// 第一段代码: 100
// 第二段代码: 211
// 函数执行结束~
生成器抛出异常 – throw函数
除了给生成器函数内部传递参数之外,也可以给生成器函数内部抛出异常,终止函数运行。它针对的是 yield 语句,所以捕获它的异常就能继续运行下去。catch 中也继续添加 yield 中断。
function* foo(num) {
console.log("函数开始执行~")
const value1 = 100
console.log("第一段代码:", value1)
// 发生异常终止函数运行
const n = yield
// 捕获异常
// const n = null
// try {
// n = yield
// } catch (error) {
// console.log('异常捕获');
// }
const value2 = 200 + num + n
console.log("第二段代码:", value2)
console.log("函数执行结束~")
}
const generator = foo(1)
generator.next()
generator.throw('抛出异常')
generator.next(10)
// 函数开始执行~
// 第一段代码: 100
// const n = yield
// ^
// 抛出异常
生成器替代迭代器
我们发现生成器是一种特殊的迭代器,那么在某些情况下我们可以使用生成器来替代迭代器:
迭代器的 next 方法会返回一个对象{done: , value: }
,yield
也会返回这样一个对象。
yield 就是迭代器的语法糖一样,它可以在生成器函数中像迭代器一样以上面对象的形式将值返回。
- 注意:yield 需要在生成器函数中使用,函数需要加
*
```javascript function* generatorFn() { for (let i = 0; i < 3; i++) { yield i } } const generator = generatorFn() console.log(generator.next()); console.log(generator.next()); console.log(generator.next());
// { value: 0, done: false } // { value: 1, done: false } // { value: 2, done: false }
```javascript
// 一个写死的容器(可迭代对象)
const iterableObj = {
arr: ["abc", "cba", "nba"],
// 手动编写的迭代器函数,可以返回一个迭代器对象
[Symbol.iterator]: function() {
let index = 0
return {
next: () => {
if (index < this.arr.length) {
return { done: false, value: this.arr[index++] }
} else {
return { done: true, value: undefined }
}
}
}
}
}
console.log(iterableObj[Symbol.iterator]().next()); // { done: false, value: 'abc' }
// 可以利用 yield 代替手动实现迭代器
// yield 像 return 一样返回值,它取出可迭代对象中的值,并按迭代器返回对象的格式返回
const iterableObj1 = {
arr: ["abc", "cba", "nba"],
// 改成生成器函数,并用 yield 代替 return 和 迭代器对象
[Symbol.iterator]: function*() {
let index = 0
yield this.arr[index++]
}
}
console.log(iterableObj1[Symbol.iterator]().next()); // { value: 'abc', done: false }
yield*
yield*
相当于 yield 的语法糖,语法糖的语法糖了属于是。yield*
可以直接迭代可迭代对象,手动取值都不需要了。
const iterableObj2 = {
arr: ["abc", "cba", "nba"],
// 改成生成器函数,并用 yield* 直接迭代可迭代对象
[Symbol.iterator]: function*() {
yield* this.arr
}
}
console.log(iterableObj2[Symbol.iterator]().next()); // { value: 'abc', done: false }
自定义容器中也可以使用 yield* 来简化代码
// 创建一个教室容器类, 创建出来的对象都是可迭代教室对象
class Classroom {
constructor(address, name, students) {
this.address = address
this.name = name
this.students = students
}
// 注意:将函数改成生成器函数如果使用了键值对的语法糖, * 号加在最前面
*[Symbol.iterator]() {
yield* this.students
}
// 或者写成函数表达式的形式再加 * 号
// [Symbol.iterator] = function* () {
// yield* this.students
// }
}
const classroom = new Classroom("3幢5楼205", "计算机教室", ["james", "kobe", "curry"])
console.log(classroom[Symbol.iterator]().next()); // { value: 'james', done: false }
异步处理方案
学完了我们前面的Promise、生成器等,我们目前来看一下异步代码的最终处理方案。
需求:
- 我们需要向服务器发送网络请求获取数据,一共需要发送三次请求;
- 第二次的请求url依赖于第一次的结果;
- 第三次的请求url依赖于第二次的结果;
- 依次类推;
也就是说,我们要请求中发送请求,并且下一次的请求依赖上一次请求的结果。
最原始的方案:在请求成功后的 then 函数中套娃再次调用请求函数发送请求。但发生了回调地狱
// request.js
function requestData(url) {
// 异步请求的代码会被放入到executor中
return new Promise((resolve, reject) => {
// 模拟网络请求
setTimeout(() => {
// 拿到请求的结果,这里 url 就是请求结果
resolve(url)
}, 2000)
})
}
// 需求:
// 1> url: why -> res: why
// 2> url: res + "aaa" -> res: whyaaa
// 3> url: res + "bbb" => res: whyaaabbb
// 1.第一种方案: 多次回调
// 回调地狱
requestData('why').then(res => {
requestData(res + 'aaa').then(res => {
requestData(res + 'bbb').then(res => {
console.log(res)
})
})
})
第二种方案:利用 Promise 中 then 的返回值来解决。
之前第一次请求成功后,在 then 中再次调用 requestData 发送请求,然后再直接 .then。这样就造成回调嵌套。
requestData('why').then(res => {
requestData(res + 'aaa').then() // 在嵌套中调用 then
}
现在我们不直接调用 then 方法了,而是将 requestData 方法调用后 return 出去。requestData 函数返回一个 promise 对象,所以可以将下一个 then 方法,进行链式调用到当前 then 方法的后面。因为这样刚好能取到返回的 promise 对象的请求结果。
// 2.第二种方案: Promise中then的返回值来解决
requestData("why").then(res => {
return requestData(res + "aaa") // 这次请求的结果会作为 then 返回的 promise 的结果
}).then(res => { // 这个 then 方法可以获取到回调中返回的 promise 的请求结果
return requestData(res + "bbb")
}).then(res => {
console.log(res)
})
但是这样阅读性还是不够清晰。
所以推出了第三种方案:promise + generator
// 先封装一个生成器函数
function* getData() {
yield requestData('why') // yield 会返回 value 为 promise 的对象
}
// 实现生成器函数
const generator = getData()
generator
.next() // 拿到两个属性的对象
.value // 从对象中取出了 promise 对象
.then(res => {
// 这里就拿到了当然 requestData 方法中 promise 的请求结果
// 但是我们这里拿到了没有用啊,我们想要拿到结果能给下一次的 promise 请求
})
这个时候我们就要想到 next 函数传递参数了,next 函数的参数会作为上一个 yield 表达式的返回值。
// 先封装一个生成器函数
function* getData() {
// res1 接收到了 next 的传参,也就是 res1 拿到了 requestData('why') 的请求结果
const res1 = yield requestData('why')
yield requestData(res1 + 'bbb') // 第二次的 requestData 就接收到了上一次请求的结果
}
// 执行生成器函数
const generator = getData()
generator
.next()
.value
.then(res => {
// 这里就拿到了当前 requestData 方法中 promise 的请求结果 res
// 但是我们这里拿到了没有用啊,我们想要拿到结果能给下一次的 promise 请求
generator.next(res) // 这样一传参,就把 res 传给了上一个 yield 的返回值
})
function* getData() {
const res1 = yield requestData('why') // 多个请求就这样分块执行下去
const res2 = yield requestData(res1 + 'bbb')
yield requestData(res2 + 'ccc')
}
// 执行生成器函数
const generator = getData()
generator.next() .value .then(res => {
generator.next(res).value.then(res => {
generator.next(res).value.then(res => {
console.log(res) // whybbbccc
})
})
})
现在发送请求部分是分块了,很清晰,但是执行生成器函数的时候,又回调地狱了。但是这个回调是很有规律的,可以利用递归解决。
// 封装一个工具函数execGenerator自动执行生成器函数
function execGenerator(genFn) {
// 获取生成器
const generator = genFn()
// 递归函数
function exec(res) {
// 手动执行第一个步
const result = generator.next(res)
// 停止条件
if (result.done) return result.value
// 停止条件不成立,开始递归执行
result.value.then(res => {
exec(res)
})
}
exec()
}
除了手动封装生成器函数执行函数,还可以使用第三方库:co
// npm i co
const co = require('co')
co(getData) // 将生产器函数传进去就可以了
终极方案:async/await