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 = {};但是全局变量存在很多问题,它很容易造成命名空间污染。以下几种方式可以相对降低全局变量带来的命名污染。
使用命名空间
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: {}
}
};使用闭包封装私有变量
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还没有评论
来说两句吧!