前端js用得上的设计模式

2,365 阅读14分钟

以下内容来源于《JavaScript设计模式与开发实践》

经典的书籍,值得反复去学习和品味,每一次看都能得到新的认识,因为我们一直在进步,观察问题的视角也一直在上升。

单例模式

单例模式的核心是确保只有一个实例,并提供全局访问。

var obj;
if (!obj) {
  obj = xxx;
}

惰性单例指的是在需要的时候才创建对象实例。惰性单例是单例模式的重点,这种技术在实 际开发中非常有用,有用的程度可能超出了我们的想象

我们把如何管理单例的逻辑从业务的代码中抽离出来,这些逻辑被封装在 getSingle 函数内部,创建对象的方法 fn 被当成参数动态传入 getSingle 函数

var getSingle = function (fn) {
  var result;
  return function () {
    return result || (result = fn.apply(this, arguments));
  };
};
var createLoginLayer = function () {
  var div = document.createElement("div");
  div.innerHTML = "我是登录浮窗";
  div.style.display = "none";
  document.body.appendChild(div);
  return div;
};
var createSingleLoginLayer = getSingle(createLoginLayer);

总结:单例模式是一种简单但非常实用的模式,特别是惰性单例技术,在合适的时候才创建对象,并且只创建唯一的一个。更奇妙的是,创建对象和管理单例的职责被分布在两个不同的方法中,这两个方法组合起来才具有单例模式的威力。

策略模式

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

奖金计算

// before
var calculateBonus = function (performanceLevel, salary) {
  if (performanceLevel === "S") {
    return salary * 4;
  }
  if (performanceLevel === "A") {
    return salary * 3;
  }
  if (performanceLevel === "B") {
    return salary * 2;
  }
};
calculateBonus("B", 20000); // 输出:40000
calculateBonus("S", 6000); // 输出:24000

// after
var strategies = {
  S: function (salary) {
    return salary * 4;
  },
  A: function (salary) {
    return salary * 3;
  },
  B: function (salary) {
    return salary * 2;
  },
};
var calculateBonus = function (level, salary) {
  return strategies[level](salary);
};
console.log(calculateBonus("S", 20000)); // 输出:80000
console.log(calculateBonus("A", 10000)); // 输出:30000

表单校验

<html>
<body>
  <form action="http:// xxx.com/register" id="registerForm" method="post">
    请输入用户名:<input type="text" name="userName" />
    请输入密码:<input type="text" name="password" />
    请输入手机号码:<input type="text" name="phoneNumber" />
    <button>提交</button>
  </form>
  <script>
    /***********************策略对象**************************/
    var strategies = {
      isNonEmpty: function (value, errorMsg) {
        if (value === '') {
          return errorMsg;
        }
      },
      minLength: function (value, length, errorMsg) {
        if (value.length < length) {
          return errorMsg;
        }
      },
      isMobile: function (value, errorMsg) {
        if (!/(^1[3|5|8][0-9]{9}$)/.test(value)) {
          return errorMsg;
        }
      }
    };
    /***********************Validator 类**************************/
    var Validator = function () {
      this.cache = [];
    };
    Validator.prototype.add = function (dom, rules) {
      var self = this;
      for (var i = 0, rule; rule = rules[i++];) {
        (function (rule) {
          var strategyAry = rule.strategy.split(':');
          var errorMsg = rule.errorMsg;
          self.cache.push(function () {
            var strategy = strategyAry.shift();
            strategyAry.unshift(dom.value);
            strategyAry.push(errorMsg);
            return strategies[strategy].apply(dom, strategyAry);
          });
        })(rule)
      }
    };
    Validator.prototype.start = function () {
      for (var i = 0, validatorFunc; validatorFunc = this.cache[i++];) {
        var errorMsg = validatorFunc();
        if (errorMsg) {
          return errorMsg;
        }
      }
    };
    /***********************客户调用代码**************************/
    var registerForm = document.getElementById('registerForm');
    var validataFunc = function () {
      var validator = new Validator();
      validator.add(registerForm.userName, [{
        strategy: 'isNonEmpty',
        errorMsg: '用户名不能为空'
      }, {
        strategy: 'minLength:10',
        errorMsg: '用户名长度不能小于10 位'
      }]);
      validator.add(registerForm.password, [{
        strategy: 'minLength:6',
        errorMsg: '密码长度不能小于6 位'
      }]);
      validator.add(registerForm.phoneNumber, [{
        strategy: 'isMobile',
        errorMsg: '手机号码格式不正确'
      }]);
      var errorMsg = validator.start();
      return errorMsg;
    }
    registerForm.onsubmit = function () {
      var errorMsg = validataFunc();
      if (errorMsg) {
        alert(errorMsg);
        return false;
      }
    };
  </script>
