javascript 设计模式之迭代器模式

概念

迭代器模式提供一种方法顺序访问一个聚合对象中的各个元素,而又不暴露该对象的内部表示。

特点:

  1. 为遍历不同数据结构的 “集合” 提供统一的接口;
  2. 能遍历访问 “集合” 数据中的项,不关心项的数据结构

从 Array.prototype.forEach 说起

let arr = [1, 2, 3]
arr.forEach((item) => {
	console.info(item) // 1 2 3
})

在数组上遍历是没有问题的,但将它作为在 NodeList 就会发现报错

<div id="div1">
		<a href="#">a1</a>
		<a href="#">a2</a>
		<a href="#">a3</a>
		<a href="#">a4</a>
		<a href="#">a5</a>
</div>

遍历 id 为 div1 下的所有 a 标签

let nodeList = document.getElementById('div1').getElementsByTagName('a')
nodeList.forEach((item) => {
	console.info(item)
})

运行会报Uncaught TypeError: nodeList.forEach is not a function 错误。
这是由于 nodeList 只是类数组,并没有实现 forEach 方法。
使用 for 循环遍历

let nodeList = document.getElementById('div1').getElementsByTagName('a')
let i, length = nodeList.length
for (i = 0; i < length; i++) {
	console.info(nodeList[i].innerHTML)
}

可以正常打印

jQuery 的 each 方法

上述的数组跟 nodeList 采用不同的遍历方式,为了处理这种情况,通过借助jQuery的each方法,我们可以用同一套遍历规则遍历不同的集合对象:

let arr = [1, 2, 3]
let nodeList = document.getElementById('div1').getElementsByTagName('a')
let $a = $('a')
$.each(arr, function (index, item) {
	console.log(`数组的第${index}个元素是${item}`)
})
$.each(nodeList, function (index, aNode) {
	console.log(`DOM类数组的第${index}个元素是${aNode.innerText}`)
})
$.each($a, function (index, aNode) {
	console.log(`jQuery集合的第${index}个元素是${aNode.innerText}`)
})

可以正常输出内容

自定义个迭代器

// 主体
class Container {
	constructor(list) {
		this.list = list
	}
	// 生成遍历器
	getIterator() {
		return new Iterator(this)
	}
}

// 迭代器
class Iterator {
	constructor(container) {
		this.list = container.list
		this.index = 0
	}
	next() {
		if (this.hasNext()) {
			return this.list[this.index++]
		}
		return null
	}
	hasNext() {
		if (this.index < this.list.length) {
			return true
		}
		return false
	}
}

const arr = [1, 2, 3, 4, 5, 6] // 是有序的集合,比如数组,nodeList
const container = new Container(arr)
const iterator = container.getIterator()
while (iterator.hasNext()) {
	console.info(iterator.next())
}

ES6 之后的迭代器

ES6 新增了 Set 和 Map ,导致目前有序集合有 Array Map Set String TypedArray argument NodeList 这么多种,需要一个统一的遍历接口访问这些有序集合中的数据,所以 ES6 引入了 Iterator。
ES6 默认的 Iterator 接口部署在数据结构的 [Symbol.iterator] 属性上,该属性本身是一个函数,执行这个函数会返回个遍历器对象。
遍历器对象的特征:

  • 拥有 next 方法
  • 执行 next(),会返回个包含 value 和 done 属性的对象
    • value: 当前数据结构成员的值
    • done: 布尔值,表示遍历是否结束
      在数组上测试下
let arr = [1, 2, 3]
let iterator = arr[Symbol.iterator]();
console.info(iterator.next()) // { value: '1', done: false }
console.info(iterator.next()) // { value: '2', done: false }
console.info(iterator.next()) // { value: '3', done: false }
console.info(iterator.next()) // { value: undefined, done: false }

代码优化并封装个 each 方法:

function each(data) {
	const iterator = data[Symbol.iterator]() // 生成迭代器
	let item = { done: false }
	while (!item.done) {
		item = iterator.next()
		if (!item.done) {
			console.info(item)
			console.info(item.value)
		}
	}
}
let arr = [1, 2, 3]
let nodeList = document.getElementsByTagName('P')
let map = new Map()
map.set('name', 'zhangSan')
map.set('age', 12)
each(arr)
each(nodeList)
each(map)

只要具有 [Symbol.iterator] 属性的集合,就可以使用 each 方法进行遍历

for of

