JavaScript中的14种设计模式

字数 5113阅读 319

本文源于本人关于《JavaScript设计模式与开发实践》(曾探著)的阅读总结。想详细了解具体内容建议阅读该书。

1. 策略模式:

定义:定义一系列的算法,把他们一个个封装起来,并且使他们可以相互替换。

前端中的利用:表单验证(不同的表单有不同的验证方式)

一个简单的例子:公司发奖金根据每个人的绩效不同来发不同的奖金,不同的绩效,奖金有不同的计算方式。 我们可以用if-else,判断每个人的绩效是什么,从而采用不同的计算方式。但是如果又增加了一个种绩效水平,那么我们又得增加if-else分支,这明显是违反开放-封闭原则的。

核心思想:创建一个策略组,每次有新的绩效计算方法则直接加入该组里,不会变动其他代码。 调用时,传入绩效字符串,从而采用调用属性的方法访问到正确策略,并调用该策略。

利用策略模式构建奖金发放:

var strategies = {
  "S": function(salary) {
    return salary * 4;
  },
  "A": function(salary) {
    return salary * 3;
  },
  "B": function(salary) {
    return salary * 2;
  }
}

function calculateBonus(level, salary) {
  return strategies[level](salary);
}

console.log(calculateBonus('A', 13333));

2. 代理模式:

定义:提供一个代用品或占位符,以便控制对它的访问。

前端中的利用:图片预加载(loading图片)、缓存代理

核心思想:对象A访问对象B,创建一个对象C,控制对象A对对象B的访问,从而达到某种目的。 或者A进行某个行为,创建一个对象C控制A进行的这个行为。

图片预加载:

var myImage = (function () {
      var imgNode = document.createElement('img');
      document.body.appendChild(imgNode);

      return {
        setSrc: function (src) {
          imgNode.src = src;
        }
      }
    })()

它返回了一个对象,拥有普通的图片加载功能,但是这个功能有一个弊端,网络环境差,图片迟迟没有完全加载完成时,会产生一个白框,我们希望这个时候有一个loading的动画。

var proxyImage = (function () {
      var img = new Image();
      img.onload = function () {
        myImage.setSrc(this.src);
      }
      return {
        setSrc: function (src) {
          myImage.setSrc('./屏幕快照 2017-09-19 上午10.15.58.png');
          img.src = src;
        }
      }
    })()

现在创建了一个代理,我们想要加载图片时,并不直接调用图片加载对象,而是调用这个代理函数,达到有loading动画的目的。

它先把imgNode设置为loading动画的gif图片,然后创建了一个Image对象,等传入的真实图片链接,图片加载完成后,再用真实图片替换掉loading动画gif。

当你已经写完了某个函数,但是某时希望这个函数的行为有其他效果时,你就可以写一个代理达到你的目的。

3. 迭代器模式:

定义:提供一种方法顺序访问一个聚合对象中的各个元素。

前端中的利用:循环

很多语言都内置了迭代器,我们很多时候不认为他是一种设计模式。
这里我们说一下外部迭代器:
  • 必须显式地请求迭代下一个元素。
  • 增加了一些调用的复杂性,但是更为灵活,我们可以手工控制迭代过程和顺序。