</body>
</html>

优缺点

  1. 策略模式利用组合、委托和多态等技术和思想,可以有效地避免多重条件选择语句。
  2. 策略模式提供了对开放—封闭原则的完美支持,将算法封装在独立的strategy 中,使得它们易于切换,易于理解,易于扩展。
  3. 策略模式中的算法也可以复用在系统的其他地方,从而避免许多重复的复制粘贴工作。
  4. 首先,使用策略模式会在程序中增加许多策略类或者策略对象,但实际上这比把它们负责的逻辑堆砌在Context 中要好。

缺: 要使用策略模式,必须了解所有的strategy,必须了解各个strategy 之间的不同点, 这样才能选择一个合适的strategy。比如,我们要选择一种合适的旅游出行路线,必须先了解选 择飞机、火车、自行车等方案的细节。此时strategy 要向客户暴露它的所有实现,这是违反最少 知识原则的。

代理模式

代理模式是为一个对象提供一个代用品或占位符,以便控制对它的访问。

代理模式是一种非常有意义的模式,在生活中可以找到很多代理模式的场景。比如,明星都有经纪人作为代理。如果想请明星来办一场商业演出,只能联系他的经纪人。经纪人会把商业演出的细节和报酬都谈好之后,再把合同交给明星签。

代理模式的关键是,当客户不方便直接访问一个对象或者不满足需要的时候,提供一个替身对象来控制对这个对象的访问,客户实际上访问的是替身对象。替身对象对请求做出一些处理之后,再把请求转交给本体对象

图片预加载

var myImage = (function () {
  var imgNode = document.createElement("img");
  document.body.appendChild(imgNode);
  return {
    setSrc: function (src) {
      imgNode.src = src;
    },
  };
})();
var proxyImage = (function () {
  var img = new Image();
  img.onload = function () {
    myImage.setSrc(this.src);
  };
  return {
    setSrc: function (src) {
      myImage.setSrc("file:// /C:/Users/svenzeng/Desktop/loading.gif");
      img.src = src;
    },
  };
})();
proxyImage.setSrc("http:// imgcache.qq.com/music/photo/k/000GGDys0yA0Nk.jpg");

单一职责原则指的是,就一个类(通常也包括对象和函数等)而言,应该仅有一个引起它变化的原因。如果一个对象承担了多项职责,就意味着这个对象将变得巨大,引起它变化的原因可能会有多个。面向对象设计鼓励将行为分布到细粒度的对象之中,如果一个对象承担的职责过多,等于把这些职责耦合到了一起,这种耦合会导致脆弱和低内聚的设计。当变化发生时,设计可能会遭到意外的破坏。

给img 节点设置src 和图片预加载这两个功能,被隔离在两个对象里,它们可以自变化而不影响对方。何况就算有一天我们不再需要预加载,那么只需要改成请求本体而不是请求代理对象即可。

发布—订阅模式

发布—订阅模式又叫观察者模式,它定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知。在JavaScript 开发中,我们一般用事件模型来替代传统的发布—订阅模式。

全局的发布订阅对象

