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

六月 7, 20269读书笔记JavaScript

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

单例模式

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

简单实现单例模式

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

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

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

// 验证
var a = Singleton.getInstance('sevn1');
var b = Singleton.getInstance('sevn2');

alert(a === b); // true

代理实现单例模式

// 一个普通的创建div的类
var CreateDiv = function(html) {var ProxySingletonCreateDiv = (function() {
  var instance;
  return function(html) {
    if (!instance) {
      instance = new CreateDiv(html);
    }
    return instance;
  }
})();

// 接下来代理ProxySingletonCreateDiv
var a = new ProxySingletonCreateDiv('div1');
var b = new ProxySingletonCreateDiv('div2');

alert(a === b); // true
  this.html = html;
  this.init();
};

CreateDiv.prototype.init = function() {
  var div = document.createElement('div');
  div.innerHTML = this.html;
  document.body.appendChild(div);
};

通过引入代理类的方式,我们完成了一个单例模式的编写,现在我们把负责管理单例的逻辑移到了代理类proxySingletonCreateDiv中。这样一来,CreateDiv就变成了一个普通的类,它跟proxySingletonCreateDiv组合起来可以达到单例模式的效果。

JavaScript 中的单例模式

在 JavaScript 中创建对象的方法非常简单,既然我们只需要一个“唯一”的对象,为什么要为它先创建一个“类”呢?这无异于穿棉衣洗澡,传统的单例模式实现在 JavaScript 中并不适用。
全局变量不是单例模式,但在 JavaScript 开发中,我们经常会把全局变量当成单例来使用。
var a = {};
但是全局变量存在很多问题,它很容易造成命名空间污染。以下几种方式可以相对降低全局变量带来的命名污染。
  1. 使用命名空间

var namespace = {
  a: function() {},
  b: function() {},
}

// 动态创建命名空间
var MyApp = {};
MyApp.namespace = function(name) {
  var parts = name.split('.');
  var current = MyApp;
  for (var i in parts) {
    if (!current[parts[i]]) {
      current[parts[i]] = {};
    }
    current = current[parts[i]];
  }
};

MyApp.namespace('event');
MyApp.namespace('dom.style');

// 等价与
var MyApp = {
  event: {},
  dom: {
    style: {}
  }
};
  1. 使用闭包封装私有变量

var user = (function() {
  var __name = 'sevn',
    __age = 29;
    
   return {
     getUserInfo: function() {
       return __name + '-' + __age;
     }
   }
})();

惰性单例

惰性单例指的是在需要的时候才创建对象实例
基于类的惰性单例实现,在 JavaScript 中并不适用
Singleton.getInstance = (function() {
  var instance = null;
  return function(name) {
     if (!instance) {
       instance = new Singleton(name);
     }
     return instance;
  }
})();
JavaScript 实现惰性单例
// 把如何管理单例的逻辑抽离出来,这些逻辑被封装在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);

document.getElementById('loginbtn').onclick = function() {
  var loginLayer = createSingleLoginLayer();
  loginLayer.style.display = 'block';
};
把创建实例对象的职责和管理单例的职责分别放置在两个方法里,这两个方法可以独立变化而互不影响,当它们连接在一起的时候,就完成了创建唯一实例对象的功能。

策略模式

策略模式的定义是:定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换。
以年终奖的计算为例:很多公司的年终奖是根据员工的工资基数和年底绩效情况来发放的。例如,绩效为S的人年终奖有4倍工资,绩效为A的人年终奖有3倍工资,而绩效为B的人年终奖是2倍工资。假设财务部要求我们提供一段代码,来方便他们计算员工的年终奖。

一般实现

var calculateBonus = function(level, salary) {
  if (level === 'S') {
    return salary * 4;
  }
  
  if (level === 'A') {
    return salary * 3;
  }
  
  if (level === 'B') {
    return salary * 2;
  }
}

calculateBonus('S', 10000);
calculateBonus('B', 10000);

传统面向对象语言的实现

// 一个基于策略模式的程序至少由两部分组成。
// 第一个部分是一组策略类,策略类封装了具体的算法,并负责具体的计算过程。
// 第二个部分是环境类Context, Context接受客户的请求,随后把请求委托给某一个策略类。
// 要做到这点,说明Context中要维持对某个策略对象的引用。

var performanceS = function() {};
performanceS.prototype.calculate = function(salary) {
  return salary * 4;
};

var performanceA = function() {};
performanceA.prototype.calculate = function(salary) {
  return salary * 3;
};

var performanceB = function() {};
performanceB.prototype.calculate = function(salary) {
  return salary * 2;
};

// 定义context,奖金类Bonus
var Bonus = function() {
  this.salary = null;
  this.strategy = null;
}

Bonus.prototype.setSalary = function(salary) {
  this.salary = salary;
};

Bonus.prototype.setStrategy = function(strategy) {
  this.strategy = strategy;
};

Bonus.prototype.getBonus = function() {
  return this.strategy.calculate(this.salary);
};

// 使用
var bonus = new Bonus();

bonus.setSalary(10000);
bonus.setStrategy(new performanceS());
console.log(bonus.getBonus());

bonus.setStrategy(new performanceB());
console.log(bonus.getBonus());

JavaScript 版本的策略模式

实际上在 JavaScript 语言中,函数也是对象,所以更简单和直接的做法是把strategy直接定义为函数
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', 10000));
console.log(calculateBonus('B', 10000));

