设计模式-迭代器模式

714 阅读6分钟

简介

相关概念

目的性极强:只解决一个问题--遍历
迭代器模式不需要关系集合的内部结构,而常规的公元前的js遍历中需要考虑到具体的内部结构,如远古的forEach对于同样是集合出生的类数组就显得很尴尬了,因此就需要兼容不同内部结构的数据类型的遍历,需要有额外的兼容处理逻辑,如jQuery中的$.each(nodes,(index,aNode) => {// ......})

迭代器模式提供一种方法顺序访问一个聚合对象(将多个对象聚合在一起形成的总体)中的各个元素,而又不暴露该对象的内部表示,其本质是抽离集合对象迭代行为到迭代器中,对外提供一致的访问接口;

迭代器和循环不是等价的,循环是迭代器的基础,迭代器是按照指定的顺序进行多次重复的循环某一程序,但是会有明确的终止条件;

遍历是指按照规定访问非线性结构中的每一项 - 可以访问某一个区间段内的元素,而迭代只能按顺序依次访问;

迭代器模式可以把迭代的过程从业务逻辑中分离出来,在使用迭代器模式之后,即使不了解对象的内部构造,也可以按照顺序访问其中每个元素;

迭代器模式在生活中常见的有:快递传送带和地铁扫码进站等;
快递传送带:不用关注快递内部是什么物品,只需要将快递进行打包贴码即可,实现解耦的效果;
地铁扫码:不用关注扫码人的个人信息(性别、体征等),只需要实现扫码进站即可;

UML类图

image.png

迭代器模式应该具备的角色信息
  • 抽象迭代器(Iterator):抽象迭代器负责定义访问和遍历元素的接口。
  • 具体迭代器(ConcreteIterator):提供具体的元素遍历行为。
  • 抽象容器(IAggregate):负责定义提供具体迭代器的接口。
  • 具体容器(ConcreteAggregate):创建具体迭代器。

特点

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

分类

  1. 内部迭代器 (jQuery 的 $.each / for...of)
  2. 外部迭代器 (ES6 的 yield)
内部迭代器

内部定义迭代规则,控制整个迭代规则,外部只需一次初始调用即可

内部迭代器在调用的时候非常方便,外部只需要进行一次初始化即可,不用关心内部的实现,但正因为内部提前定义了迭代规则,所以无法较为方便的进行相关拓展、无法满足复杂遍历的需求,确实灵活性,可以在回调函数里进行一些操作;

function each(ary, callback) {
  for (let i = 0; i < ary.length; i++) {
    callback.call(ary[i], i, ary[i]);
  }
}
each([1, 2, 3], (i, n) => console.log([i, n]));
外部迭代器

外部显示的进行控制迭代你下一个数据项

外部迭代器必须显示的请求迭代下一个元素,增加了一些遍历调用的复杂度,但因为需要显示的控制迭代逻辑,因此相对灵活一些,可以手动控制迭代过程或者迭代顺序;

外部迭代器需要提供的API
  • 访问下一个元素的方法 next()
  • 当前遍历是否结束 isDone()
  • 获取当前元素 getCurrentItem()
const iterator = function (obj) {
  let current = 0;

  const next = () => (current += 1);

  const isDone = () => current >= obj.length;

  const getCurrItem = () => {
    return obj[current];
  };

  const getCurrIndex = () => {
    return current;
  };

  return {
    next,
    isDone,
    getCurrItem,
    getCurrIndex,
    length: obj.length,
  };
};

let test = iterator(["a", "b", "c"]);

console.log(test.length," length");
while (!test.isDone()) {
  console.log(test.getCurrItem()," -> ",test.getCurrIndex());
  test.next();
}

// 3  length
// a  ->  0
// b  ->  1
// c  ->  2
迭代器对比
  • 内部迭代器调用方式上简单,和外部的使用也只进行一次初始化即可,但是在灵活性上欠缺,适合较为简单的迭代场景
  • 外部迭代器相对于内部迭代器来说调用方式上复杂了许多,但在灵活度上来说有了明显提升,可以满足更多多边的需求;
  • 两者没有绝对的优劣之分,需要根据特定的需求场景来说; 迭代器模式不仅可以迭代数组,还可以迭代类数组对象,只要被迭代的聚合对象有length属性且可以用下标访问,就可以被迭代
  if (isArray) {
    // 迭代类数组
    for (; i < length; i++) {
      value = callback.call(obj[i], i, obj[i]);
      if (value === false) {
        break;
      }
    }
  } else {
    // 迭代object 对象
    for (i in obj) {
      value = callback.call(obj[i], i, obj[i]);
      if (value === false) {
        break;
      }
    }
  }
  return obj;
};

应用场景

  1. 访问一个集合对象的内容,无需暴露其内部表示;
  2. 为遍历不同对的集合结构提供一个统一的访问接口;
  3. 需要为聚合对象提供多种遍历方式

是否符合设计原则

  • 使用者和目标分离,解耦
  • 目标能自行控制内部逻辑
  • 使用者不关心目标和内部结构

相关应用

封装上传检测函数,用于适配不同浏览器对上传功能的支持度

常规情况下需要封装不同的兼容函数到不同环境中,并且需要做错误处理,当采用迭代器模式将不同的上传方式分别定义后再进行统一管理,这时外部只需要调用指定暴露的接口即可,无需关注内部实现细节,同时也方便后续的拓展与维护;


const getActiveUploadObj = function () {
    try {
        return new ActionXObject('TXFTNActiveX.FTNUpload')
    } catch (e) {
        return false
    }
}

const getFlashUploadObj = function () {
    if (supportFlash()) { // 未提供这个函数, 是否支持flash
        const str = `<object type="application/x-shockwave-flash"></object>`
        return $(str).appendTo($('body'))
    }
    return false
}

const getFormUploadObj = function () {
    const str = '<input name="file" type="file" />' // 表单上传
    return $(str).appendTo($('body'))
}


// 这三个函数都有同一个约定, 如果该函数里面的upload对象是可用的,则让函数返回该对象, 否则返回false, 提示迭代器继续迭代

// 1.提供一个可以被迭代的方法, 使用三个方法 依照优先级被循环迭代
// 2. 如果正在被迭代的函数返回一个函数,则表示找到了upload的对象, 反之函数返回false,则继续迭代

const iteratorUploadObj = function () {
    for (let i = 0, fn; fn = arguments[i++]) {
        const uploadObj = fn()
        if (uploadObj !== false) {
            return uploadObj
        }
    }
}

const uploadObj = iteratorUploadObj(getActiveUploadObj, getFlashUploadObj, iteratorUploadObj)

// 后续拓展
// const getWebkitUploadObj = function () {
//     // ...代码略
// }
// const getHtml5UploadObj = function () {
//     // ...代码略
// }

// // 依靠优先级添加进迭代器
// const uploadObj = iteratorUploadObj(getActiveUploadObj, getFlashUploadObj, iteratorUploadObj, getWebkitUploadObj, getHtml5UploadObj)

code by

for...of...的内部interator实现可以迭代类数组对象的功能,使用for...of...的时候会调用Symbol.iterator接口

一种数据结构只要定义了 Iterator 接口,我们就称这种数据结构是可遍历的

js中的Iterator接口定义在数据结构的Symbol.iterator属性上,可以通过调用原生的Symbol.iterator方法返回一个遍历器对象,该对象就包含next等方法属性,放回当前对象的信息;

let arr = ['a', 'b', 'c'];
let iter = arr[Symbol.iterator](); // 返回一个遍历器对象
iter.next() // { value: 'a', done: false }
iter.next() // { value: 'b', done: false }
iter.next() // { value: 'c', done: false }
iter.next() // { value: undefined, done: true }

interator遍历的时候会先生成一个指针对象,指向当前数据结构的起始位置。也就是说,遍历器对象本质上,就是一个指针对象。这个对象里面有一个next方法,然后调用该next方法,移动指针使得指针指向数据结构中的第一个元素。每调用一次next方法,指针就指向数据结构里的下一个元素,这样不断的调用next方法就可以实现遍历元素的效果了!另外每次调用next方法,都会返回数据结构的当前成员的信息。具体来说,就是返回一个包含valuedone两个属性的对象。其中,value属性是当前成员的值,done属性是一个布尔值,表示遍历是否结束,没有结束返回false,结束为true

function myIteration(arr) {
  let index = 0;
  return {
    next: function () {
      return index < arr.length ?
        { value: arr[index++], done: false } :
        { value: undefined, done: true }
    }
  }
}
var test = myIteration([1, 2])
console.log(test.next()) //{ "value": 1, "done": false }
console.log(test.next()) //{ "value": 2, "done": false }
console.log(test.next()) //{ "value": undefined, "done": true }
调用
  • generator函数、解构赋值、拓展运算符、Array.from()、Promise.all()等
let iterator = {
  [Symbol.iterator]: function* () {
    yield 'a';
    yield 'b';
    yield 'c';
  }
};
console.log([...iterator]) // [a, b, c]

for (let i of iterator){
  console.log(i)  // [a, b, c]
}
更改原生及对象,使其支持迭代
// let obj = {
//   'name': 'Lbxin',
//   'age': '20'
// }
// for (let i of obj) {
//   console.log(i) //TypeError: obj is not iterable , obj不是可迭代对象
// }

// 添加 Symbol.iterator 接口 使其支持迭代
let obj = {
  data: ['name: Lbxin', 'age: 20'],
  [Symbol.iterator]: function () {
    const _this = this
    let index = 0;
    return {
      next: function () {
        if (index < _this.data.length) {
          return {
            value: _this.data[index++],
            done: false
          };
        }
        return { value: undefined, done: true };
      }
    }
  }
}
for (let i of obj) {
  console.log(i)
}
// name: Lbxin
// age: 20


// const obj = {
//     arr : [1,2,3,4],
//     *[Symbol.iterator](){
//         yield* this.arr;
//     }
// }

// // 1 2 3 4
// for(let i of obj){
//     console.log(i);
// }

原生js具备interator接口得到数据结构有

  • Array
  • Map
  • Set
  • String
  • TypedArray
  • 函数的 arguments 对象
  • NodeList 对象

通用迭代器封装

class Iterator {
  constructor(data) {
    this.data = data
    this.index = 0
  }

  next() {
    const list = this.data.list
    const flag = this.hasNext()
    if (flag) {
      return { value: list[this.index++], flag }
    } else {
      return { value: undefined, flag }
    }
  }

  hasNext() {
    const lg = this.data.list.length
    if (this.index >= lg) return false
    else return true
  }
}

class data {
  constructor(list) {
    this.list = list
  }

  getIterator() {
    return new Iterator(this)
  }
}

迭代器协议分类 -- 可迭代协议和迭代器协议

可迭代协议

允许JS对象定义或定制它们的迭代行为,要想成为可迭代对象,该对象必须实现@@iterator方法,即该对象(或其原型链上的某个对象)必须有一个键为@@iterator的属性,可通过常量Symbol.interator(该属性是一个无参数的函数,其返回值为一个复合迭代器协议的对象)访问该属性;

当一个对象需要被迭代时(如通过for...of...循环时),首先会不带参数的调用它的@@iterator方法,然后使用该方法返回的需要迭代的值;

迭代器协议

迭代器协议定义了产生一系列的值的标准方式;只有实现了一个拥有以下语义的next()方法,一个对象才可以成为迭代器;一个迭代器对象一般包括next()done:可选value:可选(当done为true时可以省略)、return(value):可选(调用该方法表明迭代器的调用者不打算调用更多的next(),并且可以进行清理工作)、throw(exception):可选(调用这个方法表明迭代器的额调用者监测到错误的情况了)

// *可迭代迭代器*
// Satisfies both the Iterator Protocol and Iterable
const myIterator = {
  next() {
    // ...
  },
  [Symbol.iterator]() {
    return this;
  },
};

优缺点分析

优点
  • 为不同的聚合对象提供一致的遍历接口
  • 将集合对象的具体迭代行为接口抽离到迭代器中,简化集合对象的逻辑
  • 同一个集合对象可以有不同的迭代行为
  • 将集合对象与迭代接口抽离解耦,各自的变化不会相互影响
缺点
  • 对于比较简单的对象遍历,使用迭代器反而繁琐了
  • 因为将对象数据和迭代接口抽离,增加新的内存去存储对应的逻辑,在一定程度上增加了系统的复杂性和存储成本

推荐文献

js - 迭代器与生成器
一文搞懂JS新的原始数据类型—Symbol👍🏻👍🏻👍🏻