JavaScript迭代器的含义及用法

什么是迭代器

迭代器就是为实现对不同集合进行统一遍历操作的一种机制,只要给需要遍历的数据结构部署Iterator接口,通过调用该接口,或者使用消耗该接口的API实现遍历操作。

迭代器模式

在接触迭代器之前,一起先了解什么是迭代器模式,回想一下我们生活中的事例。我们在参观景区需要买门票的时候,售票员需要做的事情,他会对排队购票的每一个人依次进行售票,对普通成人,对学生,对儿童都依次售票。售票员需要按照一定的规则,一定顺序把参观人员一个不落的售完票,其实这个过程就是遍历,对应的就是计算机设计模式中的迭代器模式。迭代器模式,提供一种方法顺序访问一个聚合对象中的各种元素,而又不暴露该对象的内部表示。

为什么要有迭代器

回忆在我们的javascript中,可遍历的结构以及方式有很多。JavaScript 原有的表示“集合”的数据结构,主要是数组(Array)和对象(Object),ES6 又添加了Map和Set,这样就有了四种数据集合,而遍历这四种结构都有不同的方法。举个栗子,服务端提供数据给前端,前端进行数据可视化工作,对数据进行遍历展示使用的for,但是由于业务的变化,使得后端返回的数据结构发生变化,返回对象或者是set,map,导致前端遍历代码大量重写。而迭代器的目的就是要标准化迭代操作。

如何部署迭代器接口

ES6为迭代器引入了一个隐式的标准化接口。Javascript许多内建的数据结构,例如Array、Map、Set、String、TypedArray、函数的 arguments 对象、NodeList 对象都具备 Iterator 接口。可以通过在控制台打印一个Array实例,查看其原型上具有一个Symbol.iterator属性(Symbol.iterator其实是Symbol('Symbol.iterator')的简写,属性名是Symbol类型代表着这个属性的唯一以及不可重写覆盖),它就是迭代器函数,执行这个函数,就会返回一个迭代器对象。

虽然Javascript许多内建的数据结构已经实现了该接口,还有些结构是没有迭代器接口的(比如对象),那怎么办,我们需要写迭代器,那么就需要知道迭代器是如何工作的。下面代码实现的一个简单迭代器:

//迭代器就是一个函数,也叫迭代器生成函数
function Iterator(o){
let curIndex = 0;
let next = () => {
return {
value: o[curIndex],
done: o.length == ++curIndex
}
}
//返回迭代对象,该对象有next方法
return {
next
}
}
let arr = [1,2]
let oIt = Iterator(arr)
oIt.next();//{value:1,done:false}
oIt.next();//{value:2,done:false}
oIt.next();// {value: undefined, done: true}
oIt.next();// {value: undefined, done: true}

调用迭代器函数,返回一个对象,该对象就是迭代器对象,对象上拥有next方法,每一次调用next方法,都会返回数据结构的当前成员的信息。具体来说,就是返回一个包含value和done两个属性的对象。其中,value属性是当前成员的值,done属性是一个布尔值,表示遍历是否结束。
next()迭代

在上面调用next方法的栗子中,需要注意的是:

在获得数组最后一位元素的时候,迭代器不会报告done:true,这时候需要再次调用next(),越过数组结尾的值,才能得到完成信号done:true。

通常情况下,在已经迭代完毕的迭代器对象上继续调用next方法会继续返回{value: undefined, done: true}而不会报错。

可选的return()和throw()

遍历器对象除了必须具有next方法,还可以具有可选的return方法和throw方法。

return方法被定义为向迭代器发送一个信号,表明不会在消费者中再提取出任何值。

Object.prototype[Symbol.iterator] = function () {
let curIndex = 0;
let next = () => {
return {
value: this[curIndex],
done: this.length == curIndex++
}
}
return {
next,
return() {
console.log('执行return啦')
return {}
}
}
}
let obj = {
0: 'a',
1: 'b',
2: 'c'
}
//自动调用---遇到对迭代器消耗提前终止的条件
for (let item of obj) {
if (item == 'c') {
break
} else {
console.log(item)
}
}
//自动调用---抛出异常
for (let item of obj) {
if (item == 'c') {
throw new Error('Errow')
} else {
console.log(item)
}
}
//手动调用
let ot = obj[Symbol.iterator]()
console.log(ot.return())

