JavaScript 设计模式学习笔记(三)

六月 7, 20266读书笔记JavaScript

很早之前看《JavaScript 设计模式与开发实践》整理的笔记,把这些笔记搬到这里,仅供个人学习记录使用,如有需要请支持正版图书,侵删。

迭代器模式

迭代器模式是指提供一种方法顺序访问一个聚合对象中的各个元素,而又不需要暴露该对象的内部表示。
迭代器模式可以把迭代的过程从业务逻辑中分离出来,在使用迭代器模式之后,即使不关心对象的内部构造,也可以按顺序访问其中的每个元素。

实现迭代器

var each = function(ary, cb) {
  for (var i = 0; i < ary.length; i++) {
    cb.call(ary[i], i, ary[i]);
  }
};

each([1, 2, 3], function(i, v) {
  alert([i, v]);
});

内部迭代器

上面编写的each函数属于内部迭代器,each函数的内部已经定义好了迭代规则,它完全接手整个迭代过程,外部只需要一次初始调用。
内部迭代器在调用的时候非常方便,外界不用关心迭代器内部的实现,跟迭代器的交互也仅仅是一次初始调用,但这也刚好是内部迭代器的缺点。由于内部迭代器的迭代规则已经被提前规定,上面的each函数就无法同时迭代2个数组了。

外部迭代器

var Interator = 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,
    getCurrentItem: getCurrentItem,
    length: obj.length
  }
};

// 比较两个数组
var compare = function(iter1, iter2) {
  if (iter1.length ! == iter2.length) {
     alert('不相等');
  }
  while(!iter1.isDone() && !iter2.isDone()) {
    if (iter1.getCurrentItem() !== iter2.getCurrentItem()) {
      throw new Error('不相等');
    }
    iter1.next();
    iter2.next();
  }
  alert('相等');
};

倒序迭代器

var reverseEach = function(ary, cb) {
  for (var l = ary.length - 1; l >= 0; l==) {
    cb(l, ary[l]);
  }
};

reverseEach([0, 1, 2], function(i, v) {
   console.log(v); // 2, 1, 0
});

中止迭代器

迭代器可以像普通for循环中的break一样,提供一种跳出循环的方法。
var each = function(ary, cb) {
  for (var i = 0; i < ary.length; i++) {
    if (cb.call(ary[i], i, ary[i]) === false) {
       break; 
    }
  }
};

each([1, 2, 3, 4, 5], function(i, v) {
  if (v > 3) {
    return false;
  }
  console.log(v); // 1, 2, 3
});
应用举例,根据不同的浏览器获取相应的上传组件对象
var getActiveUploadObj = function() {
  try {
    return new ActiveXObject('TXFTNActiveX.FTNUpload');
  } catch(e) {
    return false;
  }
};

var getFlashUploadObject = function() {
  if (supportFlash()) {
    var str = '<object type="application/x-shockwave-flash"></object>';
    return $(str).appendTo($('body'));
  }
  return false;
};

var getFormUploadObj = function() {
  var str = '<input name="file" type="file" class="ui-file" />';
  return $(str).appendTo($('body'));
};

// 1. 提供一个可以被迭代的方法,使得getActiveUploadObj, getFlashUploadObj
// 以及getFlashUploadObj依照优先级被循环迭代。
// 2. 如果正在被迭代的函数返回一个对象,则表示找到了正确的upload对象,
// 反之如果该函数返回false,则让迭代器继续工作。
var iteratorUploadObj = function() {
  for (var i = 0, fn; fn = arguments[i++];) {
    var uploadObj = fn();
    if (uploadObj !== false) {
      return uploadObj;
    }
  }
};

var uploadObj = iteratorUploadObj(getActiveUploadObj, getFormUploadObj, iteratorUploadObj);
我们可以看到,获取不同上传对象的方法被隔离在各自的函数里互不干扰,try、catchif分支不再纠缠在一起,使得我们可以很方便地的维护和扩展代码。比如,后来我们又给上传项目增加了Webkit控件上传和HTML5上传,我们要做的仅仅是下面一些工作。

发布订阅模式

发布—订阅模式又叫观察者模式,它定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知。在JavaScript开发中,我们一般用事件模型来替代传统的发布—订阅模式。
  • 发布—订阅模式可以广泛应用于异步编程中,这是一种替代传递回调函数的方案。
  • 发布—订阅模式可以取代对象之间硬编码的通知机制,一个对象不用再显式地调用另外一个对象的某个接口。

发布订阅实现

var event = {
  clientList: [],
  listen: function(key, fn) {
    if (!this.clientList[key]) {
      this.clientList[key] = [];
    }
    this.clientList[key].push(fn);
  },
  trigger: function() {
    var key = Array.protorype.shift.call(arguments);
    var fns = this.clientList[key];
    if (!fns || fns.length === 0) {
      return false;
    }
    for (var i = 0, fn; fn = fns[i++];) {
      fn.apply(this, arguments);
    }
  },
  remove: function(key, fn) {
    var fns = this.clientList[key];
    if (!fns) {
      return false;
    }
    if (!fn) {
      fns && (fns.length = 0);
    } else {
      for (var l = fns.length - 1; l >= 0; l--) {
        var _fn = fns[l];
        if (_fn === fn) {
          fns.sploce(l, 1);
        }
      }
    }
  }
};

