更多文章参见: https://github.com/elevenbeans/elevenbeans.github.io

前言

本系列可以看作是我个人对于 Addy Osmani 的著作《Learning JavaScript Design Patterns》中内容的提炼,类似阅读笔记,目的是为了简单快速、又不失全面地了解设计模式的相关概念、特点、分类及其在 Javascript 中的实际用法。

分为五篇:

  1. 概述
  2. 创建型
  3. 结构型设计模式(本篇)
  4. 行为型
  5. MV*(待补充)

结构型设计模式

1. Decorator(装饰者) 模式

Decorator 模式修改现有类创建的对象,为其添加额外功能。
比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// vehicle 构造函数
function vehicle(type) {
// 默认
this.vehicleType = type || 'car';
this.model = 'default';
this.setModel = function(modelName) {
this.model = modelName;
};
}
var car = new vehicle('car');
console.log(car); // vehicleType: car, model: default, ...
// 装饰一个 vehicle 实例为 truck
var truck = new vehicle('truck');
truck.setColor = function(color) { // 新方法
this.color = color;
}
// 验证
truck.setColor('blue');
console.log(truck); // vehicleType: truck, model: default, color: blue, ...
// vehicle 类本身不受影响
var secondCar = new vehicle('car');
console.log(secondCar); // vehicle: car, model: default

上述例子是为原对象新增方法,同样也可以修改实例中现有的方法:

1
2
3
4
5
6
7
var thirdCar = new vehicle('truck');
thirdCar.setModel = function(modelType){ // 修改现有方法
this.model = modelType + '-truck';
};
thirdCar.setModel('CAT');
console.log(thirdCar); // vehicle: car, model: CAT-truck, ...

优点:使用透明 && 灵活,随心所欲的改造成自己想要的心对象,不用担心对原对象类的污染。

缺点:引入了太多小型而又类似的对象,数量一单太多,不方便管理。

2. Facade(外观) 模式

Facade 模式核心是为更复杂的代码逻辑提供一个方便的高层次入口。尽管支持广泛的行为,但实用的方法只有一个相同的 “外观”。

在 jQuery 中,这样的例子特别多。比如,一个相同的外观下 get 和 set 功能都予以囊括。

  • $(element).css(): *Get the value of a computed style property for the first element in the set of matched elements or set one or more CSS properties for every matched element.*
  • val(): *Get the current value of the first element in the set of matched elements or set the value of every matched element.**

当然,还有 $(document).ready(), 发散一下

优点:使用简单

缺点:抽象存在的隐形成本,比如性能啊。

3. Flyweight(享元) 模式

首先,Flyweight(享元)模式的目标是:在对象之间,尽可能多的共享数据来减少开销。

在 Javascript 中,Flyweight(享元)模式主要有两种应用方式。

1. 数据中的应用

区分系统的内部数据和外部数据。具有相同内部数据的对象可以被替换成工厂方法创建的单一共享对象(具体可以参考 前一篇 中提到的 Singleton 模式),极大减少我们存储数据的总量。然后用管理器来处理外部状态。

例如,设计一个图书馆的 Book 类:

1
2
3
4
5
6
7
var Book = function(id, title, author, genre, pageCount,publisherID, ISBN, checkoutDate, checkoutMember, dueReturnDate, availability) {
this.id = id;
this.title = title;
...
this.checkoutDate = checkoutDate;
this.availability = availability;
}

内部状态有:title、author 等。
外部状态:checkoutMember、checkoutDate 等

1
2
3
4
5
6
7
8
9
10
var Book = function(id, title, author, genre, pageCount,publisherID, ISBN) {
this.id = id;
this.title = title;
...
this.ISBN = ISBN;
}
Book.prototype = {
// ...
}

外部状态已删除,统一交由管理器管理。

2. Dom 中的应用

这个也很好理解,举个例子:事件代理,懂的可以直接略过。

假设有一个 UL 的父节点,包含了很多个 Li 的子节点:

1
2
3
4
5
6
7
<ul id="list">
<li id="li-1">Li 1</li>
<li id="li-2">Li 2</li>
<li id="li-3">Li 3</li>
<li id="li-4">Li 4</li>
<li id="li-5">Li 5</li>
</ul>

当某个Li被点击的时候需要触发相应的处理事件。我们通常的写法,是为每个Li都添加一些类似onClick之类的事件监听。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function addListenersLi(liElement) {
liElement.onclick = function clickHandler() {
//TODO
};
liElement.onmouseover = function mouseOverHandler() {
//TODO
}
}
window.onload = function() {
var ulElement = document.getElementById("list");
var liElements = ulElement.getElementByTagName("Li");
for (var i = liElements.length - 1; i >= 0; i--) {
addListenersLi(liElements[i]);
}
}

如果这个UL中的Li子元素会频繁地添加或者删除,我们就需要在每次添加Li的时候都调用这个addListenersLi方法来为每个Li节点添加事件处理函数。这会造成添加或者删除过程的复杂度和出错的可能性。

解决问题方法是使用事件代理机制,当事件被抛到更上层的父节点的时候,我们通过检查事件的目标对象(target)来判断并获取事件源Li

1
2
3
4
5
6
7
8
9
// 获取父节点,并为它添加一个 click 事件
document.getElementById("list").addEventListener("click",function(e) {
// 检查事件源 e.targe 是否为Li
if(e.target && e.target.nodeName.toUpperCase == "LI") {
//
//TODO
console.log("List item ",e.target.id," was clicked!");
}
});

4. Adapter(适配器) 模式

适配器模式很像 Facade(外观) 模式。她们都要对别的对象进行包装并改变其呈现的接口。

二者的区别在于:

Facade 展现的是一个简化的接口,支持广泛的行为,但并不提供额外的选择,而且有时为了方便完成任务她还会做出一些预设。

Adapter 适配器则是简单的把一个接口转换为另一个接口,她并不滤除某些能力,也不会简化接口

假设有一个对象,还有一个以三个字符串为参数的函数:

1
2
3
4
5
6
7
8
var clientObject = {
string1: 'foo',
string1: 'bar',
string1: 'baz'
};
function interfaceMethod(str1, str2, str3) {
console.log('ehhe');
}

为了把clientObject作为参数传递给interfaceMethod,需要用到适配器。如下:

1
2
3
function clientToInterfaceAdapter(o){//适配器
interfaceMethod(o.string1, o.string2, o.string3);
}

适配器适用于客户系统期待的接口与现有 API 提供的接口不兼容这种场合。她只能用来协调语法上的差异问题。适配器所适配的两个方法执行的应该是类似的任务,否则她就解决不了问题。

优点:避免大规模改写现有客户代码

缺点:如果现有API还未定形,或者新接口还未定形,那么适配器可能不会一直管用。

5. Proxy(代理) 模式

代理模式可以通俗的解释为:在 source 到 target 之间通过一道中间层进行作业的方式。

通过代理模式,从而可以操作真实对象,同时代理对象提供与真实对象相同的接口以便在任何时刻都能代替真实对象。

jQuery 中 JQuery.proxy(function,context) 有实现方法;官方 API

ECMAScript 6 中已经有了具体实现:ES6 Proxy

结构型设计模式汇总介绍到此结束,持续关注请 Star and Watch This github repo, 谢谢 :)