一等函数对象与策略模式

Peter Norvig在他的演讲中曾说过:“在函数作为一等对象的语言中,策略模式是隐形的。strategy就是值为函数的变量。”
在 JavaScript 中,除了使用类来封装算法和行为之外,使用函数当然也是一种选择。
这些“算法”可以被封装到函数中并且四处传递,也就是我们常说的“高阶函数”。实际上在 JavaScript 这种将函数作为一等对象的语言里,策略模式已经融入到了语言本身当中,我们经常用高阶函数来封装不同的行为,并且把它传递到另一个函数中。当我们对这些函数发出“调用”的消息时,不同的函数会返回不同的执行结果。在 JavaScript 中,“函数对象的多态性”来得更加简单。
var S = function(salary) {
  return salary * 4;
};

var A = function(salary) {
  return salary * 3;
};

var B = function(salary) {
  return salary * 2;
};

var calculateBonus = function(func, salary) {
  return func(salary);
};

calculateBonus(S, 10000);

策略模式优缺点

优点
  • 策略模式利用组合、委托和多态等技术和思想,可以有效地避免多重条件选择语句。
  • 策略模式提供了对开放—封闭原则的完美支持,将算法封装在独立的strategy中,使得它们易于切换,易于理解,易于扩展。
  • 策略模式中的算法也可以复用在系统的其他地方,从而避免许多重复的复制粘贴工作。
  • 在策略模式中利用组合和委托来让Context拥有执行算法的能力,这也是继承的一种更轻便的替代方案。
并不严重的缺点
  • 使用策略模式会在程序中增加许多策略类或者策略对象,但实际上这比把它们负责的逻辑堆砌在Context中要好。
  • 使用策略模式,必须了解所有的strategy,必须了解各个strategy之间的不同点,这样才能选择一个合适的strategy。此时strategy要向客户暴露它的所有实现,这是违反最少知识原则的。

代理模式

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

保护代理和虚拟代理

保护代理:代理B可以帮助A过滤掉一些请求,这种请求就可以直接在代理B处被拒绝掉。这种代理叫作保护代理。
虚拟代理:把一些开销很大的对象,延迟到真正需要它的时候才去创建。

虚拟代理实现图片预加载

不用代理的预加载实现
var MyImage = (function() {
  var imgNode = document,createElement('img');
  document.body.appendChild(imgNode);
  var img = new Image;
  
  img.onload = function() {
    myImage.setSrc(this.src);
  }
  
  return {
    setSrc: function(src) {
      myImage.setSrc('loading.gif');
      img.src = src;
    }
  }
})();

MyImage.setSrc('real.jpg')
单一职责原则指的是,就一个类(通常也包括对象和函数等)而言,应该仅有一个引起它变化的原因。如果一个对象承担了多项职责,就意味着这个对象将变得巨大,引起它变化的原因可能会有多个。面向对象设计鼓励将行为分布到细粒度的对象之中,如果一个对象承担的职责过多,等于把这些职责耦合到了一起,这种耦合会导致脆弱和低内聚的设计。当变化发生时,设计可能会遭到意外的破坏。
上段代码中的 MyImage 对象除了负责给 img 节点设置 src 外,还要负责预加载图片。我们在处理其中一个职责时,有可能因为其强耦合性影响另外一个职责的实现。
虚拟代理实现
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('loading.gif');
      img.src = src;
    }
  }
})();

proxyImage.setSrc('real.jpg');
// 通过proxyImage间接地访问MyImage。proxyImage控制了客户对MyImage的访问,
// 并且在此过程中加入一些额外的操作,比如在真正的图片加载好之前,
// 先把img节点的src设置为一张本地的loading图片

代理和本体接口的一致性

如果有一天我们不再需要预加载,那么就不再需要代理对象,可以选择直接请求本体。
其中关键是代理对象和本体都对外提供了setSrc方法,在客户看来,代理对象和本体是一致的,代理接手请求的过程对于用户来说是透明的,用户并不清楚代理和本体的区别,这样做有两个好处
  • 用户可以放心地请求代理,他只关心是否能得到想要的结果。
  • 在任何使用本体的地方都可以替换成使用代理。
另外,如果代理对象和本体对象都为一个函数(函数也是对象),函数必然都能被执行,则可以认为它们也具有一致的“接口”
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.setSrc(this.src);
  }
  
  return function(src) {
    myImage.setSrc('loading.gif');
    img.src = src;
  }
})();

proxyImage('real.jpg');

缓存代理

缓存代理可以为一些开销大的运算结果提供暂时的存储,在下次运算时,如果传递进来的参数跟之前一致,则可以直接返回前面存储的运算结果。
缓存代理计算乘积
var mult = function() {
  var a = 1;
  for (var i = 0; i < arguments.length; i++) {
    a = a * arguments[i];
  }
  return a;
}

mult(2, 3); // 6
mult(2, 3, 4) // 24

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

proxyMult(1, 2, 3, 4); // 24
proxyMult(1, 2, 3, 4); // 24 不会再次计算
虽然代理模式非常有用,但我们在编写业务代码的时候,往往不需要去预先猜测是否需要使用代理模式。当真正发现不方便直接访问某个对象的时候,再编写代理也不迟。
END

评论

欢迎分享你的看法

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

全部评论

0

还没有评论

来说两句吧!