JavaScript迭代器的含义及用法


Posted in Javascript onJune 21, 2019

什么是迭代器

迭代器就是为实现对不同集合进行统一遍历操作的一种机制,只要给需要遍历的数据结构部署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方法传参向函数内部传递一个值。可以理解为多次传参,多个返回值。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
jQuery的12招常用技巧分享
Aug 08 Javascript
JavaScript 处理Iframe自适应高度(同或不同域名下)
Mar 29 Javascript
jQuery修改li下的样式以及li下的img的src的值的方法
Nov 02 Javascript
Javascript基础教程之数据类型转换
Jan 18 Javascript
js获取滚动距离的方法
May 30 Javascript
javascript中对变量类型的判断方法
Aug 09 Javascript
Javascript删除指定元素节点的方法
Jun 21 Javascript
BootStrap Tooltip插件源码解析
Dec 27 Javascript
AngularJS动态绑定ng-options的ng-model实例代码
Jun 21 Javascript
D3.js(v3)+react 实现带坐标与比例尺的散点图 (V3版本)
May 09 Javascript
微信小程序webview与h5通过postMessage实现实时通讯的实现
Aug 20 Javascript
Vue SSR 即时编译技术的实现
May 06 Javascript
js事件触发操作实例分析
Jun 21 #Javascript
微信小程序实现下拉刷新动画
Jun 21 #Javascript
vue elementUI使用tabs与导航栏联动
Jun 21 #Javascript
Ajax请求时无法重定向的问题解决代码详解
Jun 21 #Javascript
vue配置文件实现代理v2版本的方法
Jun 21 #Javascript
微信小程序自定义多列选择器使用详解
Jun 21 #Javascript
详解Webpack如何引入CDN链接来优化编译后的体积
Jun 21 #Javascript
You might like
PHP中的cookie
2006/11/26 PHP
用PHP实现小写金额转换大写金额的代码(精确到分)
2012/01/10 PHP
PHP中如何判断AJAX提交的数据
2012/02/05 PHP
百度地图API使用方法详解
2015/08/25 PHP
ThinkPHP实现的rsa非对称加密类示例
2018/05/29 PHP
PHP实现单文件、多个单文件、多文件上传函数的封装示例
2019/09/02 PHP
php 命名空间(namespace)原理与用法实例小结
2019/11/13 PHP
filemanage功能中用到的lib.js
2007/04/08 Javascript
jquery+json实现的搜索加分页效果
2010/03/31 Javascript
扩展jquery实现客户端表格的分页、排序功能代码
2011/03/16 Javascript
常用Extjs工具:Extjs.util.Format使用方法
2012/03/22 Javascript
jQuery+css+html实现页面遮罩弹出框
2013/03/21 Javascript
自定义jQuery选项卡插件实例
2013/03/27 Javascript
JavaScript自定义方法实现trim()、Ltrim()、Rtrim()的功能
2013/11/03 Javascript
JavaScript计算器网页版实现代码分享
2016/07/15 Javascript
jQuery插件WebUploader实现文件上传
2016/11/07 Javascript
angularJS 指令封装回到顶部示例详解
2017/01/22 Javascript
JavaScript寄生组合式继承实例详解
2018/01/06 Javascript
微信小程序中的店铺评分组件及vue中用svg实现的评分显示组件
2018/11/16 Javascript
vue实现新闻展示页的步骤详解
2019/04/11 Javascript
解决使用layui对select append元素无效或者未及时更新的问题
2019/09/18 Javascript
vue中解决拖拽改变存在iframe的div大小时卡顿问题
2020/07/22 Javascript
Ruby使用eventmachine为HTTP服务器添加文件下载功能
2016/04/20 Python
python实现n个数中选出m个数的方法
2018/11/13 Python
对python当中不在本路径的py文件的引用详解
2018/12/15 Python
对python捕获ctrl+c手工中断程序的两种方法详解
2018/12/26 Python
Python正则表达式匹配和提取IP地址
2019/06/06 Python
TensorFlow通过文件名/文件夹名获取标签,并加入队列的实现
2020/02/17 Python
python数据处理——对pandas进行数据变频或插值实例
2020/04/22 Python
html5构建触屏网站之touch事件介绍
2013/01/07 HTML / CSS
美国汽车性能部件和赛车零件网站:Vivid Racing
2018/03/27 全球购物
初中成绩单评语
2014/12/29 职场文书
小学科学教学计划
2015/01/21 职场文书
谢师宴家长答谢词
2015/09/30 职场文书
思想品德课教学反思
2016/02/24 职场文书
MySQL的全局锁和表级锁的具体使用
2021/08/23 MySQL