前言
最近看到一道面试题觉得很有意思,如何才能使下面的代码打印出结果
js
const obj = {
a: 1,
b: 2,
c: 3
}
for (let v of obj) {
console.log(v)
}
猛地看上去挺简单,但是里面其实蕴含了很多东西,想让其打印出结果就必须要了解一个概念就是迭代器Iterator
,这也正是本文所要讲的,先说下答案吧。
js
Object.prototype[Symbol.iterator]=function(){
return Object.value(this)[Symbol.Iterator]()
}
只需要在for of之前给对象添加Symbol.iterator
属性就可以完美打印出1,2,3,这个属性是一个函数,这里巧妙借用了数组可迭代这一点完成对对象的遍历,首先对象本身会默认调用Symbol.iterator
方法,那么此时函数中的this
指向的是对象本身,然后使用Object.value
方法取出该对象的值并组成一个数组,数组本身是有Symbol.iterator
方法的,所以相当于是在遍历一个数组。听不懂?没关系,接下来才是正文部分,那就先从概念部分说起吧。
Iterator是什么
相信经验比较丰富的一些同学可能会看到过这个,在一些数组或者Map、Set中经常会看到这个属性,但在对象(Object)中就没有,下面就来一起彻底弄明白这个东西。
Iterator
名为遍历器,ES6中规定遍历器是一种机制,也是一种接口,为各种不同的数据结构提供统一的访问机制 ,任何数据结构只要部署 Iterator 接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)。原生具备Iterator
的数据结构有
- Array
- Map
- Set
- String
- arguments对象
- NodeList对象
为什么需要Iterator
在Javascript中表示集合的数据结构有数组、对象、Map和Set数据结构,它们都有各自的特点,但同时也有一个共同的需求:遍历它们的内容。然而不同的数据结构都有对应的遍历方法,比如,数组有forEach、map、filter
等方法,对象有for in/Object.keys()
方法,Map和Set也都有各自的遍历方法,这就导致一个问题,如果要处理一个不同类型的集合,那么就要写很多的判断条件来根据集合的类型选择对应的遍历方法,这显然是不方便的。
于是ES6中为了解决这个问题引入了Iterator
这种统一的访问机制,即for of
循环,使用for of
循环时会自动去寻找该数据结构是否部署了Iterator
接口。这就是为什么会出现Iterator
的原因,总结一下:
- 为各种数据结构,提供一个统一的、简便的访问接口;
- 使得数据结构的成员能够按某种次序排列(自定义遍历行为);
- 供
for of
使用;
Iterator的实现
Iterator
其实就是一个对象,对象中有一个next
方法(固定格式),第一次调用对象的next
方法可以返回当前数据结构中的第一个成员,第二次调用对象的next
方法可以返回当前数据结构中的第二个成员,直到数据结构中的结束位置结束。下面来模拟实现一个遍历器生成函数:
js
const arr = [1,2,3];
function makeIterator(arr){
let nextIndex = 0;
return {
next(){
return nextIndex < arr.length?
{value:arr[nextIndex++],done:false}:
{value:undefined,done:true}
}
}
}
const iterator = makeIterator(arr);
console.log(iterator.next()) // { value: 1, done: false }
console.log(iterator.next()) // { value: 2, done: false }
console.log(iterator.next()) // { value: 3, done: false }
console.log(iterator.next()) // { value: undefined, done: true }
可以看出调用遍历器对象的next
方法,就可以遍历事先给定的数据结构。在makeIterator
函数中首先声明了一个表示从头开始的下标,每调用一次next
方法就会以对象的形式返回数据结构中对应的元素,其中value
表示数据结构中当前位置的值,done
表示遍历是否结束。
上面提到的Symbol.iterator
属性,它本身是一个函数,是当前数据结构默认的遍历器生成函数,这个函数的实现类似于makeItrerator
函数,它默认部署在可遍历的数据结构中。
只要数据结构具有Symbol.iterator
属性就可以使用for of
遍历,for...of
循环内部调用的其实就是数据结构的Symbol.iterator
方法。
js
const arr = [1,2,3,4];
for (let v of arr){
console.log(v) // 1 2 3 4
}
Object对象上的没有默认的Iterator
接口,是因为对象是无序的,里面的哪个属性先遍历哪个属性后遍历是不确定的,需要开发者手动确定。要使其可以进行遍历,添加一个遍历器生成函数即可。
js
const obj = {
name:"xxx",
age:18,
}
// 如果不加此函数,将会报错 obj is not iterable
Object.prototype[Symbol.iterator] = function(){
let nextIndex = 0
return {
next:()=>{ // 确定this指向
return nextIndex < Object.values(this).length?{value:Object.values(this)[nextIndex++],done:false}:{value:undefined,done:true}
}
}
}
for (let v of obj){
console.log("v",v) // xxx 18
}
此外其他默认调用Iterator
接口的场景还有解构赋值、扩展运算符。再补充一下for in
和for of
是两种不同的数据结构,分别用于遍历两种不同的数据结构,for in
枚举的对象的键,而不是值,for of
直接遍历的是对象的值,而不是键。
总结
这篇文章主要讲解了JavaScript中一个可迭代对象是如何进行迭代的,详细说明了Iterator
遍历器的概念、出现的原因以及如何实现,属于原理级别的知识,虽然在工作中不会经常用到,但是本着知其然知其所以然的道理了解它的原理,内部是如何运行的才能知道一些报错信息出现的原因以及解决方案,大家加油吧!