var Iterator = function(obj) {
  var current = 0;

  var next = function() {
    current += 1;
  };

  var isDone = function() {
    return current >= obj.length;
  };

  var getCurrItem = function() {
    return obj[current];
  };

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

var compare = function(iterator1, iterator2) {
  if(iterator1.length!==iterator2.length) {
    console.log('不相等');
  }
  while(!iterator1.isDone() && !iterator2.isDone()){
    if(iterator1.getCurrItem() !== iterator2.getCurrItem()){
      console.log('不相等');
    }
    iterator1.next();
    iterator2.next();
  }
  console.log('相等');
}

compare(Iterator([1, 2, 3]), Iterator([1, 2, 3])); // 相等

4. 命令模式

定义:指的是一个执行某些特定事情的指令。
使用场景:有时候需要向某些对象发送请求,但是并不知道请求的接收者是谁,也不知道被请求的操作是什么。

前端中的利用:菜单程序,按键动画

背景:前端协作中,有人负责写界面,有人负责开发按钮之类的具体功能。我们希望写界面的人直接调用命令就好,不用关心,具体实现。

按键动画(每个按键代表不同的动画):

命令创建函数:

var makeCommand = function (receiver, state) {
      return function () {
        receiver[state]();
      }
    }

receiver代表具体动画的执行函数。

界面同学只负责:

document.onkeypress = function (ev) {
      var keyCode = ev.keyCode,
        command = makeCommand(Ryu, commands[keyCode]);

      if (command) {
        command();
      }
    };

而实现操作的同学写具体实现,和不同按键所对应的指令名称:

var Ryu = {
      attack: function () {
        console.log('攻击');
      },
      defense: function () {
        console.log('防御');
      },
      jump: function () {
        console.log('跳跃');
      },
      crouch: function () {
        console.log('下蹲');
      }
    };
    
var commands = {
      '119': 'jump', // W
      '115': 'crouch', // S
      '97': 'defense', // A 
      '100': 'attack' // D
    }
目前我们的命令模式,只有一个设置命令,但是这其实完全可以写成一个对象,包含,记录命令调用过程,包含取消命令,等等。

5. 组合模式:

定义:将对象组合成树形结果,以表示“部分-整体”的层次结果。除了用来表示树形结构之外,组合模式令一个好处是通过对象的多态性表现,使得用户对单个对象和组合对象的使用具有一致性。

前端中的利用:文件夹扫描

核心思想:树形结构,分为叶子对象和非叶子对象, 叶子对象和非叶子对象拥有一组同样的方法属性, 调用非叶子对象的方法后,该对象和该对象下的所有对象都会执行该方法。

文件扫描:当我们负责粘贴时,我们不会关心我们选中的是文件还是文件夹,我们都会一并进行负责粘贴。

文件夹:

var Folder =  function(name) {
  this.name = name;
  this.files = [];
};

Folder.prototype.add = function(file) {
  this.files.push(file);
}

Folder.prototype.scan = function() {
  console.log('开始扫描文件夹:' + this.name);
  for(var i = 0, file; file = this.files[i++];) {
    file.scan();
  }
}

文件:

var File = function(name){
  this.name = name;
}

File.prototype.add = function() {
  throw new Error('文件下面不能再添加文件');
}

File.prototype.scan = function() {
  console.log('开始扫描文件:' + this.name);
}

组成文件结构:

var folder = new Folder('学习资料');
var folder1 = new Folder('JS');
var folder2 = new Folder('JQ');

var file = new File('学习资料');
var file1 = new File('学习资料1');
var file2 = new File('学习资料2');
var file3 = new File('学习资料3');

folder.add(file);
folder.add(file1);

folder1.add(file2);
folder2.add(file3);

var rootFolder = new Folder('root');

rootFolder.add(folder);
rootFolder.add(folder1);
rootFolder.add(folder2);

扫描:

rootFolder.scan();

// 输出:
// 开始扫描文件夹:root

// 开始扫描文件夹:学习资料
// 开始扫描文件:学习资料
// 开始扫描文件:学习资料1

// 开始扫描文件夹:JS
// 开始扫描文件:学习资料2

// 开始扫描文件夹:JQ
// 开始扫描文件:学习资料3

6. 模版方法模式

定义:由两部分结构组成,第一部分就是抽象父类,第二部分就是具体实现的子类。通常父类中封装了子类的算法框架,包括实现一些公共方法及封装子类中所有方法的执行顺序。

使用场景:假如我们有一些平行的子类,各个子类之间有一些相同的行为,也有一些不同的行为。如果相同和不同的行为都混合在各个子类的实现中,说明这些相同的行为会在各个子类中重复出现。

模版方法模式所做的事情:我们不必重写一个子类,如果属于同一类型就可以直接继承抽象类,然后把变化的逻辑封装到子类中即可,不需要改动其他子类和父类。

例子:

  • 泡咖啡:
    • 把水煮沸
    • 把沸水冲泡咖啡
    • 把咖啡倒进杯子
    • 加糖和牛奶
  • 泡茶:
    • 把水煮沸
    • 用沸水浸泡茶叶
    • 把水倒进杯子里
    • 加柠檬

然后进行抽象:

  • 把水煮沸
  • 用沸水冲泡饮料
  • 把饮料倒进杯子里
  • 加调料

抽象类代码:

var Beverage = function() {};

Beverage.prototype.boilWater = function(){
  console.log('把水煮沸');
};

// 空方法,应该由子类来重写
Beverage.prototype.brew = function() {
  throw new Error('子类必须重写brew方法');
};

// 空方法,应该由子类来重写
Beverage.prototype.pourInCup = function() {
  throw new Error('子类必须重写pourInCup方法');
};

// 空方法,应该由子类来重写
Beverage.prototype.addCondiments = function() {
  throw new Error('子类必须重写addCondiments方法');
};

Beverage.prototype.init = function() {
  this.boilWater();
  this.brew();
  this.pourInCup();
  this.addCondiments();
};

因为JS没有继承机制,但是子类如果继承了父类没有重写方法,编辑器不会提醒,那么执行的时候会报错,为了防止程序员漏重写方法,故在需要重写的方法中抛出异常。

coffee:

var Coffee = function() {};

Coffee.prototype = new Beverage();

Coffee.prototype.brew = function() {
  console.log('用水冲泡咖啡');
};

Coffee.prototype.pourInCup = function() {
  console.log('把咖啡倒进杯子里');
};

Coffee.prototype.addCondiments = function() {
  console.log('加糖和牛奶');
};

var coffee = new Coffee();
coffee.init();

tea:

var Tea = function() {};

Tea.prototype = new Beverage();

Tea.prototype.brew = function() {
  console.log('用水浸泡茶');
};

Tea.prototype.pourInCup = function() {
  console.log('把茶水倒进杯子里');
};

Tea.prototype.addCondiments = function() {
  console.log('加柠檬');
};

var tea = new Tea();
tea.init();

# 7. 单例模式

定义:保证一个类仅有一个实例,并提供一个访问它的全局访问点。

前端中的利用:登录框,弹层

核心思想:利用一个变量保存第一次创建的结果(对象中的某个属性或者闭包能访问的变量), 再次创建时,该变量不为空,直接返回改对象。

类:

var Singleton = function(name) {
  this.name = name;
  this.instance = null;
}

Singleton.prototype.getName = function() {
  console.log(this.name);
}

Singleton.getInstance = function(name) {
  if(!this.instance) {
    this.instance = new Singleton(name);
  }
  return this.instance;
}

var a = Singleton.getInstance('123');
var b = Singleton.getInstance('321');
console.log(a === b); // true

Singleton.getInstance是静态方法。

通用的惰性单例:

function getSingleton(fn) {
  var instance = null;
  return function() {
    return instance || (instance = fn.apply(this, arguments) );
  }
}

var createObj = function(name) {
  return {name: name};
}

var getSingleObj = getSingleton(createObj);

console.log(getSingleObj('123') === getSingleObj('321'));

fn为实例创建函数,用通用的单例模式包装之后,他就变成了单例创建函数。

# 8. 发布-订阅模式

定义:也可以叫观察者模式,它定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知。

前端中的利用:Vue双向绑定、事件监听函数。

一个例子-售楼处:

  • 很多人登记了信息,当有楼盘的时候,将会通知所有人前来购买。
  • 但是每个人的经济能力有限,有些人关注的是别墅楼盘,有些人关注的是小户楼盘,所以每个行为订阅的内容也不一样。
  • 有些人嫌这家售楼处的服务态度不好,想取消订阅。
通用实现:创建一个订阅-发布对象,该对象拥有一个客户组对象,拥有订阅方法,发布方法,取消方法。
  • 当订阅时:将客户订阅的内容,和执行方法存在客户组对象中:
listen = function (key, fn) {
    if (!cacheList[key]) {
      cacheList[key] = [];
    }
    cacheList[key].push(fn);
  };
  • 取消订阅时:
remove = function (key, fn) {
    var fns = cacheList[key];
    if (!fns) return false;
    // 如果只传了key 代表取消该key下所有客户
    if (!fn) {
      fns && (fns.length = 0);
    } else {
      for (var i = fns.length - 1; i >= 0; i--) {
        if (fns[i] === fn) {
          fns.splice(i, 1);
        }
      }
    }
  };
  • 发布:
trigger = function () {
    var key = Array.prototype.shift.call(arguments),
      args = arguments,
      fns = cacheList[key];

    if (!fns || fns.length === 0) return false;

    for (var i = 0, fn; fn = fns[i++];) {
      fn.apply(this, args);
    }
  }
其实仅仅只有一个客户组时远远不够的,更应该有创建命名空间的功能,详见《JavaScript设计模式与实践》8.11。

# 9. 享元模式

定义:享元模式是一种用于性能优化的模式,核心运用共享技术来支持大量细粒度的对象。

例子:我们有50件男士内衣,和50件女士内衣,我们需要模特穿上拍照。 我们有两种可能性:

  • 为50件男士内衣找50个男模特分别拍照 ,为50件女士内衣找50个女模特分别拍照。
  • 找一个男模特,和一个女模特,分别穿50次照相。(享元模式)

这个便是享元模式的模型,目的在于减少共享对象的数量,我们需要将对象分为内部状态和外部状态:

  • 内部状态存在于对象内部
  • 内部状态可以共享
  • 内部状态独立与场景,通常不会改变。
  • 外部状态决定于场景,根据场景的变化而改变。

上面的例子中,性别是内部状态,内衣是外部状态,通过区分这两种状态来减少系统的对象数量。

前端中的利用:
文件上传:用户选中文件之后,扫码文件后,为每个文件创建一个upload对象,每个upload对象有一个上传类型(插件上传,Flash上传等,不同文件可能适合不同的上传方式),但是如果用户一次性选择的文件太多,则会出现对象过多,对象爆炸。

我们利用以上的方法,分离出外部状态和内部状态。 每个共享对象不变的应该是它的上传类型(内部状态),而改变的是每个上传对象的此时此刻拥有的文件,不同的文件就是外部状态。

创建upload对象:

var Upload = function (uploadType) {
      this.uploadType = uploadType;
    };

    Upload.prototype.delFile = function (id) {
      uploadManager.setExternalState(id, this);

      if (this.fileSize < 3000) {
        return this.dom.parentNode.removeChild(this.dom);
      }

      if (window.confirm('确定要删除该文件吗?' + this.fileName)) {
        return this.dom.parentNode.removeChild(this.dom);
      }
    }

每次要删除文件的时候,将这个共享对象指向触发点击函数的文件,执行删除该文件,对象仍然保留。

创建不同内部状态的对象(被共享的不同上传类型的对象):

var UploadFactory = (function () {
      var createdFlyWeightObjs = {};

      return {
        create: function (uploadType) {
          if (createdFlyWeightObjs[uploadType]) {
            return createdFlyWeightObjs[uploadType];
          }

          return createdFlyWeightObjs[uploadType] = new Upload(uploadType);
        }
      }
    })()

定义了一个工厂模式来创建upload对象,如果某种内部状态对应的共享状态已经被创建过,那么直接返回这个对象,否则创建一个新的对象。

管理器封装外部状态:

var uploadManager = (function () {
      var uploadDatabase = {};

      return {
        add: function (id, uploadType, fileName, fileSize) {
          var flyWeightObj = UploadFactory.create(uploadType);

          var dom = document.createElement('div');
          dom.innerHTML = '<span>文件名称:' + fileName + ',文件大小:' + fileSize + '</span>' +
            '<button class="delFile">删除</button>';

          dom.querySelector('.delFile').onclick = function () {
            flyWeightObj.delFile(id);
          }

          document.body.appendChild(dom);

          uploadDatabase[id] = {
            fileName, fileSize, dom
          };

          return flyWeightObj;
        },
        setExternalState: function (id, flyWeightObj) {
          var uploadData = uploadDatabase[id];
          for (var i in uploadData) {
            flyWeightObj[i] = uploadData[i];
          }
        }
      }
    })()

uploadManager对象负责像UploadFactory提交创建对象的请求,并用一个uploadDatabase对象保存upload对象的所有外部状态。

上传函数:

var id = 0;
    window.startUpload = function (uploadType, files) {
      for (var i = 0, file; file = files[i++];) {
        var uploadObj = uploadManager.add(++id, uploadType, file.fileName, file.fileSize);
      }
    }

    startUpload('plugin', [
      {
        fileName: '1.txt',
        fileSize: 1000,
      },
      {
        fileName: '2.txt',
        fileSize: 2000,
      },
      {
        fileName: '3.txt',
        fileSize: 3000,
      }
    ]);

    startUpload('Flash', [
      {
        fileName: '4.txt',
        fileSize: 4000,
      },
      {
        fileName: '5.txt',
        fileSize: 5000,
      },
      {
        fileName: '6.txt',
        fileSize: 6000,
      }
    ]);

现在不管上传6个文件,还是2000个文件,都只会创建2个对象。

核心思想:
  • 创建能共享的对象,每个不同的能共享的对象区别在于内部状态的不同(uploadType)。
  • 每个共享的对象依然加上自己的操作,但是在执行操作之前,需要将共享对象指向当前外部状态(文件)。
  • 创建一个工厂,能够创建不同内部状态都共享对象,如果该种内部状态的共享对象已经存在,则直接返回。
  • 创建一个外部状态管理对象,包含一个数据库对象存储不同外部状态,包含一个添加函数,和指向函数(共享对象指向外部状态)。

# 10. 责任链模式

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

例子:高峰期公交车,我们不能直接把钱递给售票员,直接给离得比较近的一个人,一直传递下去,最终会到售票员手里。

前端中的利用:
电商网站不同用户种类的下单策略:

  • orderType1用户:已经支付500元,得到100元优惠券;未支付500,降级到普通用户购买界面。
  • orderType2用户:已经支付200元,得到50元优惠券;未支付200,降级到普通用户购买界面。
  • orderType3用户:普通购买。
  • 库存限制,针对code3。

新手写法:根据orderType,isPay,stock来写if-else分支来进行具体操作。
责任链模式写法:

分别写order500、order200、orderNormal的函数,如果满足条件则执行,不满足条件则返回一个字段表示交给下一个节点执行:

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('手机库存不足');
  }
}

