JS设计模式与开发实践

275 阅读8分钟

一、单例模式

有些对象我们只需要一个,比如线程池、全局缓存、浏览器中的window对象、登录框、购物车、JQuery的$,vuex和redux等

单例模式创建登录框,创建对象和管理单例的职责被分布在不同的方法中,这两个方法组合起来才具有单例模式的威力。

1、创建登录框

const getSinge = function(fn){
  var result;
  return function(){
  if(result){ return result };
  result = fn.apply(this, arguments);
    return result;
  }
}

const createLoginLayer = function(name){
  var div = document.createElement('div');
  div.innerHTML = name;
  div.style.display = 'none';
  document.body.append(div);
  return div;
}

const createSingeLoginLayer = getSinge(createLoginLayer);
document.getElementById('loginBtn').onclick = function(){ // 页面只会有一个div
  var loginLayer = createSingeLoginLayer('chenshun Login');
  loginLayer.style.display = 'block';
}

二、策略模式

1.策略模式重构表单校验

<!DOCTYPE html><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;
          }
        }
      }
      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(event){
        event.preventDefault();
        var errorMsg = validataFunc();
        if(errorMsg){
          alert(errorMsg);
          return false;
        }
      }
    </script>
  </body>
</html>

三、代理模式

1.代理实现图片预加载

<!DOCTYPE html>
<html>
  <body>
    <script>
      var myImage = (function(){
        var imgNode = document.createElement('img');
        document.body.appendChild(imgNode);
        return function(src){
          imgNode.src = src;
        }
      })();
      var proxyImage = (function(){
        var img = new Image;
        img.onload = function(){
          myImage(this.src); // 回调函数(非箭头函数回调)中this指向监听的对象
        }
        return function(src){
          myImage('https://ss0.bdstatic.com/70cFuHSh_Q1YnxGkpoWK1HF6hhy/it/u=3919534254,824588318&fm=26&gp=0.jpg');
          img.src = src;
        }
      })();
      proxyImage('https://ss3.bdstatic.com/70cFv8Sh_Q1YnxGkpoWK1HF6hhy/it/u=2454599664,1105030419&fm=26&gp=0.jpg');
      // 必要时可以直接去掉代理
      myImage('https://ss3.bdstatic.com/70cFv8Sh_Q1YnxGkpoWK1HF6hhy/it/u=2454599664,1105030419&fm=26&gp=0.jpg');
    </script>
  </body>
</html>

2.代理文件上传,频繁点击限制每两秒才能上传一次

<!DOCTYPE html>
<html>
  <body>
    <input type="checkbox" id="1"></input>1
    <input type="checkbox" id="2"></input>2
    <input type="checkbox" id="3"></input>3
    <input type="checkbox" id="4"></input>4
    <input type="checkbox" id="5"></input>5
    <script>
      var synchronousFile = function(id){
        console.log(`开始同步文件,id为${id}`)
      };
      var proxySynchronousFile = (function(){
        const cache = [];
        let timer;
        return function(id){
          cache.push(id);
          if(timer){ return; };
          timer = setTimeout(()=>{
            synchronousFile(cache.join(','));
            clearTimeout(timer);
            timer = null;
            cache.length = 0;
          }, 2000)
        }
      })()
      var checkbox = document.getElementsByTagName('input');
      for(var i = 0, c; c = checkbox[i++];){
        c.onclick = function(){
          if(this.checked === true){
            proxySynchronousFile(this.id);
          }
        }
      }
    </script>
  </body>
</html>

3.缓存代理,计算乘积

var mult = function(){
  console.log('开始计算乘积');
  var a = 1;  for( var i = 0, l = arguments.length; i < l ; i++){
    a = a * arguments[i];
  }
  return a;
}

var proxyMult = (function(){
  var cache = {};
  return function(){
    var args = Array.prototype.join.call(arguments, ',');
    if(args in cache){
      return cache[args];
    }
    return cache[ args ] = mult.apply(this, arguments);
  }})();

console.log(proxyMult(1,2,3,4));
console.log(proxyMult(1,2,3,4));

4.高阶函数动态创建代理

const mult = function(){
  console.log('乘积');
  let a = 1;
  for( let i = 0, l = arguments.length; i < l ; i++){
    a = a * arguments[i];
  }
  return a;
}

