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方法传参向函数内部传递一个值。可以理解为多次传参,多个返回值。

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

时间: 2019-06-20

JavaScript中的迭代器和生成器详解

处理集合里的每一项是一个非常普通的操作,JavaScript提供了许多方法来迭代一个集合,从简单的for和for each循环到 map(),filter() 和 array comprehensions(数组推导式).在JavaScript 1.7中,迭代器和生成器在JavaScript核心语法中带来了新的迭代机制,而且还提供了定制 for-in 和 for each 循环行为的机制. 迭代器 迭代器是一个每次访问集合序列中一个元素的对象,并跟踪该序列中迭代的当前位置.在JavaScript中

总结javascript中的六种迭代器

1.forEach迭代器 forEach方法接收一个函数作为参数,对数组中每个元素使用这个函数,只调用这个函数,数组本身没有任何变化 //forEach迭代器 function square(num){ document.write(num + ' ' + num*num + '<br>'); } var nums = [1,2,3,4,5,6,7,8]; nums.forEach(square); 在浏览器中输出的结果是: 2.every迭代器 every方法接受一个返回值为布尔类型的函数,

学习JavaScript设计模式之迭代器模式

迭代器模式是指提供一种方法顺序访问一个聚合对象中的各个元素,而又不需要暴露该对象的内部表示. JavaScript中的Array.prototype.forEach 一.jQuery中的迭代器 $.each([1, 2, 3], function(i, n) { console.log("当前下标为:"+ i + " 当前元素为:"+ n ); }); 二.实现自己的迭代器 var each = function(ary, callback) { for(var i

JavaScript数组迭代器实例分析

本文实例讲述了JavaScript数组迭代器用法.分享给大家供大家参考.具体如下: 这里注意:如果数组中有0.false."".null.NaN迭代器将会停止 function createIterator(x) { var i = 0; return function(){ return x[i++]; }; } var iterator=createIterator(['a','b','c','d','e','f','g']); var current; while(current

深入理解JavaScript系列(35):设计模式之迭代器模式详解

介绍 迭代器模式(Iterator):提供一种方法顺序一个聚合对象中各个元素,而又不暴露该对象内部表示. 迭代器的几个特点是: 1.访问一个聚合对象的内容而无需暴露它的内部表示. 2.为遍历不同的集合结构提供一个统一的接口,从而支持同样的算法在不同的集合结构上进行操作. 3.遍历的同时更改迭代器所在的集合结构可能会导致问题(比如C#的foreach里不允许修改item). 正文 一般的迭代,我们至少要有2个方法,hasNext()和Next(),这样才做做到遍历所有对象,我们先给出一个例子: 复

AngularJS入门教程之迭代器过滤详解

我们在上一步做了很多基础性的训练,所以现在我们可以来做一些简单的事情喽.我们要加入全文检索功能(没错,这个真的非常简单!).同时,我们也会写一个端到端测试,因为一个好的端到端测试可以帮上很大忙.它监视着你的应用,并且在发生回归的时候迅速报告. 请重置工作目录: git checkout -f step-3 我们的应用现在有了一个搜索框.注意到页面上的手机列表随着用户在搜索框中的输入而变化. 步骤2和步骤3之间最重要的不同在下面列出.你可以在GitHub里看到完整的差别. 控制器 我们对控制器不做

AngularJS入门教程之双向绑定详解

在这一步你会增加一个让用户控制手机列表显示顺序的特性.动态排序可以这样实现,添加一个新的模型属性,把它和迭代器集成起来,然后让数据绑定完成剩下的事情. 请重置工作目录: git checkout -f step-4 你应该发现除了搜索框之外,你的应用多了一个下来菜单,它可以允许控制电话排列的顺序. 步骤3和步骤4之间最重要的不同在下面列出.你可以在GitHub里看到完整的差别. 模板 app/index.html Search: <input ng-model="query"&g

AngularJS入门教程之静态模板详解

为了说明angularJS如何增强了标准HTML,我们先将创建一个静态HTML页面模板,然后把这个静态HTML页面模板转换成能动态显示的AngularJS模板. 在本步骤中,我们往HTML页面中添加两个手机的基本信息,用以下命令将工作目录重置到步骤1. git checkout -f step-1 请编辑app/index.html文件,将下面的代码添加到index.html文件中,然后运行该应用查看效果. app/index.html <ul> <li> <span>

AngularJS入门教程之数据绑定原理详解

本文实例讲述了AngularJS数据绑定原理.分享给大家供大家参考,具体如下: 注 这篇文章主要是写给新手的,是给那些刚刚开始接触Angular,并且想了解数据帮定是如何工作的人.如果你已经对Angular比较了解了,那强烈建议你直接去阅读源代码. Angular用户都想知道数据绑定是怎么实现的.你可能会看到各种各样的词汇:$watch,$apply,$digest,dirty-checking...它们是什么?它们是如何工作的呢?这里我想回答这些问题,其实它们在官方的文档里都已经回答了,但是我

AngularJS入门教程之表格实例详解

AngularJS 表格 ng-repeat 指令可以完美的显示表格. 在表格中显示数据 使用 angular 显示表格是非常简单的: AngularJS 实例 <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <script src="http://apps.bdimg.com/libs/angular.js/1.4.6/angular.min.js"&g

AngularJS 入门教程之事件处理器详解

在这一步,你会在手机详细信息页面让手机图片可以点击. 请重置工作目录: git checkout -f step-10 手机详细信息视图展示了一幅当前手机的大号图片,以及几个小一点的缩略图.如果用户点击缩略图就能把那张大的替换成自己那就更好了.现在我们来看看如何用AngularJS来实现它. 步骤9和步骤10之间最重要的不同在下面列出.你可以在GitHub里看到完整的差别. 控制器 app/js/controllers.js ... function PhoneDetailCtrl($scope

AngularJS入门教程中SQL实例详解

AngularJS SQL 在前面章节中的代码也可以用于读取数据库中的数据. 使用 PHP 从 MySQL 中获取数据 AngularJS 实例 <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <script src="http://apps.bdimg.com/libs/angular.js/1.4.6/angular.min.js"></s

AngularJS入门教程之更多模板详解

在这一步,你将实现手机详细信息视图,这个视图会在用户点击手机列表中的一部手机时被显示出来. 请重置工作目录: git checkout -f step-8 现在当你点击列表中的一部手机之后,这部手机的详细信息页面就会被显示出来. 为了实现手机详细信息视图我们将会使用$http来获取数据,同时我们也要增添一个phone-detail.html视图模板. 步骤7和步骤8之间最重要的不同在下面列出.你可以在GitHub里看到完整的差别. 数据 除了phones.json,app/phones/目录也包

AngularJS入门示例之Hello World详解

本文实例讲述了AngularJS入门示例Hello World.分享给大家供大家参考,具体如下: 以前项目都是使用jQuery和原始的JavaScript,最近参加一个项目需要用到AngularJS.RequireJS等比较潮的框架.这里记录自己的学习过程,虽然冠以原创之名,其实都是参考网上的一些资料,加上自己的一些实践和理解.再没有熟悉AngularJS之前,估计也不出什么高质量的文章,只能算是学习笔记和备忘录.练习使用的版本是1.2.25. 示例代码如下: <!doctype html>

AngularJS入门教程之ng-checked 指令详解

AngularJS ng-checked 指令 AngularJS 实例 选择一个或选择所有选项: <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <script src="http://apps.bdimg.com/libs/angular.js/1.4.6/angular.min.js"></script> </head>