参考资料
曾探《JavaScript设计模式与开发实践》;
定义
提供一种方法顺序访问一个聚合对象中的各个元素,而又不需要暴露该对象的内部表示。迭代器模式可以把迭代的过程从业务逻辑中分离出来,在使用迭代器模式之后,即使不关心对象的内部构造,也可以按顺序返回其中的每个元素。
迭代、循环有什么区别:
循环,循环就是在满足一定条件时,重复执行同一段代码,典型的例子: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中的例子
使用场景:迭代器模式在平时的开发中其实是比较常见的,但由于js语言中已经内置了迭代器,我们通常不需要刻意进行封装。但有几个场景下也可以考虑自行封装迭代器:
当我们需要遍历不同类型的数据时,类似jQuery中的each方法,可以考虑封装一个统一的方法,在方法内部处理遍历的逻辑,实现一个内部迭代器。这样的优势在于,可以将遍历逻辑与业务解耦,这样也满足开闭原则和单一职责原则。
当我们需要手动控制迭代的过程和顺序时,也可以考虑ES6的迭代器或自行封装可迭代对象,通过next方法手动的控制迭代时机,在一些有流程化的需求中,应该会有不错效果。
当我们需要通过不同的规则去使用不同方法时,也可以考虑使用迭代器模式,按顺序判断是否满足条件。
jQuery中的迭代器
迭代器模式就是循环访问聚合对象中的各个元素,比如jQuery中的$.each:
$.each = function( obj, callback ) {
var value,
i = 0,
length = obj.length,
isArray = isArraylike( obj );
if ( isArray ) { // 迭代类数组
for ( ; i < length; i++ ) {
value = callback.call( obj[ i ], i, obj[ i ] );
if ( value === false ) {
break;
}
}
} else {
for ( i in obj ) { // 迭代object 对象
value = callback.call( obj[ i ], i, obj[ i ] );
if ( value === false ) {
break;
}
}
}
return obj;
};
$.each( [1, 2, 3], function( i, n ){
console.log( '当前下标为: '+ i );
console.log( '当前值为:' + n );
});
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,这个方法是判断对象自身属性中是否具有指定的属性,可以忽略掉那些从原型链上继承来的属性。
实现自己的迭代器
参数1:被循环的数组;参数2:回调函数。
var each = function( ary, callback ){
for ( var i = 0, l = ary.length; i < l; i++ ){
callback.call( ary[i], i, ary[ i ] ); // 把下标和元素当作参数传给callback 函数
}
};
each( [ 1, 2, 3 ], function( i, n ){
alert ( [ i, n ] );
});
迭代器分类
迭代器可以分为内部迭代器和外部迭代器两大类。
内部迭代器完全接手整个迭代过程,外部只需一次初始调用。
var each = function( ary, callback ){
for ( var i = 0, l = ary.length; i < l; i++ ){
callback.call( ary[i], i, ary[ i ] ); // 把下标和元素当作参数传给callback 函数
}
};
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 不相等' );
外部迭代器必须显示地请求迭代下一个元素。外部迭代器增加了一些调用的复杂度,但相对也增强了迭代器的灵活性,我们可以手工的控制迭代的过程或顺序。
var Iterator = function( obj ){
var current = 0;
var next = function(){
current += 1;
};
var isDone = function(){
return current >= obj.length;
};
var getCurrItem = function(){
return obj[ current ];
};
return {
next: next,
isDone: isDone,
getCurrItem: getCurrItem
}
};
//再看看如何改写compare 函数:
var compare = function( iterator1, iterator2 ){
while( !iterator1.isDone() && !iterator2.isDone() ){
if ( iterator1.getCurrItem() !== iterator2.getCurrItem() ){
throw new Error ( 'iterator1 和iterator2 不相等' );
}
iterator1.next();
iterator2.next();
}
alert ( 'iterator1 和iterator2 相等' );
}
var iterator1 = Iterator( [ 1, 2, 3 ] );
var iterator2 = Iterator( [ 1, 2, 3 ] );
compare( iterator1, iterator2 ); // 输出:iterator1 和iterator2 相等
迭代器模式应用-根据不同浏览器选择相应的上传组件
我们会优先选择控件上传,如果没有安装上传控件则使用Flash上传,如果Flash也没有安装,那就只好使用浏览器原生的表单上传了。但是我们目前的代码里充斥着try...cache、if等,很难阅读而且违反了开闭原则。
原始的代码,可能是这样的:
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'));
}
}
}
我们考虑用迭代器模式来优化:
提供一个可以被迭代的方法,使得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)