//再定义一个installEvent函数,这个函数可以给所有的对象都动态安装发布—订阅功能
var installEvent = function(obj) {
  for (var i in event) {
     obj[i] = event[i];
  }
};

// 使用
var salesOffices = {};
installEvent(salesOffices);

网站登录

假如我们正在开发一个商城网站,网站里有header头部、nav导航、消息列表、购物车等模块。这几个模块的渲染有一个共同的前提条件,就是必须先用ajax异步请求获取用户的登录信息。
// 之前的实现
login.succ(function(data) {
  header.setAvatar(data.avatar);
  nav.setAvatar(data.avatar);
  message.refresh();
  cart.refresh();
  ...
  address/refresh(); // 新增,不符合开闭原则
});
发布订阅模式
$.ajax('http://xxx.com?login', function(data) {
  login.trigger('loginSucc', data); // 发布登录成功的消息
});

// 各模块监听消息
var header = (function() {
  login.listen('loginSucc', function(data) {
    header.setAvatar(data.avatar);
  });
  return {
    setAvatar: function(data) {
      console.log('设置header模块头像');
    }
  }
})();

var nav = (function() {
  login.listen('loginSucc', function(data) {
    nav.setAvatar(data.avatar);
  });
  return {
    setAvatar: function(data) {
      console.log('设置nav模块头像');
    }
  }
})();
如果有一天在登录完成之后,又增加一个刷新收货地址列表的行为,那么只要在收货地址模块里加上监听消息的方法即可。

全局的发布订阅对象

刚刚实现的发布—订阅模式还存在两个小问题。
  1. 我们给每个发布者对象都添加了listentrigger方法,以及一个缓存列表clientList,这其实是一种资源浪费。
  2. 用户跟售楼处对象还是存在一定的耦合性,用户至少要知道售楼处对象的名字是salesOffices,才能顺利的订阅到事件。
同样在程序中,发布—订阅模式可以用一个全局的Event对象来实现,订阅者不需要了解消息来自哪个发布者,发布者也不知道消息会推送给哪些订阅者,Event作为一个类似“中介者”的角色,把订阅者和发布者联系起来。
var Event = (function() {
  var clientList = {},
    listen,
    trigger,
    remove;
    
  listen = function(key, fn) {
    if (!this.clientList[key]) {
      this.clientList[key] = [];
    }
    this.clientList[key].push(fn);
  };
  
  trigger = function() {
    var key = Array.protorype.shift.call(arguments);
    var fns = this.clientList[key];
    if (!fns || fns.length === 0) {
      return false;
    }
    for (var i = 0, fn; fn = fns[i++];) {
      fn.apply(this, arguments);
    }
  };
  
  remove = function(key, fn) {
    var fns = this.clientList[key];
    if (!fns) {
      return false;
    }
    if (!fn) {
      fns && (fns.length = 0);
    } else {
      for (var l = fns.length - 1; l >= 0; l--) {
        var _fn = fns[l];
        if (_fn === fn) {
          fns.sploce(l, 1);
        }
      }
    }
  };
})();

Event.listen('squareMeter88', function(price) { // 用户订阅消息
  // ...
});

Event.trigger('squareMeter88', 200000); // 售楼处发布消息

订阅模式的便利性

在 JavaScript 中,我们无需去选择使用推模型还是拉模型。
推模型是指在事件发生时,发布者一次性把所有更改的状态和数据都推送给订阅者。
拉模型不同的地方是,发布者仅仅通知订阅者事件已经发生了,此外发布者要提供一些公开的接口供订阅者来主动拉取数据。
拉模型的好处是可以让订阅者“按需获取”,但同时有可能让发布者变成一个“门户大开”的对象,同时增加了代码量和复杂度。
在 JavaScript 中,arguments 可以很方便地表示参数列表,所以我们一般都会选择推模型,使用 Function.prototype.apply 方法把所有参数都推送给订阅者。

优缺点

优点
  • 时间上的解耦
  • 对象之间的解耦
缺点
  • 创建订阅者本身要消耗一定的时间和内存,而且当你订阅一个消息后,也许此消息最后都未发生,但这个订阅者会始终存在于内存中
  • 发布—订阅模式虽然可以弱化对象之间的联系,但如果过度使用的话,对象和对象之间的必要联系也将被深埋在背后,会导致程序难以跟踪维护和理解
  • 有多个发布者和订阅者嵌套到一起的时候,要跟踪一个bug不是件轻松的事情

命令模式