编写责任链控制函数:

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;
};

类似于链表,每个节点都保存着下一个节点,并含有一个该节点的执行函数,和设置下一个节点的函数。

// 将每个具体执行函数封装为一个责任链节点
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);
chainOrder500.passRequest(2, true, 500);
chainOrder500.passRequest(3, true, 500);
chainOrder500.passRequest(1, false, 0);

这样只需要第一个节点执行,如果不满足则请求自动交付给下一个节点,直到到达节点尾部。

如果未来还有更多情况,比如有交了50定金的,可以给10元的优惠券,这样的情况可以直接添加节点,改变节点顺序,不会对已有的方法做更改。

本例子可以用Promise做该写,如果成功则Promise.resolve()否则Promise.reject()
还可以使用AOP的方式Function.prototype.after做改写。

核心思想:将具体执行方法包装为一个个责任链子节点,执行第一个节点,如果情况满足则执行,不满足则调用下一个节点的执行方法。

# 11. 中介者模式

定义:将行为分布到各个对象中,把对象划分为更小的细粒度,但是由于细粒度之间对象的联系激增,又有可能反过来降低它们的可复用性。中介者模式使网状的多对多关系变成了相对简单的一对多关系。

例子:

  • 机场指挥中心:每架飞机不可能和其他所有飞机逐一联系,来确定是否能起飞,是否能滑动,这样的联系都交给了指挥中心来做。每架飞机只需要联系中介者即可。
  • 博彩公司算赔率:和机场指挥中心是一样的道理。

