• 设计模式之迭代器模式


    简介

    相关概念

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

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

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

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

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

    类图

    在这里插入图片描述

    迭代器模式应该具备的角色信息
    • 抽象迭代器(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]));
    复制代码
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    外部迭代器

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

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

    外部迭代器需要提供的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
    复制代码
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    迭代器对比
    • 内部迭代器调用方式上简单,和外部的使用也只进行一次初始化即可,但是在灵活性上欠缺,适合较为简单的迭代场景
    • 外部迭代器相对于内部迭代器来说调用方式上复杂了许多,但在灵活度上来说有了明显提升,可以满足更多多边的需求;
    • 两者没有绝对的优劣之分,需要根据特定的需求场景来说; 迭代器模式不仅可以迭代数组,还可以迭代类数组对象,只要被迭代的聚合对象有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
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    应用场景
    1. 访问一个集合对象的内容,无需暴露其内部表示;
    2. 为遍历不同对的集合结构提供一个统一的访问接口;
    3. 需要为聚合对象提供多种遍历方式
    是否符合设计原则
    • 使用者和目标分离,解耦
    • 目标能自行控制内部逻辑
    • 使用者不关心目标和内部结构
    相关应用
    封装上传检测函数,用于适配不同浏览器对上传功能的支持度

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

    const getActiveUploadObj = function () {
        try {
            return new ActionXObject('TXFTNActiveX.FTNUpload')
        } catch (e) {
            return false
        }
    }
    
    const getFlashUploadObj = function () {
        if (supportFlash()) { // 未提供这个函数, 是否支持flash
            const str = ``
            return $(str).appendTo($('body'))
        }
        return false
    }
    
    const getFormUploadObj = function () {
        const str = '' // 表单上传
        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)
    复制代码
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49

    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 }
    复制代码
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    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 }
    复制代码
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    调用
    • 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]
    }
    复制代码
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    更改原生及对象,使其支持迭代
    // 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);
    // }
    复制代码
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46

    原生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)
      }
    }
    复制代码
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    优缺点分析

    优点
    • 为不同的聚合对象提供一致的遍历接口
    • 将集合对象的具体迭代行为接口抽离到迭代器中,简化集合对象的逻辑
    • 同一个集合对象可以有不同的迭代行为
    • 将集合对象与迭代接口抽离解耦,各自的变化不会相互影响
    缺点
    • 对于比较简单的对象遍历,使用迭代器反而繁琐了
    • 因为将对象数据和迭代接口抽离,增加新的内存去存储对应的逻辑,在一定程度上增加了系统的复杂性和存储成本
  • 相关阅读:
    浏览器中环境判断和系统判断-js封装
    第二次新建一个Vue前端项目我都做了哪些操作
    Python基于随机游走模型的PageRank算法及应用
    内网穿透工具frp使用入门
    隧道精确定位系统全方位保障隧道施工安全
    超星平台——东电影答案
    如何将Vapor项目部署到Ubuntu?
    前端开发:创建可拖动的固定位置 `<div>` 和自动隐藏悬浮按钮
    maven打包命令打出的可执行的jar包和可依赖的jar包的区别
    SpringMVC中文乱码(request或response)前后端处理
  • 原文地址:https://blog.csdn.net/qq_42583549/article/details/126922596