JS的迭代器和可迭代对象详解
迭代器(iterator),是使用户在容器对象(container,例如链表或数组)上可以遍历访问的对象,使用该接口无需关心容器对象的内部实现细节。从迭代器的定义我们可以看出来,迭代器是帮助我们对某个数据结构进行遍历的对象。在JavaScript中,迭代器也是一个具体的对象,这个对象需要符合迭代器协议(iterator protocol):next方法要求:next方法是一个无参数或者有一个参数的
文章目录
1.迭代器
1.1 认识迭代器
迭代器(iterator),是使用户在容器对象(container,例如链表或数组)上可以遍历访问的对象,使用该接口无需关心容器对象的内部实现细节。
- 其行为像数据库中的光标,迭代器最早出现在1974年设计的CLU编程语言中;
- 在各种编程语言的实现中,迭代器的实现方式各不相同,但是基本都有迭代器,比如Java、Python等;
从迭代器的定义我们可以看出来,迭代器是帮助我们对某个数据结构进行遍历的对象。
在JavaScript中,迭代器也是一个具体的对象,这个对象需要符合迭代器协议(iterator protocol):
- 迭代器协议定义了产生一系列值(无论是有限还是无限个)的标准方式;
- 在JavaScript中这个标准就是一个特定的next方法;
next方法要求:next方法是一个无参数或者有一个参数的函数,返回结果应当拥有以下两个属性的对象:
1.done(boolean) : done返回一个布尔值: 可能是false, 可能是true
- 如果迭代器可以产生序列中的下一个值,则为 false。(这等价于没有指定 done 这个属性。)
- 如果迭代器已将序列迭代完毕,则为 true。这种情况下,value 是可选的,如果它依然存在,即这个值为迭代结束之后默认返回值。
2. value:value的返回值: 可能是一个具体的值, 可能是undefined
- 迭代器返回的任何 JavaScript 值。done 为 true 时可省略。
1.2 理解迭代器
上面的概念可能会有些抽象 , 下面我们来看看代码帮助我们理解next方法需要满足的要求 :
- 创建一个满足要求的迭代器
const arr = [10, 20, 30]
// 给数组arr创建一个迭代器
// 1.迭代器是一个对象 首先我们创建一个对象
const arrIterator = {
// 2.迭代器中有一个next方法, 这个函数要求无参数或者仅有一个参数, 在对象中添加next方法
next: function() {
// 3.next方法需要返回一个对象, 且对象中需要包含done和value两个属性
return {done: false, value: 10}
}
}
- 但是这个迭代器对象还并不能帮助我们对数组进行迭代, 我们需要实现如下功能:
// 第一次调用迭代器, 由于没有完成所有的迭代, done应该为false, value应该为10
console.log(arrIterator.next())
// 第二次调用迭代器, 由于没有完成所有的迭代, done应该为false, value应该为20
console.log(arrIterator.next())
// 第三次调用迭代器, 由于没有完成所有的迭代, done应该为false, value应该为30
console.log(arrIterator.next())
// 第四次调用迭代器, 由于已经完成所有的迭代, done应该为true, value应该为undefined
console.log(arrIterator.next())
- 添加一个if添加语句, 我们就可以实现如上功能, 但是这个迭代器是arr这个数组独有的迭代器, 不具备通用性
const arr = [10, 20, 30]
// 给数组arr创建一个迭代器
let index = 0
// 1.迭代器是一个对象 首先我们创建一个对象
const arrIterator = {
// 2.迭代器中有一个next方法, 这个函数要求无参数或者仅有一个参数, 在对象中添加next方法
next: function() {
// 3.next方法需要返回一个对象, 且对象中需要包含done和value两个属性
if (index < arr.length) {
return { done: false, value: arr[index++] }
} else {
return { done:true, value: undefined }
}
}
}
console.log(arrIterator.next()) // {done: false, value: 10}
console.log(arrIterator.next()) // {done: false, value: 20}
console.log(arrIterator.next()) // {done: false, value: 30}
console.log(arrIterator.next()) // {done: true, value: undefined}
- 我们封装一个函数, 让上面我们实现的迭代器具有通用性
// 例如有两个数组
const nums = [10, 20, 30, 40]
const names = ["aaa", "bbb", "ccc", "ddd"]
// 封装一个用于创建迭代器的函数
function createArrayIterator(arr) {
let index = 0
return {
next: function() {
if (index < arr.length) {
return { done: false, value: arr[index++] }
} else {
// value不写, 默认就是undefined
return { done: true }
}
}
}
}
// 调用函数创建nums构造器
const numsIterator = createArrayIterator(nums)
console.log(numsIterator.next()) // {done: false, value: 10}
console.log(numsIterator.next()) // {done: false, value: 20}
console.log(numsIterator.next()) // {done: false, value: 30}
console.log(numsIterator.next()) // {done: false, value: 40}
console.log(numsIterator.next()) // {done: true}
// 调用函数创建names构造器
const namesIterator = createArrayIterator(names)
console.log(namesIterator.next()) // {done: false, value: 'aaa'}
console.log(namesIterator.next()) // {done: false, value: 'bbb'}
console.log(namesIterator.next()) // {done: false, value: 'ccc'}
console.log(namesIterator.next()) // {done: false, value: 'ddd'}
console.log(namesIterator.next()) // {done: true}
2. 可迭代对象
2.1 认识可迭代对象
但是上面的代码整体来说看起来是有点奇怪的:
- 我们获取一个数组的时候,需要自己创建一个index变量,再创建一个所谓的迭代器对象;
事实上我们可以对上面的代码进行进一步的封装,让其变成一个可迭代对象;
- 我们再来看一下类似于刚刚的代码
const info = {
friends: ["aaa", "bbb", "ccc"]
}
let index = 0
// 创建一个info的迭代器
const infoIterator = {
next: function() {
if (index < info.friends.length) {
return { done: false, value: info.friends[index++] }
} else {
return { done: true }
}
}
}
console.log(infoIterator.next()) // {done: false, value: 'aaa'}
console.log(infoIterator.next()) // {done: false, value: 'bbb'}
console.log(infoIterator.next()) // {done: false, value: 'ccc'}
console.log(infoIterator.next()) // {done: true}
- 我们发现, 想要迭代的对象和我们的迭代器是分开的, 那么有没有办法就将要迭代的对象和迭代器合并起来呢?(往下面看)
什么又是可迭代对象呢?
- **可迭代对象和迭代器是两个不同的概念, 其实我们将上面的代码中, 迭代的目标对象和迭代器合并起来就一个可迭代对象 **;
- 当一个对象实现了iterable protocol协议时,它就是一个可迭代对象;
- 可迭代对象的要求一 : 是必须实现 @@iterator (这是规范的名字) 方法,在代码中我们使用 [Symbol.iterator] (这是实际用的名字)访问该属性;
- 可迭代对象的要求二 : 这个[Symbol.iterator] 方法需要返回一个迭代器
- 那么我们根据这两个要求, 试着将上面代码中的info对象变成一个可迭代对象
const info = {
friends: ["aaa", "bbb", "ccc"],
// 1.必须实现一个特定的方法[Symbol.iterator] (名字是固定的)
[Symbol.iterator] () {
// 将我们创建的info对象的迭代器放过来
let index = 0
const infoIterator = {
next: function() {
if (index < info.friends.length) {
return { done: false, value: info.friends[index++] }
} else {
return { done: true }
}
}
}
// 2.这个方法需要返回一个迭代器(这个迭代器用于迭代当前对象)
return infoIterator
}
}
- 为了进一步优化, 我们想在next方法中使用this, 但是next方法中的this不一定指向info, 我们需要让this指向info
const info = {
friends: ["aaa", "bbb", "ccc"],
[Symbol.iterator] () {
let index = 0
const infoIterator = {
// 将next方法改为箭头函数, 这样next方法中就不在绑定this, this回去上层作用域中寻找, 上层作用域中的this会指向info
next: () => {
if (index < this.friends.length) {
return { done: false, value: this.friends[index++] }
} else {
return { done: true }
}
}
}
return infoIterator
}
}
当然我们要问一个问题,我们转成这样的一个东西有什么好处呢?
-
当一个对象变成一个可迭代对象的时候,就可以进行某些迭代操作;
-
比如 for…of 操作时(当然不仅限于for…of),其实就会调用它的 [Symbol.iterator] 方法(@@iterator);
// 例如上面的info对象本来是没办法for...of操作的, 变成可迭代对象后就可以进行for...of操作 for (item of info) { console.log(item) // aaa bbb ccc }
2.2 可迭代对象特点
上面我们将info变成可迭代对象可以发现可迭代对象会具备一下特点:
-
可迭代对象我们一定**可以访问它的[Symbol.iterator]**方法
info[Symbol.iterator]
-
调用**[Symbol.iterator]方法一定会返回一个迭代器**
const iterator = info[Symbol.iterator]()
-
可以调用返回的迭代器的next方法, 对 对象进行迭代
const iterator = info[Symbol.iterator]() console.log(iterator.next()) // {done: false, value: 'aaa'} console.log(iterator.next()) // {done: false, value: 'bbb'} console.log(iterator.next()) // {done: false, value: 'ccc'} console.log(iterator.next()) // {done: true}
2.3 原生迭代器对象
事实上我们平时创建的很多原生对象已经实现了可迭代协议,会生成一个迭代器对象的:
- 比如 : String、Array、Map、Set、arguments对象、NodeList集合;
我们都知道数组是一个可迭代对象, 那么数组是不是有[Symbol.iterator]方法并且满足我们刚刚总结的特点呢? 我们来验证一下
-
打印数组的[Symbol.iterator]方法, 我们发现确实有这样一个函数
const nums = [10, 20, 30] console.log(nums[Symbol.iterator]) // ƒ values() { [native code] }
-
调用数组的[Symbol.iterator]方法, 我们发现会返回一个数组的迭代器
const nums = [10, 20, 30] console.log(nums[Symbol.iterator] ()) // Array Iterator {}
-
并且我们可以拿到这个数组的迭代器, 通过迭代器的next方法访问数组元素
const nums = [10, 20, 30] const iterator = nums[Symbol.iterator]() console.log(iterator.next()) // {value: 10, done: false} console.log(iterator.next()) // {value: 20, done: false} console.log(iterator.next()) // {value: 30, done: false} console.log(iterator.next()) // {value: undefined, done: true}
由此发现, 数组中是有一个实现好了的迭代器, 因此数组是一个可迭代对象, 数组可迭代的原理我们就很清楚了, 其他可迭代对象同理
2.4 可迭代对象场景
**上面我们已经讲过, 可迭代对象可以使用for…of, 那么除此之外还可以应用在以下场景 **:
- 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);
简单举几个例子:
// 例如两个对象, 一个转为可迭代对象的info, 一个是默认对象obj
const obj = {
name: "kaisa",
age: 18
}
const info = {
friends: ["aaa", "bbb", "ccc"],
[Symbol.iterator] () {
let index = 0
const infoIterator = {
next: () => {
if (index < this.friends.length) {
return { done: false, value: this.friends[index++] }
} else {
return { done: true }
}
}
}
return infoIterator
}
}
// 1.可迭代对象可以使用展开语法
console.log(...info) // aaa bbb ccc
// 普通对象不可使用展开语法
console.log(...obj) // Found non-callable @@iterator
// 2.set方法也是要求传入可迭代对象
const set1 = new Set(info)
console.log(set1) // Set(3) {'aaa', 'bbb', 'ccc'}
// 普通对象传入就会报错
const set2 = new Set(obj)
console.log(set2)
// 3.转为数组的方法Array.from也是要求传入可迭代对象
const arr1 = Array.from(info)
console.log(arr1) // ['aaa', 'bbb', 'ccc']
// 不可迭代对象无法正确转为数组
const arr2 = Array.from(obj)
console.log(arr2) // []
2.5 自定义类的迭代
在前面我们看到Array、Set、String、Map等类创建出来的对象都是可迭代对象:
- 在面向对象开发中,我们可以通过class定义一个自己的类,这个类可以创建很多的对象:
- 如果我们也希望自己的类创建出来的对象默认是可迭代的,那么在设计类的时候在类的实例方法上添加迭代器;
自定义类的迭代的实现: 写一个案例尝试创建一系列可迭代对象
class Person {
constructor(name, age, friends) {
this.name = name
this.age = age
this.friends = friends
}
// 添加实例方法, 迭代器
[Symbol.iterator] () {
let index = 0
return {
next: () => {
if (index < this.friends.length) {
return { done: false, value: this.friends[index++] }
} else {
return { done: true }
}
}
}
}
}
// 这样Person类创建出来的对象都是可迭代对象
const p1 = new Person("kaisa", 18, ["aaa", "bbb", "ccc"])
const p2 = new Person("coder", 19, ["ddd", "eee", "fff"])
// 可以进行for...of操作
for (item of p1) {
console.log(item) // aaa bbb ccc
}
for (item of p2) {
console.log(item) // ddd eee fff
}
2.6 迭代器的中断
迭代器在某些情况下会在没有完全迭代的情况下中断:
- 如遍历的过程中通过break、return、throw中断了循环操作;
- 比如在解构的时候,没有解构所有的值;
那么这个时候我们想要监听中断的话,可以添加return方法:
- 例如我们用上面的代码举例
class Person {
constructor(name, age, friends) {
this.name = name
this.age = age
this.friends = friends
}
// 添加实例方法, 迭代器
[Symbol.iterator] () {
let index = 0
return {
next: () => {
if (index < this.friends.length) {
return { done: false, value: this.friends[index++] }
} else {
return { done: true }
}
},
// 添加一个return方法, 用于监听迭代中断, 当迭代器中断就会执行return方法
return: () => {
console.log("监听到迭代器中断")
// 迭代器需要返回对象
return { done:true }
}
}
}
}
const p1 = new Person("kaisa", 18, ["aaa", "bbb", "ccc"])
for (item of p1) {
console.log(item) // aaa bbb
// 如果在某种情况写退出了循环, 我们需要告知迭代器
if (item === "bbb") {
break
}
}
更多推荐
所有评论(0)