单例模式

概述

顾名思义,也就是一个类只允许一个实例,多用于线程池,全局缓存等等

满足条件:1. 只有一个实例;2. 全局访问

但其实,js本身就提供了单例模式,比如就在全局 const instance = {}; 这便满足的单例的两个条件

所以传统的单例模式在 js 中是不适用的

实现

普通实现

const CreateInstance = (function () {
  let _instance = null;
  return function (name) {
    if (_instance) return _instance;
    this.name = name;
    _instance = this;
  };
})();
const instance = new CreateInstance("x-1");
const instance2 = new CreateInstance("x-2");
console.log(instance === instance2); // true

代理模式实现

上面的实现方式有一种缺点,就是万一我需要这个构造函数又能单例使用,也能正常使用,那就要写两个,而且构造函数高度重合,且这种方式实现阅读不友好,且违背了单一职责原则(创建和管理写在了一起),于是我们可以采用另一种方式实现:代理模式实现

正常创建实例的构造函数正常定义,而在需要让这个构造函数满足单例模式的时候,用代理去创建

const CreateInstance = function (name) {
  this.name = name;
  // 此处不管有多少内容,我始终只需要写一遍
};
const ProxyCreate = (function () {
  let _instance = null;
  return function (name) {
    if (_instance) return _instance;
    _instance = new CreateInstance(name);
    return _instance;
  };
})();
const instance = new ProxyCreate("x-1");
const instance2 = new ProxyCreate("x-2");
console.log(instance === instance2); // true

惰性单例

这是单例模式的重点

在使用的时候才去创建实例,且这个创建满足单例模式,即惰性单例

满足单一职责的情况下实现:

// 创建实例构造函数
const CreateDiv = function () {
  const div = document.createElement("div");
  return document.body.appendChild(div);
};
const CreateIframe = function () {
  const div = document.createElement("iframe");
  return document.body.appendChild(div);
};
// 管理实例
const getInstance = function (creator) {
  let _instance = null;
  return function () {
    return _instance || (_instance = creator.apply(this, arguments));
  };
};
// 操作实例
const createLoginLayer = getInstance(CreateDiv);
document.getElementById("button").addEventListener("click", () => {
  const loginLayer = createLoginLayer();
  loginLayer.className = "show";
});

策略模式

概述

定义一些列算法,将他们封装起来,并是他们可以互相替换

互相替换是对于静态语言而言,因为在传入不同策略的时候,这些策略的类型得保持一致,静态语言中用抽象接口实现,而 js 中仅仅只需要保证他们有同一目的即可

实现

比如现在要实现一个年终奖计算程序,对于不同的绩效等级,有着不同的计算方法,而这些方法就是一个个策略

const strategies = {
  A: salary => salary * 3,
  B: salary => salary * 2,
  C: salary => salary * 1,
};
const getBonus = (level, salary) => {
  return strategies[level](salary);
};
console.log(getBonus('A', 1000)) // 3000
console.log(getBonus('B', 2000)) // 4000

这里有多态得体现,对于 getBonus 函数而言,传入不同参数,执行不同策略,这就是多态的体现

策略模式实现小球不同缓动效果

<body>
  <div style="position: absolute; background: blue; left: 0" id="ball">
    我是 div
  </div>
  <button onclick="main('linear')" style="margin-top: 100px">linear</button>
  <button onclick="main('easeIn')" style="margin-top: 100px">easeIn</button>
</body>
<script>
  const main = (() => {
    let _instance = null;
    return type => {
      const animate =
        _instance || (_instance = new Animate(document.getElementById("ball")));
      animate.start("left", type, 0, 400, 2000);
    };
  })();
  // 算法
  var strategies = {
    linear: function (time, startPos, endPos, duration) {
      return (endPos * time) / duration + startPos;
    },
    easeIn: function (time, startPos, endPos, duration) {
      return endPos * (time /= duration) * time + startPos;
    },
  };
  const Animate = function (dom) {
    this.dom = dom;
    this.startPos = 0;
    this.endPos = 0;
    this.duration = 0;
    this.type = null; // 运动算法
    this.time = null;
    this.property = "";
  };
  // 开始
  Animate.prototype.start = function (
    property,
    type,
    startPos,
    endPos,
    duration
  ) {
    this.property = property;
    this.type = type;
    this.startPos = startPos;
    this.endPos = endPos;
    this.duration = duration;
    this.time = new Date().getTime();
    // 箭头函数 this 已绑定域外
    const timerId = setInterval(() => {
      // 如果运动结束
      if (this.time + this.duration <= new Date().getTime()) {
        clearInterval(timerId);
        this.update(this.endPos);
      }
      this.update();
    }, 19);
  };
  // 更新位置
  Animate.prototype.update = function (endPos) {
    const newPos = endPos || this.step(this.type);
    this.dom.style[this.property] = newPos + "px";
  };
  // 计算目前该在那个位置,使用策略
  Animate.prototype.step = function (type) {
    return strategies[type](
      new Date().getTime() - this.time,
      this.startPos,
      this.endPos,
      this.duration
    );
  };
