javascript设计模式

1. 单例模型

定义:保证一个类仅有一个实例,并提供一个访问它的全局访问点
应用场景:当有些对象只需要一个的时候,不想被多次重复创建,比如线程池、全局缓存、浏览器中的window对象等。
单例模型根据具体的实现方式不同,又有以下几种划分:

(1)标准单例模型
/**
 * @description 标准单例模型
 */
function singleModel() {
  let single = function singleObj(name) {
    this.name = name; 
    this.instance = null; // 初始实例为空null
  }
  single.prototype.getName = function() {
    console.log(this.name); // 打印name属性
  };
  single.getInstance = function(name) {
    if(!this.instance) {
      this.instance == new single(name);
    }
    return this.instance;
  };
  let cat = single.getInstance("cat");
  let dog = single.getInstance("dog");
  console.log(cat === dog);
}

这种就是简单判断是否已经有了实例,有则返回,没有则创建。
缺点:使用single.getInstance创建实例,相比于new的方式还不够直白,不够透明化。

(2) 透明的单例模式
/**
 * @description 2. 透明单例模型
 */
function transparentSingleModel() {
  // 获取实例
  let Animal = (function() {
    let instance = null;  // 产生闭包
    let getInstance = function(name) {
      if(!instance) {
        this.name = name;
        this.getName(); // 可以是做其他的事
        return instance = this;
      }
      return instance;
    }
    Animal.prototype.getName = function() {
      console.log(this.name); 
    };
    return getInstance;
  })() // 匿名函数
  let cat = new Animal("cat");
  let dog = new Animal("dog");
  console.log(cat === dog)
}

缺点:这种写法会产生闭包和匿名函数,增加了复杂度。可以发现,和标准单例模式不同的是:在getInstance中多做了其他操作:设置name且执行getName方法,但是我们写的函数最好要保证职责要单一。

(3) 代理实现单例模式
/**
 * @description 3. 代理单例模式
 */
function proxySingleModal() {
  let Animal = function(name) { // 全部归到Animal类中处理
    this.name = name;
    this.getName();
  }
  Animal.prototype.getName = function() {
    console.log(this.name);
  }
  let proxyAnimal = (function(name) { // 仅负责获取实例
    let instance = null;
    return function(name) {
      if(!instance) {
        instance = new Animal(name);
      }
      return instance;
    }
  })();
  let cat = new proxyAnimal("cat");
  let dog = new proxyAnimal("dog");
  console.log(cat === dog)
}

我们可以采用引入代理,拆分出普通的类和获取实例的代理函数,将职能拆分出去,便于代码的维护。

(4)JS中的单例模型

需要注意的是:

全局变量不是单例模式
我们要尽量少的使用全局变量,减少全局命名空间污染,有两种方式可以减少全局变量污染:
① 使用命名空间

/**
 * @description 命名空间使用
 */
function useNamespace() {
  let App = {};
  App.namespace = function(name) {
    let parts = name.split('.'); 
    let current = App;
    for(let i in parts) {
      if(!current[parts[i]]) {
        current[parts[i]] = {};
      }
      current = current[parts[i]];
    }
  }
  App.namespace("dom.style");
  console.log(App) // { dom: { style: {} } }
}

② 使用闭包

/**
 * @description 闭包
 */
function closure() {
  let animal = (function(){
    let name = "cat";
    return { getAnimal : function() {
      return name;
    }}
  })()
  console.log(animal.getAnimal(), animal.name)
}

通过暴露方法,来暴露变量,外部也无法访问内部的name

2. 策略模式

定义:定义一系列的算法,把它们一个个封装成策略类,并且使它们可以相互替换。

/**
 * 根据级别获取对应的奖励
 */
function getScoreBonus() {
  let strategies = {
    'A': function(base){
      return base * 3
    },
    'B': function(base){
      return base * 2
    },
    'C': function(base){
      return base * 1
    }
  }
  function calculateBonus(base, type) {
    return strategies[type](base);
  }
  console.log(calculateBonus(100, "A"));
  console.log(calculateBonus(100, "B"));
}

策略模式还可以用来实现动画效果,比如根据原始位置,目标位置,执行时间,方向等需要一系列参数计算出最后的结果;还可以用来做表单验证:

/**
 * 策略模式的表单验证
 */