前端的利用:

商品购买:通常商品购买会有选择框,输入框,还有信息提示框,我们需要选择或者输入时,信息都能有正确的提示,一个办法是强耦合,在选择框变动后,去修改提示框。如果添加新的选择框,代码变动会更大。

引入中介者:具体处理逻辑交给中介者处理,其他选择框只与中介者交互。

html:

<body>
  选择颜色:
  <select name="" id="colorSelect">
    <option value="">请选择</option>
    <option value="red">红色</option>
    <option value="blue">蓝色</option>
  </select>
  <br> 选择内存:

  <select name="" id="memorySelect">
    <option value="">请选择</option>
    <option value="32G">32g</option>
    <option value="16G">16g</option>
  </select>
  <br>
  <br> 输入购买数量:

  <input type="text" id="numberInput">
  <br>
  <br> 您选择了颜色:

  <div id="colorInfo"></div>
  <br> 您选择了内存:
  <div id="memoryInfo"></div>
  <br> 您输入了数量:
  <div id="numberInfo"></div>
  <br>

  <button id="nextBtn" disabled="true">请选择手机颜色和购买数量</button>
  </body>

获取各种框dom节点:

var colorSelect = document.getElementById('colorSelect');
    var memorySelect = document.getElementById('memorySelect');
    var numberInput = document.getElementById('numberInput');

    var colorInfo = document.getElementById('colorInfo');
    var memoryInfo = document.getElementById('memoryInfo');
    var numberInfo = document.getElementById('numberInfo');

    var nextBtn = document.getElementById('nextBtn');