实际使用场景如: 消息模块的未读通知
一般我们PC官网都有个消息通知模块,有这么个需求,当从消息列表中 进入详情,layout层header的消息通知icon的右上角的小红点需要重新调用接口获取未读消息数。这时候就可以在layout层的header模块注册监听(listen)在回调里执行函数调用,在消息详情接口触发后trigger

const globalEvent = (function () {
  const listener = {};
  const listen = (key, fn) => {
    if (!listener[key]) {
      listener[key] = [];
    }
    listener[key].push(fn);
  }
  const remove = (key, fn) => {
    const fns = listener[key];
    if (!fns) {
      // 如果 key 对应的消息没有被人订阅,则直接返回
      return;
    }
    if (!fn) {
      fns = []
      return;
    }
    for (let i = fns.length - 1; i >= 0; i--) {
      // 反向遍历订阅的回调函数列表
      const _fn = fns[ i ];
      if (_fn === fn) {
        fns.splice(i, 1);
      }
    }
  }
  const trigger =  (key, val) => {
    const fns = listener[key];
    if (!fns || fns.length === 0) {
      return;
    }
    for (let i = 0, fn; (fn = fns[i++]); ) {
      fn(val);
    }
  }
  return {
    listen,
    remove,
    trigger,
  }
}());

globalEvent.listen('squareMeter88', (fn1 = function (price) {
    console.log('价格= ' + price);
  })
);
globalEvent.listen('squareMeter88', (fn2 = function (price) {
    console.log('价格= ' + price);
  })
);
globalEvent.remove('squareMeter88', fn1); // 删除订阅
globalEvent.trigger('squareMeter88', 2000000); // 输出:2000000

先订阅后发布,并且加了命名空间的处理

const eventGlobal = (function () {
  const event = (function () {
    const namespaceMap = {};
    const defaultName = "__default__";
    const _listen = (key, cache, fn) => {
      if (!cache[key]) {
        cache[key] = [];
      }
      cache[key].push(fn);
    };
    const _remove = (key, cache, fn) => {
      const fns = cache[key];
      // 没有已监听函数
      if (!fns || !fns.length) {
        return;
      }
      if (!fn) {
        cache[key] = [];
        return;
      }
      for (let i = fns.length - 1; i >= 0; i--) {
        const _fn = fns[i];
        if (_fn === fn) {
          fns.splice(i, 1);
        }
      }
    };
    const _trigger = (key, cache, val) => {
      const fns = cache[key];
      each(fns, function () {
        this.call(null, val);
      });
    };
    const each = (fns, fn) => {
      fns.map((_fn) => fn.call(_fn));
    };
    const _create = (name) => {
      const nameSpace = name || defaultName;
      let offlineStack = [];
      const cache = {};
      const ret = {
        listen: (key, fn) => {
          _listen(key, cache, fn);
          if (!offLineStack) {
            return;
          }
          each(offLineStack, function () {
            this();
          });
          offlineStack = false;
        },
        remove: (key, fn) => _remove(key, cache, fn),
        trigger: (key, val) => {
          const fn = () => _trigger(key, cache, val);
          if (!offLineStack) {
            return fn();
          }
          offLineStack.push(fn);
        },
      };
      return namespaceMap[nameSpace] || (namespaceMap[nameSpace] = ret);
    };
    return {
      create: _create,
      listen: (key, fn) => {
        const event = _create();
        event.listen(key, fn);
      },
      remove: (key, fn) => {
        const event = _create();
        event.remove(key, fn);
      },
      trigger: (key, fn) => {
        const event = _create();
        event.trigger(key, fn);
      },
    };
  })();
  return event;
})();

/************** 先发布后订阅 ********************/
eventGlobal.trigger("click", 1);
eventGlobal.trigger("click", 12);
eventGlobal.listen("click", function (a) {
  console.log(a); // 输出:1
});