function form() {
  // 校验规则
  let strategries = {
    checkEmpty: function(value, errMsg) { // 检查空值
      if(value === '') {
        return `${value}:${errMsg}`;
      }
    },
    checkLength: function(value, length, errMsg) { // 检验长度
      if(value.length < length) {
        return `${value}:${errMsg}`;
      }
    },
    isMobile: function(value, errMsg) {
      if(!/(^1[3|5|8][0-9]{9}$)/.test(value)) { // 检查手机号
        return `${value}:${errMsg}`;
      }
    }
  }
  var validator = function() { 
    this.cache = [];  // 存储检验方法
  }
  // 添加验证项
  validator.prototype.add = function(value, rules) {
    var self = this;             
    for ( var i = 0, rule; rule = rules[ i++ ]; ){ 
      (function(rule) {
        var arr = rule.strategy.split(":");
        self.cache.push(function() {
          let strategy = arr.shift();
          arr.unshift(value);
          arr.push(rule.errorMsg);
          return strategries[strategy].apply(value, arr)
        })
      })(rule);
    }
  }
  // 检验所有项
  validator.prototype.check = function() {
    let errorArr = [];
    for(let i = 0, func; func = this.cache[i++];) {
      let msg = func();
      if(msg) {
        errorArr.push(msg);
      }
    }
    return errorArr;
  }
  // 校验
  let validateObj = new validator();
  validateObj.add("12345", [{strategy: "isMobile", errorMsg: "手机格式错误"}]);
  validateObj.add("张翠山", [{strategy: "checkLength:2", errorMsg: "长度最少2位"}]);
  validateObj.add("", [{strategy: "checkEmpty", errorMsg: "不能为空"}]);
  var errorMsg = validateObj.check();    // 获得校验结果    
  console.log(...errorMsg);
}

做法解析:先制定校验的一些规则,即策略,添加自定义的校验项,当检验规则发生变化时,更改代码会更简洁
优点:

  • 避免多重条件选择语句;
  • 具体的算法封装到策略中,使得代码简洁易懂;
  • 可复用性高
    缺点:
  • 在代码中增加了许多策略,必须要了解所有的策略;

3.代理模式

定义:为一个对象提供一个代用品或占位符,以便控制对它的访问。
比如明星的经纪人就是一种代理,代理明星谈合同和安排商业活动。
代理模式又分为以下几种:

  • 保护代理
    用于控制不同权限的对象对目标对象的访问,帮助对象过滤一些条件和请求;
  • 虚拟代理
    把一些开销很大的对象,延迟到真正需要它的时候创建;
    由于我们无法判断谁访问了某个对象,所以保护代理不容易实现,使用比较多的则是虚拟代理。
    使用虚拟代理可以实现图片预加载的功能:
// 背景图片设置
    let img = (function () {
      let imgNode = document.createElement("img");
      document.body.appendChild(imgNode);
      return {
        setSrc: function (src) {
          imgNode.src = src;
        },
      };
    })();

    let proxyImg = (function () {
      let proxy_img = new Image();
      // 等待加载完成,获取资源后再渲染
      proxy_img.onload = function () {
        img.setSrc(this.src);
      };
      return {
        setSrc: function (src) {
          // 设置为本地加载图片
          img.setSrc("../pictures/animal.jpg");
          proxy_img.src = src;
        },
      };
    })();
    proxyImg.setSrc(
     "https://ss3.bdstatic.com/70cFv8Sh_Q1YnxGkpoWK1HF6hhy/it/u=938935733,3477631526&fm=26&gp=0.jpg"
    );

除了可以实现图片预加载的功能,还能合并http请求
比如有多个选项,每点击一个选项则需要发起http请求,那么在短时间内,一直点击,则会对服务器造成一定的压力,那么此时就可以使用合并的方式,比如在每间隔3秒合并请求一次。

// 此处假装请求服务器
    let request = function (id) {
      console.log(`receive from ${id}`);
    };
    let checkAction = (function () {
      let idCache = [], // 此处产生了闭包
        timer;
      return function (id) {
        idCache.push(id);
        // 如果还在本次间隔期内的请求,则只添加到缓存数组
        if (timer) {
          return;
        }
        timer = setTimeout(() => {
          request(idCache.join()); // 请求数据
          clearTimeout(timer); // 清空计时器
          timer = null;
          caches.length = 0; // 清空缓存
        }, 3000);
      };
    })();
    let checkbox = document.getElementsByTagName("input");
    for (let item of checkbox) {
      item.onclick = function () {
        if (this.checked) {
          checkAction(this.id);
        }
      };
    }
  • 缓存代理
    缓存代理,可以为一些复杂的计算缓存值,举个例子,假装累乘是个复杂的操作