const plus = function(){
  console.log('和');
  let a = 0;
  for(let i = 0, l = arguments.length; i < l; i++){
    a = a + arguments[i];
  }
  return a;
}
const createProxyFactory = function(fn){
  const cache = {};
  return function(){
    const args = Array.prototype.join.call(arguments, ',');
    if(args in cache){
      return cache[args];
    }
    return cache[ args ] = fn.apply(this, arguments);
  }
};

const proxyMult = createProxyFactory(mult);
const proxyPlus = createProxyFactory(plus);

console.log(proxyMult(1,2,3,4));
console.log(proxyMult(1,2,3,4));
console.log(proxyPlus(1,2,3,4));
console.log(proxyPlus(1,2,3,4));

四、发布-订阅模式

1.支持先发布后订阅,并可以给事件对象提供创建命名空间的功能

const Event = (function(){
  var Event,
  _default = 'default';
  Event = function(){
    var _listen,
    _trigger,
    _remove,
    _shift = Array.prototype.shift,
    _unshift = Array.prototype.unshift,
    namespaceCache = {},
    _create,
    each = function(ary, fn){
      var ret;
      for(var i = 0, l = ary.length; i < l; i++){
        var n = ary[i];
        ret = fn.call(n, i, n);
      }
      return ret;
    };
    _listen = function(key, fn, cache){
      if(!cache[key]){
        cache[key] = [];
      }
      cache[key].push(fn);
    };
    _remove = function(key, cache, fn){
      if(cache[key]){
        if(fn){
          for(var i = cache[key].length; i >= 0; i--){
            if(cache[key][i] === fn){
              cache[key].splice(i, 1);
            }
          }
        }else{
          cache[key] = [];
        }
      }
    };
    _trigger = function(){
      var cache = _shift.call(arguments),
        key = _shift.call(arguments),
        args = arguments,
        _self = this,
        ret,
        stack = cache[key];
      if(!stack || !stack.length){
        return;
      }
      return each(stack, function(){
        return this.apply(_self, args);
      });
    };
    _create = function(namespace){
      var namespace = namespace || _default;
      var cache = {},
      offlineStack = [], //离线事件
      ret = {
        listen: function(key, fn, last){
          _listen(key, fn, cache);
          if(offlineStack === null){
            return
          }
          if(last === 'last'){
            offlineStack.length && offlineStack.pop();
          }else{
            each(offlineStack, function(){
              this();
            })
          }
          offlineStack = null;
        },
        one: function(key, fn, last){
          _remove(key, cache);
          this.listen(key, fn, last);
        },
        remove: function(key, fn){
          _remove(key, cache, fn);
        },
        trigger: function(){
          var fn,
          args,
          _self = this;
          _unshift.call(arguments, cache);
          args = arguments;
          fn = function(){
            return _trigger.apply(_self, args);
          };
          if(offlineStack){
            return offlineStack.push(fn);
          }
          return fn();
        }
      };
      return namespace ?
         (namespaceCache[namespace] ? namespaceCache[namespace] :
           namespaceCache[namespace] = ret)
             : ret;
    };
    return {
      create: _create,
      one: function(key, fn, last){
        var event = this.create();
        event.one(key, fn, last);
      },
      remove: function(key, fn){
        var event = this.create();
        event.remove(key, fn);
      },
      listen: function(key, fn, last){
        var event = this.create();
        event.listen(key, fn, last);
      },
      trigger: function(){
        var event = this.create();
        event.trigger.apply(this, arguments);
      }
    }
  }()
  return Event;
})()

Event.trigger('click', 1);
Event.listen('click', function(a){
  console.log(a);
});
Event.listen('click', function(a){
  console.log(a); // 监听两次只打印一次1
});

Event.create('namespace1').trigger('click', 1);
Event.create('namespace1').listen('click', function(a){
  console.log(a);
})

Event.create('namespace2').trigger('click', 2);
Event.create('namespace2').listen('click', function(a){
  console.log(a);
})

五、命令模式

JavaScript可以用高阶函数非常方便地实现命令模式,命令模式在JavaScript语言中是一种隐形的模式。点击一个按钮执行一个特点或一系列操作就是一种命令模式,以下用命令模式模拟街头霸王游戏,支持回放