</script>

在线预览:https://codepen.io/xzboss/pen/dyBzOgg

策略模式实现表单校验

我们经常使用的 el-plus 中的 form 表单的 rules 配置项就很好的体现了策略模式

比如:<font style="background-color:rgb(245, 247, 250);"> { required: true, message: 'age is required' }, { type: 'number', message: 'age must be a number' }</font>,这个数组就是这条输入框所需要验证的规则集,也就是策略。

下面进行简单实现:

<body>
  <form action="" id="form">
    <label for="username">
      用户名:
      <input type="text" id="username" />
    </label>
    <br />
    <label for="password" placeholder="密码">
      密码:
      <input type="text" id="password" />
    </label>
    <br />
    <p class="tip" style="color: red;"></p>
    <label for="submit">
      <input type="submit" value="submit" />
    </label>
  </form>
</body>
<script>
  // 验证策略
  const strategies = {
    isEmpty: (value, errMsg) => {
      if (!value) return errMsg;
    },
    minLength: (value, len = 6, errMsg) => {
      if (value.length < 6) return errMsg;
    },
    pattern: (value, reg, errMsg) => {
      if (!reg.test(value)) return errMsg;
    },
  };

  // 定义验证器
  const Validator = function () {
    this.errMsgs = []; // 验证结果集合
    this.cache = []; // 待进行的验证函数 [][]
  };
  Validator.prototype.add = function (value, rules) {
    this.cache.push([]);
    for (const rule of rules) {
      const { strategy, errMsg } = rule;
      const arg = strategy.split(":");
      this.cache[this.cache.length - 1].push(() => {
        const s = arg.shift();
        return strategies[s](value, ...arg, errMsg);
      });
    }
  };
  Validator.prototype.start = function () {
    // debugger
    this.cache.forEach(validFns => {
      for (const validFn of validFns) {
        const errMsg = validFn();
        if (errMsg) {
          this.errMsgs.push(errMsg);
          break; // 对于一个值只要有一条规则不满足,就退出这个值的剩余验证规则,因为此值的提示最多显示一条
        }
      }
    });
  };

  // 提交进行验证
  document.getElementById("form").onsubmit = () => {
    const username = document.getElementById("username");
    const password = document.getElementById("password");
    const tip = document.getElementsByClassName("tip")[0];
    const validator = new Validator();
    validator.add(username.value, [
      { strategy: "isEmpty", errMsg: "用户名不能为空" },
    ]);
    validator.add(password.value, [
      { strategy: "isEmpty", errMsg: "密码不能为空" },
      { strategy: "minLength:6", errMsg: "密码不能少于6位" },
    ]);
    validator.start();
    if (validator.errMsgs.length) {
      tip.textContent = validator.errMsgs.join(",");
      return false;
    }
    alert('通过验证')
  };
</script>

在线预览:https://codepen.io/xzboss/pen/RwzZoOm

代理模式

概述

访问真实对象改为访问代理对象,由代理对象去访问真实对象

代理对象和真实对象对外必须保持一致

代理分两种模式

  1. 保护代理:用于鉴权是否有权限访问真实对象
  2. 虚拟代理:当需要用到资源的时候才去创建,而不是直接用真实对象创建(下面都是虚拟代理例子)

虚拟代理模式实现图片预加载

<style>
  img {
    width: 50px;
    height: 50px;
  }
