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

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

(0)

相关推荐

  • 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中的六种迭代器

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

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

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

  • 学习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

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

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

  • JavaScript迭代器的含义及用法

    什么是迭代器 迭代器就是为实现对不同集合进行统一遍历操作的一种机制,只要给需要遍历的数据结构部署Iterator接口,通过调用该接口,或者使用消耗该接口的API实现遍历操作. 迭代器模式 在接触迭代器之前,一起先了解什么是迭代器模式,回想一下我们生活中的事例.我们在参观景区需要买门票的时候,售票员需要做的事情,他会对排队购票的每一个人依次进行售票,对普通成人,对学生,对儿童都依次售票.售票员需要按照一定的规则,一定顺序把参观人员一个不落的售完票,其实这个过程就是遍历,对应的就是计算机设计模式中的

  • javascript设计模式 – 迭代器模式原理与用法实例分析

    本文实例讲述了javascript设计模式 – 迭代器模式原理与用法.分享给大家供大家参考,具体如下: 介绍:迭代器模式是一种使用频率非常高的设计模式,通过引入迭代器,可以将数据的遍历功能从聚合对象中分离出来.迭代器模式用于顺序访问集合对象的元素,不需要知道集合对象的底层表示. 定义:提供一种方法来访问聚合对象,而不用暴露这个对象的内部表示,其别名为游标(Cursor).迭代器模式是一种对象行为型模式. 场景:我们做一个百家姓的迭代器 示例: function NameRepository(){

  • javascript:void(0)的含义及用法实例

    void关键字介绍 首先,void关键字是javascript当中非常重要的关键字,该操作符指定要计算或运行一个表达式,但是不返回值. 语法格式: void func() void(func()) 实例1 当点击超级链接时,什么都不发生 <!-- 1.当用户链接时,void(0)计算为0,用户点击不会发生任何效果 --> <a href="javascript:void(0);" rel="external nofollow" rel="

  • 聊聊JavaScript中.?、??、??=的用法以及含义

    目录 前言 可选链(.?) 空值合并运算符(??) 空值赋值运算符(??=) 趣味问答时间: 值得注意的是 :?? 是忽视 null ,undefined 等错误的值 最后 前言 在项目中我们往往要做很多很多的空值判断进行容错处理,往往伴随着三目运算.与或.if else来使用,不仅要写很多冗余的代码,后期维护起来也是满屏的if else可以说是非常的痛苦了.今天分享几个处理空值简单的方法,希望可以解决大家的一些问题. 可选链(.?) 如果一个值为null.或者是undefined.那么我们再去

  • ES6 javascript中Class类继承用法实例详解

    本文实例讲述了ES6 javascript中Class类继承用法.分享给大家供大家参考,具体如下: 1. 基本用法 Class 之间可以通过extends关键字实现继承, 这比 ES5 的通过修改原型链实现继承, 要清晰和方便很多. class ColorPoint extends Point {} 上面代码定义了一个ColorPoint类, 该类通过extends关键字, 继承了Point类的所有属性和方法. 但是由于没有部署任何代码, 所以这两个类完全一样, 等于复制了一个Point类. 下

  • 深入理解js 中async 函数的含义和用法

    一.终极解决 异步操作是 JavaScript 编程的麻烦事,麻烦到一直有人提出各种各样的方案,试图解决这个问题. 从最早的回调函数,到 Promise 对象,再到 Generator 函数,每次都有所改进,但又让人觉得不彻底.它们都有额外的复杂性,都需要理解抽象的底层运行机制. 异步I/O不就是读取一个文件吗,干嘛要搞得这么复杂?异步编程的最高境界,就是根本不用关心它是不是异步. async 函数就是隧道尽头的亮光,很多人认为它是异步操作的终极解决方案. 二.async 函数是什么? 一句话,

  • JS async 函数的含义和用法实例总结

    本文实例讲述了JS async 函数的含义和用法.分享给大家供大家参考,具体如下: 学习老师最后一篇文章并做总结, 前面我们认识了各种异步编程方式:回调函数,Promise对象,Generator函数, 再到两种自动执行方式:Thunk,co 好像我们为了更好的解决异步编程做了很多事情, 但是这也说明了一个问题 就是目前仍是的异步编程都有或多或少的问题,才导致我们要找对应的解决方案 今天我们仍是最后的大招:async 这个关键字在哪里见到来着? 我们可以给script标签添加 async 属性来

  • JS co 函数库的含义和用法实例总结

    本文实例讲述了JS co 函数库的含义和用法.分享给大家供大家参考,具体如下: 继续学习阮一峰老师异步编程四部曲之三:co co在很早之前就听超哥讲过,说在node编程中大量用到,源码很简单,但是想法很强大. 让我有空抓紧了解下,前一段时间弄离职的事情,跑来跑去累的够呛. 现在终于一切回归正常了,还在拼命的适应新公司的节奏.只能趁周末继续学习了. 好了,不瞎扯了,回归主题,前两篇文章我们分别学习了 Generator 函数和 Thunk 方式的自动执行. 今天我们接着上次的思路学习使用 co 工

  • JS Thunk 函数的含义和用法实例总结

    本文实例讲述了JS Thunk 函数的含义和用法.分享给大家供大家参考,具体如下: 前面我们已经学习过了Generator 函数的优势和使用场景. 这篇文章我们继续学习阮老师的第二篇文章,Thunk 函数的含义和用法 说实话,在这之前是没听过这个词的,但其实如果你对犀牛书里的不完全函数有认真看过的话 理解起来也不是很费劲. 首先什么是 thunk 函数? 很多场景下我们都会陷入一个问题,就是函数参数的求值时间. 是函数调用时即求值还是在函数内使用时才求值? var x = 1; function

  • JS Generator 函数的含义与用法实例总结

    本文实例讲述了JS Generator 函数的含义与用法.分享给大家供大家参考,具体如下: 读阮一峰老师<Generator 函数的含义与用法>总结 老师的文章通俗易懂,但是我个人理解上面有一些差,所以看了几遍之后才有呢么一点点体会 把它记录下来. 还是那句话,所有事物的出现都是为了解决对应的问题. 那么Generator出现是为了解决什么问题的呢? 在异步编程的场景下,如果有多个异步任务,如何处理他们的先后执行顺序? 举一个常见的例子,jquery的ajax请求,每一个success都是一个

随机推荐