<!DOCTYPE html>
<html>
  <body>
    <button id="replay">播放录像</button>
    <script>
      var Ryu = {
        attack: function(){
          console.log('攻击');
        },
        defense: function(){
          console.log('防御');
        },
        jump: function(){
          console.log('跳跃')
        },
        crouch: function(){
          console.log('蹲下')
        }
      };
      var makeCommand = function(receiver, state){
        if(receiver[state]){
          return function(){
            receiver[state]();
          }
        }
      };
      var commands = {
        '119': 'jump', // w
        '115': 'crouch', // s
        '97': 'defense', // a
        '100': 'attack' // d
      };
      var commandStack = []; // 保存命令的堆栈
      document.onkeypress = function(ev){
        var keyCode = ev.keyCode,
        command = makeCommand(Ryu, commands[keyCode]);
        if(command){
          command(); // 执行命令
          commandStack.push(command); // 将刚刚执行过的命令保存进堆栈
        }
      };
      document.getElementById('replay').onclick = function(){ // 点击播放录像
        var command;
        while(command = commandStack.shift()){
          command();
        }
      };
    </script>
  </body>
</html>

六、组合模式

基本对象可以被组合成更复杂的组合对象,组合对象又可以被组合,这样不断传递下去

1.组合模式打开家电

<!DOCTYPE html>
<html>
  <body>
    <button id="button">按我</button>
    <script>
      var MacroCommand = function(){
        return {
          commandList: [],
          add: function(command){
            this.commandList.push(command);
          },
          excute: function(){
            for(var i = 0, command; command = this.commandList[i++];){
              command.excute();
            }
          }
        }
      };
      var openAcCommand = {
        excute: function(){
          console.log('打开空调');
        }
      };

      /* 家里空调和音响是连在一起的,所以可以用一个宏命令来组合打开电视和打开音响的命令 */
      var openTvCommand = {
        excute: function(){
          console.log('打开电视');
        }
      };
      var openSoundCommand = {
        excute: function(){
          console.log('打开音响');
        }
      };
      var macroCommand1 = MacroCommand();
      macroCommand1.add(openTvCommand);
      macroCommand1.add(openSoundCommand);

      /* 关门、打开电脑和登录qq的命令*/
      var closeDoorCommand = {
        excute: function(){
          console.log('关门');
        }
      };
      var openPcCommand = {
        excute: function(){
          console.log('打开电脑');
        }
      };
      var openQQCommand = {
        excute: function(){
          console.log('登录QQ');
        }
      };
      var macroCommand2 = MacroCommand();
      macroCommand2.add(closeDoorCommand);
      macroCommand2.add(openPcCommand);
      macroCommand2.add(openQQCommand);

      /* 现在把所有的命令组合成一个超级命令 */
      var macroCommand = MacroCommand();
      macroCommand.add(openAcCommand);
      macroCommand.add(macroCommand1);
      macroCommand1.add(macroCommand2);

      /* 最后给遥控器绑定超级命令 */
      var setCommand = (function(command){
        document.getElementById('button').onclick = function(){
          command.excute();
        }
      })(macroCommand);
    </script>
  </body>
</html>

2.组合模式实现扫描文件夹

/** * Folder */
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, files = this.files; file = files[i++];){
    file.scan();
  }
};

/** * File */
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('JavaScript');
var folder2 = new Folder('jQuery');

var file1 = new File('JavaScript设计模式与开发实践');
var file2 = new File('精通jQuery');
var file3 = new File('重构与模式');

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

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

var folder3 = new Folder('Nodejs');
var file4 = new File('深入浅出Node.js');
folder3.add(file4);
var file5 = new File('JavaScript语言精髓与编程实践');

folder.add(folder3);
folder.add(file5);

folder.scan();

3.扫描文件夹之前可以先删除

/** * Folder */
var Folder = function(name){
  this.name = name;
  this.parent = null;
  this.files = [];
};

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

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

Folder.prototype.remove = function(){
  if(!this.parent){ // 根节点或者树外的游离节点
    return
  }
  for(var files = this.parent.files, l = files.length - 1; l >=0; l--){
    var file = files[l];
    if(file === this){
      files.splice(l, 1);
    }
  }
}

/** * File */
var File = function(name){
  this.parent = null;
  this.name = name;
};

File.prototype.add = function(){
  throw new Error('文件下面不能再添加文件');
};
File.prototype.scan = function(){
  console.log(`开始扫描文件:${this.name}`);
};
File.prototype.remove = function(){
  if(!this.parent){
 // 根节点或者树外的节点
    return;
  }
  for(var files = this.parent.files, l = files.length - 1; l >=0; l--){
    var file = files[l];
    if(file === this){
      files.splice(l, 1);
    }
  }
}