/************** 使用命名空间 ********************/
eventGlobal.create("namespace1").trigger("click", 8);
eventGlobal.create("namespace1").listen(
  "click",
  (fn1 = function (a) {
    console.log(a); // 输出:1
  })
);

eventGlobal.create("namespace1").remove("click", fn1);
eventGlobal.create("namespace1").trigger("click", 10);

发布—订阅模式在实际开发中非常有用。发布—订阅模式的优点非常明显,一为时间上的解耦,二为对象之间的解耦。它的应用非常 广泛,既可以用在异步编程中,也可以帮助我们完成更松耦合的代码编写。发布—订阅模式还可 以用来帮助实现一些别的设计模式,比如中介者模式。 从架构上来看,无论是MVC 还是MVVM, 都少不了发布—订阅模式的参与,而且JavaScript 本身也是一门基于事件驱动的语言。 当然,发布—订阅模式也不是完全没有缺点。创建订阅者本身要消耗一定的时间和内存,而 且当你订阅一个消息后,也许此消息最后都未发生,但这个订阅者会始终存在于内存中。另外, 发布—订阅模式虽然可以弱化对象之间的联系,但如果过度使用的话,对象和对象之间的必要联 系也将被深埋在背后,会导致程序难以跟踪维护和理解。特别是有多个发布者和订阅者嵌套到一 起的时候,要跟踪一个bug 不是件轻松的事情。

职责链模式

职责链模式的定义是:使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系,将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止。

var order500 = function (orderType, pay, stock) {
  if (orderType === 1 && pay === true) {
    console.log("500 元定金预购,得到100 优惠券");
  } else {
    return "nextSuccessor"; // 我不知道下一个节点是谁,反正把请求往后面传递
  }
};
var order200 = function (orderType, pay, stock) {
  if (orderType === 2 && pay === true) {
    console.log("200 元定金预购,得到50 优惠券");
  } else {
    return "nextSuccessor"; // 我不知道下一个节点是谁,反正把请求往后面传递
  }
};
var orderNormal = function (orderType, pay, stock) {
  if (stock > 0) {
    console.log("普通购买,无优惠券");
  } else {
    console.log("手机库存不足");
  }
};
// Chain.prototype.setNextSuccessor 指定在链中的下一个节点
// Chain.prototype.passRequest 传递请求给某个节点
var Chain = function (fn) {
  this.fn = fn;
  this.successor = null;
};
Chain.prototype.setNextSuccessor = function (successor) {
  return (this.successor = successor);
};
Chain.prototype.passRequest = function () {
  var ret = this.fn.apply(this, arguments);
  if (ret === "nextSuccessor") {
    return (
      this.successor &&
      this.successor.passRequest.apply(this.successor, arguments)
    );
  }
  return ret;
};

// 现在我们把3 个订单函数分别包装成职责链的节点:
var chainOrder500 = new Chain(order500);
var chainOrder200 = new Chain(order200);
var chainOrderNormal = new Chain(orderNormal);
// 然后指定节点在职责链中的顺序:
chainOrder500.setNextSuccessor(chainOrder200);
chainOrder200.setNextSuccessor(chainOrderNormal);
// 最后把请求传递给第一个节点:
chainOrder500.passRequest(1, true, 500); // 输出:500 元定金预购,得到100 优惠券
chainOrder500.passRequest(2, true, 500); // 输出:200 元定金预购,得到50 优惠券
chainOrder500.passRequest(3, true, 500); // 输出:普通购买,无优惠券
chainOrder500.passRequest(1, false, 0); // 输出:手机库存不足
/* 通过改进,我们可以自由灵活地增加、移除和修改链中的节点顺序,假如某天网站运营人员
又想出了支持300 元定金购买,那我们就在该链中增加一个节点即可:
*/
var order300 = function () {
  // 具体实现略
};
chainOrder300 = new Chain(order300);
chainOrder500.setNextSuccessor(chainOrder300);
chainOrder300.setNextSuccessor(chainOrder200);