编写中介者:

var mediator = (function () {
      return {
        changed: function (obj) {
          var color = colorSelect.value,
            memory = memorySelect.value,
            number = numberInput.value,
            stock = goods[color + '|' + memory];

          if (obj === colorSelect) {
            colorInfo.innerHTML = color;
          } else if (obj === memorySelect) {
            memoryInfo.innerHTML = memory;
          } else if (obj === numberInput) {
            numberInfo.innerHTML = number;
          }

          if (!color) {
            nextBtn.disabled = true;
            nextBtn.innerHTML = '请选择手机颜色';
            return;
          }

          if (!memory) {
            nextBtn.disabled = true;
            nextBtn.innerHTML = '请选择内存大小';
            return;
          }

          if (!(Number.isInteger(number - 0) && number > 0)) {
            nextBtn.disabled = true;
            nextBtn.innerHTML = '请输入正确的购买数量';
            return;
          }

          nextBtn.disabled = false;
          nextBtn.innerHTML = '放入购物车';
        }
      }
    })();

变动只与中介者交互:

colorSelect.onchange = function() {
      mediator.changed(this);
    };

    memorySelect.onchange = function() {
      mediator.changed(this);
    };

    numberInput.oninput = function() {
      mediator.changed(this);
    };

12. 装饰者模式