命令模式是最简单和优雅的模式之一,命令模式中的命令(command)指的是一个执行某些特定事情的指令。
命令模式最常见的应用场景是:有时候需要向某些对象发送请求,但是并不知道请求的接收者是谁,也不知道被请求的操作是什么。此时希望用一种松耦合的方式来设计程序,使得请求发送者和请求接收者能够消除彼此之间的耦合关系。
// 假设我们正在编写一个用户界面程序,该用户界面上至少有数十个Button按钮。
// 因为项目比较复杂,所以我们决定让某个程序员负责绘制这些按钮,而另外一些程序员
// 则负责编写点击按钮后的具体行为,这些行为都将被封装在对象里。
var setCommand = function (button, command) {
  buttion.onclick = function () {
    command.execute();
  }
};


var MenuBar = {
  refresh: function () {
    console.log('刷新子菜单');
  }
};


var SubMenu = {
  add: function () {
    console.log('添加子菜单');
  },
  del: function () {
    console.log('删除子菜单');
  }
};


// 把这些行为都封装在命令类中
var RefreshMenuBarCommand = function (receiver) {
  this.receiver = receiver;
};
RefreshMenuBarCommand.prototype.execute = function () {
  this.receiver.refresh();
};

var AddSubMenuCommand = function (receiver) {
  this.receiver = receiver;
};
AddSubMenuCommand.prototype.execute = function () {
  this.receiver.refresh();
};

var DelSubMenuCommand = function (receiver) {
  this.receiver = receiver;
};
DelSubMenuCommand.prototype.execute = function () {
  this.receiver.refresh();
};

// 最后就是把命令接收者传入到command对象中,并且把command对象安装到button上面
var refreshMenuBarCommand = new RefreshMenuBarCommand(MenuBar);
var addSubMenuCommand = new AddSubMenuCommand(subMenu);
var delSubMenuCommand = new DelSubMenuCommand(subMenu);

setCommand(button1, refreshMenuBarCommand);
setCommand(button2, addSubMenuCommand);
setCommand(button3, delSubMenuCommand);
JavaScript 作为将函数作为一等对象的语言,跟策略模式一样,命令模式也早已融入到了 JavaScript 语言之中。运算块不一定要封装在command.execute 方法中,也可以封装在普通函数中。函数作为一等对象,本身就可以被四处传递。
var setCommand = function (button, func) {
  button.onclick = function () {
    func();
  }
};

var MenuBar = {
  refresh: function () {
    console.log('刷新菜单界面');
  }
};

var RefreshMenuBarCommand = function (receiver) {
  return function () {
    receiver.refresh();
  }
};

var refreshMenuBarCommand = RefreshMenuBarCommand(MenuBar);

setCommand(button1, refreshMenuBarCommand);

宏命令

宏命令是一组命令的集合,通过执行宏命令的方式,可以一次执行一批命令。
// 想象一下,家里有一个万能遥控器,每天回家的时候,只要按一个特别的按钮,它就会帮我们关上房间门,顺便打开电脑并登录QQ。
var closeDoorCommand = {
  execute: function () {
    console.log('关门');
  },
};

var openPcCommand = {
  execute: function () {
    console.log('开电脑');
  },
};

var openQQCommand = {
  execute: function () {
    console.log('登录QQ');
  },
};

// 定义宏命令
var MacroCommand = function () {
  return {
    commandsList: [],
    add: function (command) {
      this.commandsList.push(command);
    },
    execute: function () {
      for (var i = 0, command; (command = this.commandsList[i++]); ) {
        command.execute();
      }
    },
  };
};

var macroCommand = MacroCommand();
macroCommand.add(closeDoorCommand);
macroCommand.add(openPcCommand);
macroCommand.add(openQQCommand);

macroCommand.execute();

智能命令与傻瓜命令

var closeDoorCommand = {
  execute: function () {
    console.log('关门');
  },
};

// closeDoorCommand中没有包含任何receiver的信息,它本身就包揽了执行请求的行为,
// 这跟我们之前看到的命令对象都包含了一个receiver是矛盾的
  • 傻瓜命令:命令模式都会在command对象中保存一个接收者来负责真正执行客户的请求,这种情况下命令对象是“傻瓜式”的,它只负责把客户的请求转交给接收者来执行,这种模式的好处是请求发起者和请求接收者之间尽可能地得到了解耦
  • 智能命令: “聪明”的命令对象可以直接实现请求,这样一来就不再需要接收者的存在,这种“聪明”的命令对象也叫作智能命令
没有接收者的智能命令,退化到和策略模式非常相近,从代码结构上已经无法分辨它们,能分辨的只有它们意图的不同。
策略模式指向的问题域更小,所有策略对象的目标总是一致的,它们只是达到这个目标的不同手段,它们的内部实现是针对“算法”而言的。而智能命令模式指向的问题域更广,command 对象解决的目标更具发散性。
END

评论

欢迎分享你的看法

发表评论
欢迎留言,请友好互动
点击添加表情:

全部评论

0

还没有评论

来说两句吧!