var folder = new Folder('学习资料');
var folder1 = new Folder('JavaScript');
var file1 = new Folder('深入浅出Node.js');

folder1.add(new File('JavaScript设计模式与开发实践'));
folder.add(folder1);folder.add(file1);

folder1.remove();
folder.scan();

七、模版方法模式

在模版方法中,子类实现中的相同部分被上移到父类中,而将不同的部分留待子类来实现。在JavaScript中,我们很多时候都不需要这样照壶画瓢地去实现一个模版方法模式,高阶函数是更好的选择。

1.以冲泡饮料为例,泡茶和泡咖啡很多过程都是相识的,可以用模版方法模式来搭建冲泡骨架

var Beverage = function(param){
  var boilWater = function(){
    console.log('把水沸腾');
  };

  var brew = param.brew || function(){
    throw new Error('必须传递brew方法');
  };

  var pourInCup = param.pourInCup || function(){
    throw new Error('必须传递pourInCup方法')
  };

  var addCondiments = param.addCondiments || function(){
    throw new Error('必须传递addCondiments方法');
  }

  var F = function(){};
  F.prototype.init = function(){
    boilWater();
    brew();
    pourInCup();
    addCondiments();
  };

  return F;
};

var Coffee = Beverage({
  brew: function(){
    console.log('用沸水冲泡咖啡');
  },
  pourInCup: function(){
    console.log('把咖啡倒进被子');
  },
  addCondiments: function(){
    console.log('加糖和牛奶');
  }
});

var Tea = Beverage({
  brew: function(){
    console.log('用沸水浸泡茶叶');
  },
  pourInCup: function(){
    console.log('把茶倒进杯子');
  },
  addCondiments: function(){
    console.log('加柠檬');
  }
});

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

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

八、享元(flyweight)模式

享元模式是为了解决性能问题而生的一种模式,这跟大部分模式的诞生原因都不一样。在一个存在大量相似对象的系统中,享元模式可以很好的解决大量对象带来的性能问题。

剥离了外部状态的对象成为共享对象,外部状态在必要时被传入共享对象来组装成一个完整的对象。虽然组装外部状态成为一个完整对象的过程需要花费一定的时间,但却可以大大减少系统中的对象数量,相比之下,这点时间或许微不足道。因此,享元模式是一种用时间换空间的优化方式。

享元模式适用场景:1、一个程序中适用了大量的相似对象。2、由于使用了大量对象,造成很大的内存开销。3、对象的大多数状态都可以变为外部状态。4、剥离出对象的外部状态之后,可以用相对较少的共享对象取代大量的对象。

1.文件上传大量创建插件上传对象和flash上传对象导致对象爆炸

<!DOCTYPE html>
<html>
  <body>
    <script>
      var id = 0;

      window.startUpload = function(uploadType, files){ // uploadType区分是控件还是flash
        for(var i = 0, file; file = files[i++];){
          var uploadObj = new Upload(uploadType, file.fileName, file.fileSize);
          uploadObj.init(id++); // 给upload对象设置一个唯一的id
        }
      };

      var Upload = function(uploadType, fileName, fileSize){
        console.log('创建一个对象'); // 打印六次
        Object.assign(this, { uploadType, fileName, fileSize });
        this.dom = null;
      };

      Upload.prototype.init = function(id){
        var that = this;
        this.id = id;
        this.dom = document.createElement('div');
        this.dom.innerHTML = `
          <span>文件名称:${this.fileName},文件大小:${this.fileSize}</span>
          <button class='delFile'>删除</button>
        `;
        this.dom.querySelector('.delFile').onclick = function(){
          that.delFile();
        }
        document.body.appendChild(this.dom);
      };

      Upload.prototype.delFile = function(){
        if(this.fileSize < 3000){
          return this.dom.parentNode.removeChild(this.dom);
        }
        if(window.confirm(`确定要删除该文件吗?${this.fileName}`)){
          return this.dom.parentNode.removeChild(this.dom);
        }
      }

      startUpload('plugin', [
        {
          fileName: '1.txt',
          fileSize: 1000
        },
        {
          fileName: '2.html',
          fileSize: 3000
        },
        {
          fileName: '3.txt',
          fileSize: 5000
        }
      ]);
      startUpload('flash', [
        {
          fileName: '4.txt',
          fileSize: 1000
        },
        {
          fileName: '5.html',
          fileSize: 3000
        },
        {
          fileName: '6.txt',
          fileSize: 3000
        }
      ])
    </script>
  </body>
