JavaScript设计模式之迭代器模式

1,787 阅读6分钟

本文为稀土掘金技术社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!

概念

在《JavaScript设计模式与开发实践》 中对迭代器模式的定义为 提供一种方法顺序访问一个聚合对象中的各个元素,而又不需要暴露该对象的内部表示迭代器模式可以把迭代的过程从业务逻辑中分离出来,在使用迭代器模式之后,即使不关心对象的内部构造,也可以按顺序返回其中的每个元素。

迭代器分类

迭代器可以分为内部迭代器外部迭代器两大类。

  • 内部迭代器完全接手整个迭代过程,外部只需一次初始调用。
  • 外部迭代器必须显示地请求迭代下一个元素。外部迭代器增加了一些调用的复杂度,但相对也增强了迭代器的灵活性,我们可以手工的控制迭代的过程或顺序。

JS中的迭代器

JavaScript中,相信大家经常听到过迭代、循环之类的名词,包括ES6中还新增了迭代器的概念,使得大家一头雾水。究竟什么是迭代呢?平时的用for又是什么呢?那么我就先来介绍把这两个概念区分一下:
循环,循环就是在满足一定条件时,重复执行同一段代码,典型的例子:do...while
迭代,迭代是指按顺序逐个访问对象中的每一项,典型的例子:forEach

那么什么样的对象可以被迭代呢?需要满足什么条件呢?

  • 大家可以去看MDN中的迭代协议,要成为可迭代对象,对象必须要实现必须实现@@iterator方法,通常可以访问常量Symbol.iterator访问该属性。
  • 目前的内置可迭代对象有:String、Array、TypedArray、Map、Set,他们的原型对象都实现了@@iterator方法。

我们可以打印一下这个对象,看下效果 image.png

  • 当这个对象是可迭代对象时,我们可以通过调用[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类型的对象,如图:

image.png

然后再通过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等,很难阅读而且违反了开闭原则

我们考虑用迭代器模式来优化:

  • 提供一个可以被迭代的方法,使得getActiveUploadObjgetFlashUploadObjgetFormUploadObj依照优先级被迭代
  • 如果正在被迭代的函数返回一个对象,则表示找到了正确的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中迭代器是如何迭代的,也算是对过去知识的巩固。我们也介绍了一些优秀的开源代码是如何使用迭代器模式的,它们使用场景、封装逻辑类似。这也同时给大家一些启发:迭代器模式可以在哪些场景下使用。也希望小伙伴们能够掌握迭代器模式,能够分享出更好的实战场景,共同学习进步~~
感谢阅读 🙏