上面代码中,throw方法的执行可以在某种情况下自动被调用,也可以手动调用。throw方法主要向迭代器报告一个异常/错误,一般配合生成器使用。

迭代器分类

迭代器分为内部迭代器和外部迭代器。

  • 内部迭代器:本身是函数,该函数内部定义好迭代规则,完全接受整个迭代过程,外部只需要一次调用。例如Array.prototype.forEach方法、jQuery.each都是内部迭代器。
  • 外部迭代器:本身是函数,执行返回迭代对象,迭代下一个元素必须显式调用。使用forEach遍历,只可以一次性把数据全部拉取消耗,而迭代器可以用于以一次一步的方式控制行为,使得迭代过程更加灵活可控。

迭代器使用

实现迭代器接口后,如何进行使用?

let arr = ['a', 'b'];
let iter = arr[Symbol.iterator]();
iter.next() // { value: 'a', done: false }
iter.next() // { value: 'b', done: false }
iter.next() // { value: undefined, done: true }

除了像上述代码这样单独使用外,实现该接口的目的,就是为所有数据结构,提供一种统一的访问机制。实现了该接口,就可以调用ES6中新增的通过调用Iterator 接口实现的API,例如for..of就是典型的消耗迭代器的API。下面具体看看for..of的实现原理:

let arr = [1,2,3];
for(let num of arr){
console.log(num);
}

输出结果为:1,2,3

for-of 循环首先会调用 arr 数组中Symbol.iterator 属性对象的函数,就会获取到该数组对应的迭代器,接下来 iterator.next()被调用,迭代器结果对象的 value 属性会被放入到变量 num 中。数组中的数据项会依次存入到变量num 中,直到迭代器结果对象中的 done 属性变成 true 为止,循环就结束。

for-of 循环完全删除了for循环中追踪集合索引的需要,更能专注于操作集合内容。

ES6 规定,默认的 Iterator 接口部署在数据结构的Symbol.iterator属性,或者说,一个数据结构只要具有Symbol.iterator属性,就可以认为是“可遍历的”(iterable)。就可以使用上述默认会调用Iterator函数的API,而如果该数据结构没有提供实现这个接口(例如对象)又该怎么样达到最大化的互操作性呢?那么就可以自己构建符合这个标准的迭代器。

下面是一个为对象添加 Iterator 接口的例子:

let obj = {
0: 'a',
1: 'b',
2: 'c',
length: 3,
[Symbol.iterator]: function () {
let curIndex = 0;
let next = () => {
return {
value: this[curIndex],
done: this.length == curIndex++
}
}
return {
next
}
}
}
for (let item of obj) {
console.log(item)
}

如果把该对象的[Symbol.iterator]属性删除,那么就会报错Uncaught TypeError: obj is not iterable,告诉我们obj是不可被遍历。

除了上面展示的for..of循环可以一个一个的消耗迭代器之外,还有其它ES6结构也可以用来消耗迭代器。例如spread运算符:

function f(x, y, z) {
console.log(x, y, z)
}
f(...[2, 3, 1])

以及结构赋值也可以部分或者完全消耗一个迭代器:

let arr = [1, 2, 3, 4, 5]
var it = arr[Symbol.iterator]()
//部分消耗
var [x, y] = it
console.log(x, y) //打印1 2
//完全消耗
var [y, ...z] = it
console.log(y, z) //打印3 [4,5]

JavaScript 默认产生迭代器的API

产生迭代器对象,我们可以通过定义迭代器函数来生产迭代器对象,还可以调用JavaScript在内置数据结构中定义好的迭代器函数来生产。除此之外,对于数组以及ES6新增的几个新的数据结构MAP、Set,这些集合不仅本身已部署迭代器接口,还提供了API方法来产生迭代器对象。ES6 的数组、Set、Map 都部署了以下三个方法,调用后都返回遍历器对象。

  • entries() 返回一个遍历器对象,用来遍历[键名, 键值]组成的数组。
  • keys() 返回一个遍历器对象,用来遍历所有的键名。
  • values() 返回一个遍历器对象,用来遍历所有的键值。

数组的迭代器使用实例

下面是数组的迭代器接口使用:

let arr = [1,2,3,4]
let arrEntires = arr.entries()
arrEntires.next() //{value: [0, 1], done: false}
let arrKeys = arr.keys() //对于数组,索引值就是键值
arrKeys.next() //{value: 0, done: false}
let arrValues = arr.values()
arrValues.next() //{value: 1, done: false}