小结:在JavaScript 开发中,职责链模式是最容易被忽视的模式之一。实际上只要运用得当,职责链模式可以很好地帮助我们管理代码,降低发起请求的对象和处理请求的对象之间的耦合性。职责链中的节点数量和顺序是可以自由变化的,我们可以在运行时决定链中包含哪些节点。

中介者模式

中介者模式的作用就是解除对象与对象之间的紧耦合关系。增加一个中介者对象后,所有的相关对象都通过中介者对象来通信,而不是互相引用,所以当一个对象发生改变时,只需要通知中介者对象即可。中介者使各对象之间耦合松散,而且可以独立地改变它们之间的交互。中介者模式使网状的多对多关系变成了相对简单的一对多关系

和发布—订阅模式的区别:

发布—订阅模式模式:只能从从一的一方循环的通知,属于单向。 中介者模式:可以从任一方循环通知,属于双向。

泡泡堂游戏

function Player(name, teamColor) {
  this.name = name; // 角色名字
  this.teamColor = teamColor; // 队伍颜色
  this.state = "alive"; // 玩家生存状态
}
Player.prototype.win = function () {
  console.log(this.name + " won ");
};
Player.prototype.lose = function () {
  console.log(this.name + " lost");
};
/*******************玩家死亡*****************/
Player.prototype.die = function () {
  this.state = "dead";
  playerDirector.reciveMessage("playerDead", this); // 给中介者发送消息,玩家死亡
};
/*******************移除玩家*****************/
Player.prototype.remove = function () {
  playerDirector.reciveMessage("removePlayer", this); // 给中介者发送消息,移除一个玩家
};
/*******************玩家换队*****************/
Player.prototype.changeTeam = function (color) {
  playerDirector.reciveMessage("changeTeam", this, color); // 给中介者发送消息,玩家换队
};
var playerFactory = function (name, teamColor) {
  var newPlayer = new Player(name, teamColor); // 创造一个新的玩家对象
  playerDirector.reciveMessage("addPlayer", newPlayer); // 给中介者发送消息,新增玩家
  return newPlayer;
};
var playerDirector = (function () {
  var players = {}, // 保存所有玩家
    operations = {}; // 中介者可以执行的操作
  /****************新增一个玩家***************************/
  operations.addPlayer = function (player) {
    var teamColor = player.teamColor; // 玩家的队伍颜色
    players[teamColor] = players[teamColor] || []; // 如果该颜色的玩家还没有成立队伍,则新成立一个队伍;
    players[teamColor].push(player); // 添加玩家进队伍
  };
  /****************移除一个玩家***************************/
  operations.removePlayer = function (player) {
    var teamColor = player.teamColor, // 玩家的队伍颜色
      teamPlayers = players[teamColor] || []; // 该队伍所有成员
    for (var i = teamPlayers.length - 1; i >= 0; i--) {
      // 遍历删除
      if (teamPlayers[i] === player) {
        teamPlayers.splice(i, 1);
      }
    }
  };
  /****************玩家换队***************************/
  operations.changeTeam = function (player, newTeamColor) {
    // 玩家换队
    operations.removePlayer(player); // 从原队伍中删除
    player.teamColor = newTeamColor; // 改变队伍颜色
    operations.addPlayer(player); // 增加到新队伍中
  };
  operations.playerDead = function (player) {
    // 玩家死亡
    var teamColor = player.teamColor,
      teamPlayers = players[teamColor]; // 玩家所在队伍
    var all_dead = true;
    for (var i = 0, player; (player = teamPlayers[i++]); ) {
      if (player.state !== "dead") {
        all_dead = false;
        break;
      }
    }
    if (all_dead === true) {
      // 全部死亡
      for (var i = 0, player; (player = teamPlayers[i++]); ) {
        player.lose(); // 本队所有玩家lose
      }
      for (var color in players) {
        if (color !== teamColor) {
          var teamPlayers = players[color]; // 其他队伍的玩家
          for (var i = 0, player; (player = teamPlayers[i++]); ) {
            player.win(); // 其他队伍所有玩家win
          }
        }
      }
    }
  };
  var reciveMessage = function () {
    var message = Array.prototype.shift.call(arguments); // arguments 的第一个参数为消息名称
    operations[message].apply(this, arguments);
  };
  return {
    reciveMessage: reciveMessage,
  };
})();
// 红队:
var player1 = playerFactory("皮蛋", "red"),
  player2 = playerFactory("小乖", "red"),
  player3 = playerFactory("宝宝", "red"),
  player4 = playerFactory("小强", "red");