let mult = function () {
  console.log("我在计算!");
  let a = 1;
  for (let i = 0, l = arguments.length; i < l; i++) {
    a = a * arguments[i];
  }
  return a;
};

let proxyMult = (function () {
  let cache = {};
  return function () {
    let args = Array.prototype.join.call(arguments, ",");
    // 根据参数的拼接方式决定属性名称,1,2,3
    // 使用 in 会从对象或者原型中查找
    if (args in cache) {
      return cache[args];
    }
    return (cache[args] = mult.apply(this, arguments));
  };
})();
console.log(proxyMult(1, 2, 3));
console.log(proxyMult(1, 2, 3));

运行结果:

我在计算!
6
6

可以发现同样的参数,实际上就只会被计算一次。同样的道理,同一个页面的数据,也可以通过缓存代理缓存起来,通过调用异步函数之后,在回调函数中缓存数据。

4. 迭代器模式

定义:提供一种方法顺序访问一个聚合对象中的各个元素,而不需要暴露该对选哪个的内部表示。
迭代器可以分为两种:

  • 内部迭代器
    比如像jquery中的each,js数组的forEach,外界不需要关心迭代器内部的实现,调用就完事了,但是局限是,这个迭代方式被约定好了,跟迭代器的交互也仅仅是一次初始的调用,比如foEach就只能迭代一个数组。
  • 外部迭代器
    外部迭代器必须显示地请求迭代下一个元素,就是可以手动请求下一个元素

使用js实现一个迭代器:

let Iterator = function (obj) {
  let current = 0;
  // 将索引指向下一个
  let next = () => (next += 1);
  // 判断是否已经取道了最后一个
  let isDone = () => current >= obj.length;
  let getCurrentItem = function () {
    return obj[current];
  };
  return {
    next,
    isDone,
    getCurrentItem,
  };
};
// 判断两个对象所有位置上对应的元素是否都相等
let compare = function (iterator1, iterator2) {
  while (!iterator1.isDone && !iterator2.isDone) {
    if (iterator1.getCurrentItem() !== iterator2.getCurrentItem()) {
      throw new Error("不相等");
      break;
    }
    iterator1.next();
    iterator2.next();
  }
  console.log("相等");
};
let iterator1 = new Iterator([1, 2, 3]);
let iterator2 = new Iterator([1, 2, 3]);
compare(iterator1, iterator2);

除此之外,迭代器还可以迭代类数组对象,比如arguments, {"0":'a', "1": 'b'},无论是内部迭代器还是外部迭代器,只要被迭代的聚合对象有length属性且可下标访问,就可以被迭代。

5. 发布-订阅模式(观察者模式)

定义:定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都会得到通知。
在js中一般有事件模型来替代订阅模式。
作用:
(1)广泛应用于异步编程中,一种替代传递回调函数的方案,比如订阅ajax请求的error,success等时间,无须关注对象在异步运行期间的内部状态,只关心想要接收的事件的发生点。
(2)可以取代对象之间编码的通知机制,一个对象不用显示地调用另一个对象的接口,降低耦合度。
DOM事件也是使用发布-订阅模式,比如用户点击了某个按钮,我们只需要注册点击事件,等待用户点击了再触发事件。使用addEventListener还能随意增加或者删除订阅者。

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

  • 设计模式 一、单例模式 definition:保证一个类仅有一个实例,并提供一个访问它的全局访问点。 普通单例模式...
    了凡和纤风阅读 4,253评论 0 3
  • javascript设计模式与开发实践 设计模式 每个设计模式我们需要从三点问题入手: 定义 作用 用法与实现 单...
    穿牛仔裤的蚊子阅读 4,503评论 0 13
  • 接触前端两三个月的时候,那时候只是听说设计模式很重要,然后我就去读了一本设计模式的书,读了一部分,也不知道这些设计...
    艰苦奋斗的侯小憨阅读 3,204评论 2 39
  • 1. 设计模式概述 简介在面向对象软件设计过程中针对特定问题的简洁而优雅的解决方案。即设计模式是在某种场合下对某个...
    nimw阅读 615评论 0 0
  • 久违的晴天,家长会。 家长大会开好到教室时,离放学已经没多少时间了。班主任说已经安排了三个家长分享经验。 放学铃声...
    飘雪儿5阅读 7,875评论 16 22

友情链接更多精彩内容