定义:在不改变对象自身的基础上,在程序运行期间给对象动态添加职责。(包装器)

例子:

  • 给自行车扩展,给4种自行车扩展3个配件,在继承的基础上需要建立出12个子类。
  • 但是动态的把这些动态添加到自行车上则住需要额外3个类(3个配件)。

装饰者:

// 保存引用的装饰者模式

var plane = {
  fire: function() {
    console.log('发射普通子弹');
  }
}

var missileDecorator = function() {
  console.log('发射导弹');
}

var atomDecorator = function() {
  console.log('发射原子弹');
}

var fire1 = plane.fire;

plane.fire = function() {
  fire1();
  missileDecorator(); 
}

var fire2 = plane.fire;

plane.fire = function() {
  fire2();
  atomDecorator();
}

plane.fire();

AtomDecorator 包装 MissileDecorator 包装 Plane。 这样写完全符合开发-封闭原则,在添加新功能的时候没有去改动别人点方法,但是不好的就是,如果包装点层次太多,中间变量就太多了。还会遇见this劫持的问题:

var _getEleById = document.getElementById;

document.getElementById = function(id) {
    alert(1);
    return _getElementById(id);
}

this被劫持了。

解决以上两个问题的最好方法就上AOP函数:

Function.prototype.before = function (fn) {
  var _self = this; // 保存原函数的引用
  return function () { // 返回了包含原函数和新函数的代理函数
    fn.apply(this, arguments);
    return _self.apply(this, arguments); // 执行原函数
  }
}