// 蓝队:
var player5 = playerFactory("黑妞", "blue"),
  player6 = playerFactory("葱头", "blue"),
  player7 = playerFactory("胖墩", "blue"),
  player8 = playerFactory("海盗", "blue");
player1.die();
player2.die();
player3.die();
player4.die();

小结:中介者模式是迎合迪米特法则的一种实现。迪米特法则也叫最少知识原则,是指一个对象应该尽可能少地了解另外的对象(类似不和陌生人说话)。如果对象之间的耦合性太高,一个对象发生改变之后,难免会影响到其他的对象,跟“城门失火,殃及池鱼”的道理是一样的。而在中介者模式里,对象之间几乎不知道彼此的存在,它们只能通过中介者对象来互相影响对方。

中介者模式可以非常方便地对模块或者对象进行解耦,但对象之间并非一定需要解耦。在实际项目中,模块或对象之间有一些依赖关系是很正常的。毕竟我们写程序是为了快速完成项目交付生产,而不是堆砌模式和过度设计。关键就在于如何去衡量对象之间的耦合程度。一般来说,如果对象之间的复杂耦合确实导致调用和维护出现了困难,而且这些耦合度随项目的变化呈指数增长曲线,那我们就可以考虑用中介者模式来重构代码。

装饰者模式

装饰者模式能够在不改变对象自身的基础上,在程序运行期间给对象 动态地添加职责。跟继承相比,装饰者是一种更轻便灵活的做法,这是一种“即用即付”的方式,比如天冷了就多穿一件外套,需要飞行时就在头上插一支竹蜻蜓,遇到一堆食尸鬼时就点开AOE(范围攻击)技能。

数据统计上报

页面中有一个登录button,点击这个button 会弹出登录浮层,与此同时要进行数据上报,来统计有多少用户点击了这个登录button

<html>
  <button tag="login" id="button">点击打开登录浮层</button>
  <script>
    Function.prototype.after = function (afterfn) {
      var __self = this;
      return function () {
        var ret = __self.apply(this, arguments);
        afterfn.apply(this, arguments);
        return ret;
      };
    };
    var showLogin = function () {
      console.log("打开登录浮层");
    };
    var log = function () {
      console.log("上报标签为: " + this.getAttribute("tag"));
    };
    showLogin = showLogin.after(log); // 打开登录浮层之后上报数据
    document.getElementById("button").onclick = showLogin;
  </script>
</html>

与代理模式的区别

代理模式和装饰者模式最重要的区别在于它们的意图和设计目的。代理模式的目的是,当直接访问本体不方便或者不符合需要时,为这个本体提供一个替代者。本体定义了关键功能,而代理提供或拒绝对它的访问,或者在访问本体之前做一些额外的事情。装饰者模式的作用就是为对象动态加入行为。换句话说,代理模式强调一种关系(Proxy 与它的实体之间的关系),这种关系可以静态的表达,也就是说,这种关系在一开始就可以被确定。而装饰者模式用于一开始不能确定对象的全部功能时。代理模式通常只有一层代理本体的引用,而装饰者模式经常会形成一条长长的装饰链。

总结

学以致用,扩充思维的广度和深度。 理想很美好,现实很残酷,这句话只有在亲身经历了项目重构才能深得体会,项目的历史债务,终究有一代人要还的。