</html>

2.享元模式重构文件上传

<!DOCTYPE html>
<html>
  <body>
    <script>
      var Upload = function(uploadType){
        this.uploadType = uploadType;
      };

      Upload.prototype.delFile = function(id){
        uploadManager.setExternalState(id, this); //(1)
        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 createFlyWeightObjs = {};
        return {
          create: function(uploadType){
            if(createFlyWeightObjs[uploadType]){
              return createFlyWeightObjs[uploadType];
            }
            console.log(`创建upload对象,类型为${uploadType}`); // 只创建了两次
            return createFlyWeightObjs[uploadType] = new Upload(uploadType);
          }
        }
      })();

      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];
            }
          }
        }
      })();

      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.html',
          fileSize: 3000
        },
        {
          fileName: '3.txt',
          fileSize: 5000
        }
      ]);

      startUpload('flash', [
        {
          fileName: '4.txt',
          fileSize: 1000
        },
        {
          fileName: '5.html',
          fileSize: 3000
        },
        {
          fileName: '6.txt',
          fileSize: 3000
        }
      ])
    </script>
  </body>
</html>

对象池是另外一种性能优化方案,它跟享元模式有一些相似之处,但没有分离内部状态和外部状态这个过程。

通用对象池实现

<!DOCTYPE html>
<html>
  <body>
    <script>
      var objectPoolFactory = function(createObjFn){
        var objectPool = [];
        return {
          create: function(){
            var obj = objectPool.length === 0 ?
              createObjFn.apply(this, arguments) : objectPool.shift();
              return obj;
          },
          recover: function(obj){
            objectPool.push(obj);
          }
        }
      };

      var iframeFactory = objectPoolFactory(function(){
        var iframe = document.createElement('iframe');
        document.body.appendChild(iframe);
        iframe.onload = function(){
          iframe.onload = null; // 防止iframe重复加载的bug
          iframeFactory.recover(iframe); // iframe加载完成后回收节点
        }
        return iframe;
      });

      var iframe1 = iframeFactory.create();
      iframe1.src = 'https://www.hao123.com/';

      var iframe2 = iframeFactory.create();
      iframe2.src = 'http://QQ.com';

      setTimeout(function(){
        var iframe3 = iframeFactory.create();
        iframe3.src = 'https://ai.taobao.com/';
      }, 3000);
    </script>
  </body>
</html>

九、职责链模式

把一个充满if-else的函数拆分成多个函数,可以去掉许多嵌套的条件分支语句。

1.商城给出了几种优惠政策,在正式购买后,已经支付过500元定金的用户会收到100元的商城优惠,200元定金的用户可以收到50元的优惠券,而之前没有支付定金的用户只能进入普通购买模式,也就是没有优惠券,且在库存有限的情况下不一定保证能买到。

var order500 = function(orderType, pay, stock){
  if(orderType === 1 && pay === true){
    console.log('500元定金预购,得到100元优惠券');
  }else{
    return 'nextSuccess'; // 我不知道下一个节点是谁,反正把请求往后面传递
  }
};

var order200 = function(orderType, pay, stock){
  if(orderType === 2 && pay === true){
    console.log('200元定金预购,得到50元优惠券');
  }else{
    return 'nextSuccess'; // 我不知道下一个节点是谁,反正把请求往后面传递
  }
};

var orderNormal = function(orderType, pay, stock){
  if(stock > 0){
    console.log('普通购买,无优惠券');
  }else{
    return '手机库存不足';
  }
};

Function.prototype.after = function(fn){
  var self = this;
  return function(){
    var ret = self.apply(this, arguments);
    if(ret === 'nextSuccess'){
      return fn.apply(this, arguments);
    }
    return ret;
  }
};

// 可以动态的指定下一节点,也可以很方便地填加节点而不需要改动原代码,符合开发-封闭原则。
var order = order500.after(order200).after(orderNormal);

order(1, true, 500);
order(2, true, 500);
order(1, false, 500);

十、中介者模式

现实中的中介者:机场指挥塔、博彩公司