</style>
<body></body>
<script>
  // 创建图片并添加到 dom
  const myImage = (function () {
    const imgNode = document.createElement("img");
    document.body.appendChild(imgNode);
    return {
      setSrc: src => {
        imgNode.src = src;
      },
    };
  })();

  // 代理对象,负责预加载
  const proxyImage = (function () {
    const img = new Image();
    img.onload = () => {
      myImage.setSrc(img.src);
    };
    return {
      setSrc: src => {
        console.log(111);
        myImage.setSrc("./loading.gif");
        img.src = src;
      },
    };
  })();
  proxyImage.setSrc(
    "https://ts1.cn.mm.bing.net/th?id=OIP-C.aBZtnPTfuqMAFSpyZg0-vQHaGH"
  );
</script>

在线预览:https://codepen.io/xzboss/pen/gONxgNm

虚拟代理模式实现合并多请求

<body>
  <input type="checkbox" id="1" />
  <input type="checkbox" id="2" />
  <input type="checkbox" id="3" />
  <input type="checkbox" id="4" />
  <input type="checkbox" id="5" />
  <input type="checkbox" id="6" />
  <input type="checkbox" id="7" />
  <input type="checkbox" id="8" />
  <script>
    // 正常请求
    const synchronousFile = id => {
      console.log("同步请求-", id, "中");
    };

    // 代理请求
    const proxyFile = (() => {
      const cache = new Set(); // 记录需要同步的 id
      let timer = null;
      return id => {
        if (cache.has(id)) {
          cache.delete(id);
        } else {
          cache.add(id);
        }
        if (timer) return;
        // 节流控制每两秒最多触发一次同步
        timer = setTimeout(() => {
          for (const id of cache) {
            synchronousFile(id);
          }
          clearTimeout(timer);
          timer = null;
        }, 2000);
      };
    })();

    // 绑定点击事件
    Array.prototype.forEach.call(
      document.getElementsByTagName("input"),
      element => {
        element.onclick = () => proxyFile(element.id);
      }
    );
  </script>
</body>

虚拟代理实现微打印台

假设我们自定义了一个打印台,这个打印台我们要在按了 f2 后才真实渲染在的页面上;但是在没按 f2 之前,我们需要提前决定要打印哪些数据

这里第一反应是用一个缓存列表,缓存打印结果,然后在 miniConsole 中进行缓存,在加载完成后,读出缓存;显然这样的方式是错误的,因为需要在 miniConsole 中做处理,违反单一职责,而且如果我不需要看打印,这个miniConsole 任然进行了加载并执行了打印,费时费存储。

正确思路,当遇到页面中需要打印的时候,交给代理进行存储,存储需要打印数据,等用户真实用 F2 打开打印弹窗时,再去加载 js 并执行打印。

即惰性加载

  1. HTML 代码
<head></head>

<body>
  <script>
    // 定义代理对象
    var miniConsole = (function () {
      const cache = []; // 缓存待执行打印函数
      document.addEventListener("keyup", e => {
        if (e.key === "F2") {
          // 加载真 miniConsole
          const scriptNode = document.createElement("script");
          document.head.appendChild(scriptNode);
          scriptNode.src = "./miniConsole.js";
          // 加载后执行堆积打印函数,此时 miniConsole 已被替换
          scriptNode.onload = () => {
            cache.forEach(fn => {
              fn();
            });
          };
        }
      });
      return {
        log (value) {
          cache.push(() => {
            return miniConsole.log(value);
          });
        },
      };
    })();

    for (let i = 0; i < 10; i++) {
      miniConsole.log("message" + i);
    }
  </script>
</body>
  1. js 库
var miniConsole = (function () {
  let modal = null;
  const createModal = function () {
    const divNode = document.createElement("div");
    divNode.style.cssText =
      "width:500px;height:500px;position:absolute;right:0;top:0;background:red;";
    document.body.appendChild(divNode);
    modal = divNode;
  };
  return {
    log: value => {
      if (!modal) createModal();
      modal.insertAdjacentHTML('beforeend', value + '<br />');
      // modal.style.display = "none";
    },
  };
})();

在线预览:https://codepen.io/xzboss/pen/jOjGjVr

迭代器模式

内部迭代器

迭代规则在内部已经决定了,无法在外部影响。比如 forEach,内部已经决定了顺序,依次的迭代,无法终止(当然可以通过抛出错误来终止,)

外部迭代器

外部需要显式的调用 next 方法来进行迭代,更加灵活,可以终止可以关闭

发布订阅模式(观察者模式)

由发布者、订阅者、事件中心组成

功能组成: 发布,订阅,取消订阅,延迟订阅,命名空间

命令模式