Function.prototype.after = function (fn) {
  var _self = this; // 保存原函数的引用
  return function () {
    var ret = _self.apply(this, arguments);
    fn.apply(this, arguments);
    return ret;
  }
}
  • 第一个:返回在函数之前执行
  • 第二个:返回在函数之后执行
前端的利用:数据上报这样和业务逻辑无关的函数都可以利用包装者进行包装。

# 13. 状态模式:

定义:区分事物的内部状态,事物的内部状态的改变往往会带来事物行为的改变。

例子:

  • 通常的电灯,只有一个按钮,按下按钮;
    • 如果电灯是关的:那么开灯
    • 如果点灯是开着的:那么关灯

这里换成代码,就是简单的if-else,但是如果再复杂一点呢:新添加一个按钮,如果这个按钮按下,那么点灯是弱-强-关模式;否则是开-关模式。

这个时候你已经开始发现if-else代码的缺点了:

  • 每次灯扩展,都需要修改内部代码,违反开放-封闭原则
  • 所有与行为有关的事情都在一个函数里
  • 状态切换不明显,仅仅只有一个字段的改变
  • if-else太多太繁杂。

状态模式下的点灯程序(假设这里只有一个按钮,切换开关):

我们第一步创建点灯(富含状态的这个对象):

var Light = function () {
    this.currState = FSM.off;
    this.button = null;
};

this.currState代表的是不同的状态:这里的状态用对象来表示,开关两个状态就是两个对象:

var FSM = {
        off: {
        buttonWasPressed: function () {
          console.log('关灯');
          this.button.innerHTML = '下一次按我是开灯';
          this.currState = FSM.on;
        }
        },
        on: {
        buttonWasPressed: function() {
          console.log('开灯');
          this.button.innerHTML = '下一次按我是关灯';
          this.currState = FSM.off;
        }
        }
    }

接下来编写初始化电灯函数:

Light.prototype.init = function () {
      var button = document.createElement('button'),
        self = this;

      button.innerHTML = '已关灯';
      this.button = document.body.appendChild(button);

      this.button.onclick = function () {
        self.currState.buttonWasPressed.call(self);
      }
    };

给按钮绑定事件,按钮触发时,触发当前状态对象的更替事件。(执行外部行为,切换当前的状态)

总结:

状态模式编写思路:

  • 设计富含状态的对象(主对象):
    • 编写各种状态下的行为
    • 状态属性
    • 初始化:绑定按钮,在该状态下的状态切换
  • 设计各种状态对象:
    • 接收主对象的this
    • 按钮触发时,改状态利用this,多状态则编写多个不同触发函数
      切换主对象状态、调用主状态行为

与策略模式的区别:

  • 策略模式中每个策略类相互平等没有关系
  • 状态模式中状态类之间的关系是提前确定好的。

14. 适配器模式

定义:解决两个软件实体之间的接口不兼容的问题。

例子:插头转换器,转换不同地区的电压问题。

前端中:

  • 地图渲染:
    假如地图渲染的函数是这样的:
var renderMap = function( map ) {
  if(map.show instanceof Function) {
    map.show();
  }
}

地图:

var googleMap = {
  show() {
    console.log('google地图开始渲染');
  }
}

var baiduMap = {
  display() {
    console.log('baidu地图开始渲染');
  }
}

我们可以只带googleMap没有问题,但是baiduMap提供的接口名明显不一致,如果去改renderMap函数违反了开放封闭原则。

那么现在我们只能用适配器包装一下baiduMap:

var baiduMapAdapter = {
  show() {
    return baiduMap.display();
  }
}

思路:封装与其他不同的方法或者对象,而不要去改动原有的函数。

其他例子:xml与json格式适配,json与对象格式的转变等。

推荐阅读更多精彩内容