Symbol.iterator并不是人人都知道的,也不是每个人都需要封装一个 each 方法,因此,ES6提供了一个新的方法 for…of 来遍历:

// for...of 自动遍历拥有 Iterator 接口的数据结构
let arr = [1, 2, 3];
for (let item of arr) {
  console.log(item);
}

// 输出:1  2  3

说明 for…of 只是个语法糖,运行原理:

  1. 首先调用遍历对象 Symobo.iterator 方法,拿到遍历器对象;
  2. 每次循环,调用遍历器对象 next() 方法,得到 {value: …, done: … } 对象

等价于下面:

// 通过调用iterator,拿到迭代器对象
const iterator = arr[Symbol.iterator]()

// 初始化一个迭代结果
let now = { done: false }

// 循环往外迭代成员
while(!now.done) {
    now = iterator.next()
    if(!now.done) {
        console.log(`现在遍历到了${now.value}`)
    }
}

ES6 Iterator 和 Generator

Iterator 的价值不限于上述几个类型的遍历,还有 Generator 函数的使用。Generator 返回的数据符合 Iterator 接口遍历的要求,所以 Generator 函数也可以使用 Iterator 语法。

function* helloWorldGenerator() {
	yield 'hello'
	yield 'world'
	return 'ending'
}
// var hw = helloWorldGenerator()
// console.info(hw)
// hw.next() //{value: "hello", done: false}
// hw.next() //{value: "world", done: false}
// hw.next() //{value: "ending", done: true}
// hw.next() //{value: undefined, done: true}
// console.info(hw[Symbol.iterator])

for (var item of helloWorldGenerator()) {
	console.info(item) // hello  world
}

根据这个特点可以用 for…of 遍历普通对象
for…of 遍历普通对象的解决方法:

  1. 使用 Objet.keys 将对象键名生成一个数组,然后遍历该数组;
  2. Generator 函数重新包装对象
let person = {
  name: 'Ken',
  sex: 'Male'
}

// Generator 包装对象
function* entries(obj) {
  for (let key of Object.keys(obj)) {
    yield [key, obj[key]];
  }
}
for (let [key, value] of entries(person)) {
  console.log(`${key}: ${value}`);
}

// 输出:
// name: Ken 
// sex: Male

迭代器分类

内部迭代器

内部已经定义好了迭代规则,它完全接手整个迭代过程,外部只需要一次初始调用

实现:

function each(arr, fn) {
  for (let i = 0; i < arr.length; i++) {
    fn(i, arr[i])
  }
}

each([1, 2, 3], function(i, n) {
  console.log(i) // 0 1 2
  console.log(n) // 1 2 3
})

优缺点:

  • 优点:内部迭代器在调用的时候非常方便,外界不用关心迭代器内部的实现,跟迭代器的交互也仅仅是一次初始调用
  • 缺点:由于内部迭代器的迭代规则已经被提前规 定,上面的 each 函数就无法同时迭代2个数组,比如要比较两数组是否相等, 只能在其回调函数中作文章了, 代码如下:
var compare = function( ary1, ary2 ){
  if ( ary1.length !== ary2.length ){
    throw new Error ( 'ary1 和ary2 不相等' );
  }
  each( ary1, function( i, n ){
    if ( n !== ary2[ i ] ){
      throw new Error ( 'ary1 和ary2 不相等' );
    }
  });
  alert ( 'ary1 和ary2 相等' );
};
compare( [ 1, 2, 3 ], [ 1, 2, 4 ] ); // throw new Error ( 'ary1 和ary2 不相等' );

jQuery 的 $.each 以及 for…of 也是内部迭代器

外部迭代器

外部迭代器必须显式地请求迭代下一个元素

上面的"自定义个迭代器" 以及 Generator yield 就是外部迭代器。
解决下两个数组比较问题:

const arr1 = [1, 2, 3], arr2 = [1, 2, 3]
const container1 = new Container(arr1), container2 = new Container(arr2)
const iterator1 = container1.getIterator(), iterator2 = container2.getIterator()
function compare(iterator1, iterator2) {
  while (iterator1.hasNext() || iterator2.hasNext()) {
    if (iterator1.next() !== iterator2.next()) {
      return false
    }
  }
  return true
}
console.info(compare(iterator1, iterator2))

优缺点:

  • 优点:外部迭代器将遍历的权利转移到外部, 因此在调用的时候拥有了更多的自由性,
  • 缺点:调用方式较复杂
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 6
    评论
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值