下面代码可以看出数组的for…of 遍历的默认迭代器接口是values

for(let item of [1,2,3]) {
console.log(item)// [1,2,3]
}

Set的迭代器使用实例

下面是Set的迭代器接口使用:

let set = new Set([1,2,3,4])
let setEntires = set.entries()//对于 Set,键名与键值相同。
setEntires.next() //{value: [1, 1], done: false}
let setKeys = set.keys()
setKeys.next() //{value: 1, done: false}
let setValues = set.values()
setValues.next() //{value: 1, done: false}

如下可以看出Set的默认迭代器接口[Symblo.iterator]是values

for(let item of new Set([1,2,3,4])){
console.log(item)// [1,2,3,4]
}

Map的迭代器使用实例

下面是Map的迭代器接口使用:

let map = new Map([[1,2],[3,4]])
let mapEntires = map.entries()
mapEntires.next() //{value: [1, 2], done: false}
let mapKeys = map.keys() 
mapKeys.next() //{value: 1, done: false}
let mapValues = map.values()
mapValues.next() //{value: 2, done: false}

Map 的默认迭代器接口[Symblo.iterator]是 entries;

for(let item of new Map([[1,2],[3,4]])){
console.log(item)// [1,2] [3,4]
}

为什么对象没有内置迭代器接口

在上面中,我们提及到对象没有设置可迭代的默认方法,是不可迭代对象,表现为其没有[Symbol.iterator]属性。虽然对象对我们来说,是键值存储的一种方式,尽管没有 map 那么好,key只可以是字符串,但是有的时候对象也是需要被迭代的,但是为什么不给对象设置可迭代的默认方法?

原因是因为,对于对象的遍历,需要考虑到遍历是对象自身的属性还是遍历对象自身上的可枚举属性还是遍历原型上的属性还是遍历原型上的可枚举属性还是连[Symbol.iterator]也希望遍历出来。鉴于各方意见不一,并且现有的遍历方式可以满足,于是标准组没有将[Symbol.iterator]加入。

生成迭代器对象的方法

在上面,我们尝试过了为一个对象添加了Symbol.iterator方法,该方法就是该对象的遍历器生成函数,调用该函数会返回该对象的一个遍历器对象。

除了上面在为对象添加遍历器生成函数的这种根据迭代器协议直接生成迭代器对象的方式外,还有什么方式可以生成迭代器对象呢?有,它是一种特殊的函数,叫生成器。

var it = {};
it[Symbol.iterator] = function* () {
yield 1;
yield 2;
yield 3;
};
//可以被...遍历,说明已经部署成功
console.log([...it])// [1, 2, 3]
let myIterator = it[Symbol.iterator]()
console.log(myIterator.next())//{value: 1, done: false}
console.log(myIterator.next())//{value: 2, done: false}
console.log(myIterator.next())//{ value: 3, done: false }
console.log(myIterator.next())//{ value: undefined, done: true }

上面代码中,生成器函数没有过多的代码,只需要使用关键字yeild来返回每次next()的值。

生成器是一种特殊的函数形式,生成器函数的声明语法为:

function *bar(){
// ...
}

*前后可以有空格也可以没有空格。生成器函数的声明虽然和普通函数有区别,但是执行和普通函数一样,一样可以传参数。那它们的主要区别是什么呢?

函数是一段执行特定任务的代码块,所以函数执行,相当于这一段代码块被执行。函数开始执行,在它执行完之前不会被打断,这段代码块将被全部执行完。在ES6引入生成器之前函数的确是这样执行的,但是前面介绍到外部迭代器可以相比内部迭代器对迭代过程进行控制,什么时候需要消耗,迭代器对象再next一下即可。类似迭代过程,函数的执行过程一样可以控制,函数可以不需要一次性执行完毕。

生成器函数的执行会返回一个迭代器对象来控制该生成器函数执行其代码。因此,函数的执行变得可控。还可以在生成器中使用新的关键字yield,用来标示一个暂停点。迭代器除了可以控制函数执行外,还可以在每一次暂停中双向传递信息,暂停的时候生成器函数会返回一个值,恢复执行的时候迭代器可以通过向next方法传参向函数内部传递一个值。可以理解为多次传参,多个返回值。

相关推荐