JavaScript设计模式之迭代器模式
概念
在《JavaScript设计模式与开发实践》 中对迭代器模式的定义为 提供一种方法顺序访问一个聚合对象中的各个元素,而又不需要暴露该对象的内部表示。迭代器模式可以把迭代的过程从业务逻辑中分离出来,在使用迭代器模式之后,即使不关心对象的内部构造,也可以按顺序返回其中的每个元素。
迭代器分类
迭代器可以分为内部迭代器和外部迭代器两大类。
- 内部迭代器完全接手整个迭代过程,外部只需一次初始调用。
- 外部迭代器必须显示地请求迭代下一个元素。外部迭代器增加了一些调用的复杂度,但相对也增强了迭代器的灵活性,我们可以手工的控制迭代的过程或顺序。
JS中的迭代器
在
JavaScript
中,相信大家经常听到过迭代、循环之类的名词,包括ES6
中还新增了迭代器的概念,使得大家一头雾水。究竟什么是迭代呢?平时的用for又是什么呢?那么我就先来介绍把这两个概念区分一下: 循环,循环就是在满足一定条件时,重复执行同一段代码,典型的例子:do...while
迭代,迭代是指按顺序逐个访问对象中的每一项,典型的例子:forEach
那么什么样的对象可以被迭代呢?需要满足什么条件呢?
- 大家可以去看MDN中的迭代协议,要成为可迭代对象,对象必须要实现必须实现
@@iterator
方法,通常可以访问常量Symbol.iterator
访问该属性。 - 目前的内置可迭代对象有:
String、Array、TypedArray、Map、Set
,他们的原型对象都实现了@@iterator
方法。
我们可以打印一下这个对象,看下效果
- 当这个对象是可迭代对象时,我们可以通过调用[
Symbol.iterator
]方法来按顺序遍历对象中的每一项:
const arr = [1, 2, 3, 4];
// 迭代器
const iterator = arr[Symbol.iterator]()
iterator.next(); // { value: 1, done: false }
iterator.next(); // { value: 2, done: false }
- 如果我们需要将一个任意的对象变成可迭代对象时,只要提供自己的
@@iterator
方法即可,比如MDN中的例子
通过上面的例子和迭代器简单介绍,大家应该也能清楚了,我们的遍历比如
forEach、for...of
等方法,其实就是封装了一个遍历可迭代对象的方法,属于内部迭代器。而当我们通过调用next
方法自行控制迭代对象遍历时,比如ES6中的生成器函数,这种就属于外部迭代器。
源码中的迭代器模式
jQuery.each
jQuery.each
会遍历一个jQuery对象,为每个匹配元素执行一个函数。通常我们可以用来遍历dom元素,比如$('li').each
,除此之外,还可以用来遍历对象、数组等,比如$.each([1, 2], function(index, value) {})
或者$.each({ name: 'a', age: 18 }, function(k, v) {})
。通过一个each方法遍历各种不同对象是如何实现的呢?我们一起看下jQuery
源码中是如何做的:
// src/core.js
each: function( obj, callback ) {
var length, i = 0;
if (isArrayLike(obj)) {
length = obj.length;
for(; i < length; i++ ) {
if (callback.call(obj[i], i, obj[i]) === false) {
break;
}
}
} else {
for (i in obj) {
if (callback.call( obj[ i ], i, obj[ i ] ) === false) {
break;
}
}
}
return obj;
},
// src/core/isArrayLike.js
function isArrayLike( obj ) {
var length = !!obj && obj.length,
type = toType( obj );
if ( typeof obj === "function" || isWindow( obj ) ) {
return false;
}
return type === "array" || length === 0 ||
typeof length === "number" && length > 0 && ( length - 1 ) in obj;
}
可以看到这个遍历的逻辑非常简单:如果是类数组时(isArrayLike
)则通过下标遍历,即遍历i
,如果是对象形式的话,则通过i in obj
,此时的i,就是对象中的key值。
而当我们遍历dom,即$('li')
时,此时jQuery先会通过一系列的转换,返回一个isArrayLike
类型的对象,如图:
然后再通过each方法,实现dom元素的遍历。
axios中的迭代器模式
在
axios
源码中,也使用到很多each方法,比如同时给axios上添加['post', 'put', 'patch']
等方法,那么axios中是如何实现的呢?
// lib/utils.js
function forEach(obj, fn) {
// Don't bother if no value provided
if (obj === null || typeof obj === 'undefined') {
return;
}
// Force an array if not already something iterable
if (typeof obj !== 'object') {
/*eslint no-param-reassign:0*/
obj = [obj];
}
if (isArray(obj)) {
// Iterate over array values
for (var i = 0, l = obj.length; i < l; i++) {
fn.call(null, obj[i], i, obj);
}
} else {
// Iterate over object keys
for (var key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
fn.call(null, obj[key], key, obj);
}
}
}
}
通过源码发现其实与jQuery
中的类似:如果是数组则使用下标方式遍历,是对象则使用for...in
方式遍历,但有不同的点在于:Axios
中对象遍历加了一层判断hasOwnProperty
,这个方法是判断对象自身属性中是否具有指定的属性,可以忽略掉那些从原型链上继承来的属性。
迭代器模式应用
迭代器模式在平时的开发中其实是比较常见的,但由于
js
语言中已经内置了迭代器,我们通常不需要刻意进行封装。但有几个场景下也可以考虑自行封装迭代器:
- 当我们需要遍历不同类型的数据时,类似
jQuery
中的each
方法,可以考虑封装一个统一的方法,在方法内部处理遍历的逻辑,实现一个内部迭代器。这样的优势在于,可以将遍历逻辑与业务解耦,这样也满足开闭原则和单一职责原则。- 当我们需要手动控制迭代的过程和顺序时,也可以考虑
ES6
的迭代器或自行封装可迭代对象,通过next
方法手动的控制迭代时机,在一些有流程化的需求中,应该会有不错效果。- 当我们需要通过不同的规则去使用不同方法时,也可以考虑使用迭代器模式,按顺序判断是否满足条件,举一个书中的例子:根据不同浏览器选择相应的上传组件
根据不同浏览器选择相应的上传组件
原始的代码,可能是这样的:
var getUploadObj = function() {
try {
return new ActiveXObject('TXFTNActiveX.FTNUpload'); // IE 上传控件
} cache(e) {
if (supportFlash()) {
var str = '<object type="application/x-shockwave-flash"></object>'
return $(str).appendTo($('body'));
} else {
var str = '<input name="file" type="file" />' // 表单上传
return $(str).appendTo($('body'));
}
}
}
我们会优先选择控件上传,如果没有安装上传控件则使用Flash上传,如果Flash也没有安装,那就只好使用浏览器原生的表单上传了。但是我们目前的代码里充斥着try...cache、if
等,很难阅读而且违反了开闭原则。
我们考虑用迭代器模式来优化:
- 提供一个可以被迭代的方法,使得
getActiveUploadObj
、getFlashUploadObj
、getFormUploadObj
依照优先级被迭代- 如果正在被迭代的函数返回一个对象,则表示找到了正确的upload对象,反之,如果返回
false
,则让迭代器继续工作
迭代器代码如下:
// 定义各个上传方法
var getActiveUploadObj = function() {
try {
return new ActiveXObject('TXFTNActiveX.FTNUpload');
} cache(e) {
return false
}
}
var getFlashUploadObj = function() {
if (supportFlash()) {
var str = '<object type="application/x-shockwave-flash"></object>'
return $(str).appendTo($('body'));
}
return false
}
var getFormUploadObj = function() {
var str = '<input name="file" type="file" />' // 表单上传
return $(str).appendTo($('body'));
}
// 按优先级迭代函数
var iteratorUploadObj = function() {
for (var i = 0, fn; fn = arguments[i++];) {
var uploadObj = fn();
if (uploadObj !== false) {
return uploadObj
}
}
}
// 获取可上传upload对象
var uploadObj = iteratorUploadObj(getActiveUploadObj, getFlashUploadObj, getFormUploadObj)
这样重构后可以看到各个上传对象的方法互不干扰,可以很好的维护和扩展代码。如果我们后面再添加一个WebKit控件上传和HTML5上传,我们仅仅要做下面的工作:
// 定义上传函数
var getWebKitUploadObj = function() {}
var getHtml5UploadObj = function() {}
// 按照优先级添加到迭代器
var uploadObj = iteratorUploadObj(getActiveUploadObj, getFlashUploadObj, getFormUploadObj, getWebKitUploadObj, getHtml5UploadObj)
总结
这篇文章我们介绍了迭代器模式的概念,同时也介绍了es6中迭代器是如何迭代的,也算是对过去知识的巩固。我们也介绍了一些优秀的开源代码是如何使用迭代器模式的,它们使用场景、封装逻辑类似。这也同时给大家一些启发:迭代器模式可以在哪些场景下使用。也希望小伙伴们能够掌握迭代器模式,能够分享出更好的实战场景,共同学习进步~~ 感谢阅读 🙏
转载自:https://juejin.cn/post/7161818772385792014