1、泡泡堂游戏普通实现,这段代码每个玩家和其它玩家都是紧紧耦合在一起的。在此代码中,每个玩家对象都有两个属性,this.partners和this.enemies,用来保存其它玩家对象的引用。当每个对象的状态发生改变,比如角色移动,吃到道具或者死亡时,都必须要显示地遍历通知其它对象。如果在大型游戏中,可能还会有玩家掉线,红队变蓝队这种case,那这段代码就只能迅速进入投降模式了

var players = [];

function Player(name, teamColor){
  this.partners = []; // 队友列表
  this.enemies = []; //敌人列表
  this.state = 'live'; // 玩家列表
  this.name = name;
  this.teamColor = teamColor;
};

Player.prototype.win = function(){ // 玩家团队胜利
  console.log(`winner:${this.name}`);
};

Player.prototype.lose = function(){ // 玩家团队失败
  console.log(`loser:${this.name}`);
};

Player.prototype.die = function(){ // 玩家死亡
  var all_dead = true;
  this.state = 'dead'; // 设置玩家死亡状态为死亡
  for(var i = 0, partner; partner = this.partners[i++];){ // 遍历队友列表
    if(partner.state !== 'dead'){ // 如果还有一个队友没有死亡,则游戏还未失败
      all_dead = false;
      break;
    }
  }
  if(all_dead === true){
    this.lose(); // 通知队友全部死亡
    for(var i = 0, partner; partner = this.partners[i++];){ //通知所有队友玩家游戏失败
      partner.lose();
    }
    for(var i = 0, enemy; enemy = this.enemies[i++];){ // 通知所有敌人游戏胜利
      enemy.win();
    }
  }
}

var playerFactory = function(name, teamColor){
  var newPlayer = new Player(name, teamColor); // 创建新玩家
  for(var i = 0, player; player = players[i++];){
    if(player.teamColor === newPlayer.teamColor){ // 如果是同一队的玩家
      player.partners.push(newPlayer); // 相互添加到队友列表
      newPlayer.partners.push(player);
    }else{
      player.enemies.push(newPlayer); // 相互添加到敌人列表
      newPlayer.enemies.push(player);
    }
  }
  players.push(newPlayer);
  return newPlayer;
};

// 红队
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();

2.泡泡堂游戏中介者模式实现

可以看到,除了中介者本身,没有一个玩家知道其它玩家的存在,玩家与玩家之间的耦合关系已经完全解除,某个玩家的任何操作都不需要通知其它玩家,而只需要给中介者发送一个消息,中介者处理完消息之后会把结果反馈给其他玩家对象。我们还可以继续给中介者扩展更多功能,以适应游戏需求的不断变化。

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.ReceiveMessage('playerDead', this); // 给中介者发送消息,玩家死亡
};

Player.prototype.remove = function(){
  playerDirector.ReceiveMessage('removePlayer', this); // 给中介者发送消息,移除一个玩家
};

Player.prototype.changeTeam = function(color){
  playerDirector.ReceiveMessage('changeTeam', this, color); // 给中介者发送消息,玩家换队
};

var playerFactory = function(name, teamColor){
  var newPlay = new Player(name, teamColor); // 创造一个新的对象
  playerDirector.ReceiveMessage('addPlayer', newPlay); // 给中介者发送消息,新增玩家
  return newPlay;
};

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();
          }
        }
      }
    }
  }
  var ReceiveMessage = function(){
    var message = Array.prototype.shift.call(arguments); // arguments的第一个参数为消息名称
    operations[message].apply(this, arguments);
  };
  return {
    ReceiveMessage
  }
})();

// 红队
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();

// player1.remove();
// player2.remove();
// player3.die();
// player4.die();

player1.changeTeam('blue');
player2.die();
player3.die();
player4.die();

中介者模式是迎合迪米特法则的一种实现。迪米特法则也叫最少知识法则,是指一个对象应该尽可能少地去了解另外的对象(类似不和陌生人说话),如果对象耦合性太高,一个对象改变之后,难免会影响到其他对象。

中介者缺陷:最大的缺陷是系统中会新增一个中介者对象,因为对象之间交互的复杂性,转移成了中介者对象的复杂性,使得中介者经常是巨大的。中介者对象自身往往就是一个难以维护的对象,有必要时再考虑使用中介者模式。

十一、装饰者模式

数据上报、统计函数执行时间、动态改变函数参数以及插件式的表单验证都可以应用装饰者模式。

1.插件式的表单验证

Function.prototype.before = function(beforefn){
  var _self = this;
  return function(){
    if(beforefn.apply(this, arguments) === false){
      // beforefn返回false的情况直接return,不再执行后面的原函数
      return;
    }
    return _self.apply(this, arguments);
  }
}

var validate = function(){
  if(username.value === ''){
    alert('用户名不能为空');
    return false;
  }
  if(password.value === ''){
    alert('密码不能为空');
    return false;
  }
}

var formSubmit = function(){
  var param = {
    username: username.value,
    password: password.value
  }
  ajax('http://xxx.com/login', param);
}

formSubmit = formSubmit.before(validate);
submitBtn.onclick = function(){
  formSubmit();
}

这段代码中,校验和提交表单完全分离开来,他们不再有任何耦合关系。但是,装饰者模式也叠加了函数的作用域,如果装饰的链条过长,性能上也会受到一些影响。

十二、状态模式

文件上传,音乐、视频播放,游戏格斗(比如跌倒时不能攻击),TCP请求有建立连接、监听、关闭等状态。状态模式的优点是把事物封装成单独的类,跟此种状态有关的行为都被封装在这个类的内部。

以电灯的开关为例子,电灯的状态不同那点击开关的行为是不一样的,可以用状态模式消除大量的if-else分支。

<!DOCTYPE html>
<html>
  <body>
    <script>
      // OffLightState;
      var OffLightState = function(light){
        this.light = light;
      };
      OffLightState.prototype.buttonWasPressed = function(){
        console.log('弱光'); // offLightState对应的行为
        this.light.setState(this.light.weakLightState); //切换到weakLightState
      };

      // WeakLightState
      var WeakLightState = function(light){
        this.light = light;
      }
      WeakLightState.prototype.buttonWasPressed = function(){
        console.log('强光');
        this.light.setState(this.light.strongLightState);
      }

      // StrongLightState
      var StrongLightState = function(light){
        this.light = light;
      };
      StrongLightState.prototype.buttonWasPressed = function(){
        console.log('关灯');
        this.light.setState(this.light.offLightState);
      };

      var Light = function(){
        this.offLightState = new OffLightState(this);
        this.weakLightState = new WeakLightState(this);
        this.strongLightState = new StrongLightState(this);
        this.button = null;
      };
      Light.prototype.init = function(){
        var button = document.createElement('button'),
        self = this;
        this.button = document.body.appendChild(button);
        this.button.innerHTML = '开关';
        this.currState = this.offLightState; // 设置当前状态
        this.button.onclick = function(){
          self.currState.buttonWasPressed();
        }
      };
      Light.prototype.setState = function(newState){
        this.currState = newState;
      };
      var light = new Light();
      light.init();
    </script>
  </body>
</html>

状态模式的缺点是会在系统中定义很多个类,编写20个状态类是一项枯燥乏味的工作,而且系统中会因此而增加不少对象。另外,由于逻辑分布在状态类中,虽然避开了不受欢迎的条件分支,但也造成逻辑分散的问题,我们无法在一个地方就看出整个状态机的逻辑。

十三、适配器模式

适配器模式是一种亡羊补牢的模式,没有人会在程序的设计之初就使用它

渲染地图:谷歌地图提供的不是renderMap需要的show方法,而是display方法,在不该动renderMap的情况下可以借助适配器模式来实现

var renderMap = function(map){
  if(map.show instanceof Function){
    map.show();  }
};

var googleMap = {
  show: function(){
    console.log('开始渲染谷歌地图');
  }
};

var baiduMap = {
  display: function(){
    console.log('开始渲染百度地图');
  }
};

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

renderMap(googleMap);
renderMap(baiduMapAdapter);

十四、设计原则与编程技巧

单一职责原则:例如代理模式、迭代器模式、单例模式、装饰者模式。

最少知识原则:中介者模式。

开放-封闭原则:几乎所有的好的设计模式都符合这一原则。其它设计原则都是达到这个目标的过程。让程序保持完全封闭是做不到的,就算技术上做得到,也需要花费太多的时间和精力。而且让程序符合开发-封闭原则的代价是引入更多的抽象层次,更多的抽象有可能会增加代码的复杂度。

参考材料

曾探:JavaScript设计模式与开发实践