JavaScript常用设计模式

设计模式(design pattern)是对软件设计中普遍存在的各种问题,所提出的解决方案。是某种场景下解决问题的范式,因此掌握设计模式对于一个 Programmer 来说非常重要。本文介绍了几种常见的设计模式。

原型模式(The Prototype Pattern)

{.line-numbers}
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
27
28
// 最直接的方法,使用 "hasOwnProperty" 时无法遍历原型对象
var vehicle = {
getModel: function () {
console.log( "The model of this vehicle is.." + this.model );
}
};

var car = Object.create(vehicle, {
"id": {
value: MY_GLOBAL.nextId(),
enumerable: true
},
"model": {
value: "Ford",
enumerable: true
}
});

// 优化后的方法
var beget = (function () {
function F() {}
return function (proto) {
F.prototype = proto;
return new F();
};
})();

var car = beget(vehicle);
  • 优点:

    • JavaScript 本身提供的原型优势;
    • 所有对象实例共享原型中的方法,可以带来性能提升。
  • 缺点:

    • 优点2即是原型模式的缺点

构造器模式(The Constructor Pattern )

{.line-numbers}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function Car(model, year, miles) {
this.model = model;
this.year = year;
this.miles = miles;
}

// 使用原型对象定义方法,避免每次实例都要重新定义同一个方法
Car.prototype.toString = function () {
return this.model + " has done " + this.miles + " miles";
};

var civic = new Car("Honda Civic", 2009, 20000);
var mondeo = new Car("Ford Mondeo", 2010, 5000);
console.log(civic.toString());
console.log(mondeo.toString());
  • 优点:

    • 每个实例的公共对象都是不同的,不会相互影响。
  • 缺点:

    • 优点1即是构造器模式的缺点。

模块模式(The Module Pattern )

{.line-numbers}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var myNamespace = (function () {
// 私有成员
var myPrivateVar, myPrivateMethod;

myPrivateVar = 0;

myPrivateMethod = function(foo) {
console.log(foo);
};

return {
// 公有成员
myPublicVar: "foo",
myPublicFunction: function(bar) {
myPrivateVar++;
myPrivateMethod(bar);
}
};
})();
  • 优点:

    • 比真正封装的想法更加清晰;
    • 支持私有变量、私有方法。
  • 缺点:

    • 无法在以后添加到对象的方法中访问私有成员;
    • 当更改可见性时,必须对使用该成员的每个位置进行更改;
    • 无法为私有成员创建自动单元测试;
    • 当错误需要热修复时的额外复杂性。

揭示模块模式(The Revealing Module Pattern )

{.line-numbers}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var myRevealingModule = (function () {
var privateVar = "Ben Cherry",
publicVar = "Hey there!";

function privateFunction() {
console.log( "Name:" + privateVar );
}

function publicSetName( strName ) {
privateVar = strName;
}

function publicGetName() {
privateFunction();
}

return {
setName: publicSetName,
greeting: publicVar,
getName: publicGetName
};
})();
myRevealingModule.setName( "Paul Kinlan" );
  • 优点:

    • 使脚本的语法更加一致;
    • 使模块结尾处的内容更加清晰,增强了可读性;
  • 缺点:

    • 无法修改被私有方法引用的公共函数。

单例模式(The Singleton Pattern )

{.line-numbers}
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
27
28
29
30
31
var SingletonTester = (function () {
function Singleton(options) {
options = options || {};
this.name = "SingletonTester";
this.pointX = options.pointX || 6;
this.pointY = options.pointY || 10;
}

// 实例容器
var instance;

// 模拟静态属性和方法
var _static = {
name: "SingletonTester",

// 返回单例模式的实例
getInstance: function(options) {
if(instance === undefined) {
instance = new Singleton(options);
}
return instance;
}
};
return _static;
})();

var singletonTest = SingletonTester.getInstance({
pointX: 5
});

console.log( singletonTest.pointX ); // Outputs: 5
  • 优点:

    • 提供了对唯一实例的受控访问;
    • 相比于静态类(或对象),单例模式可以延迟初始化;
    • 对于需要频繁创建和销毁的对象,可以避免对资源的多重占用,节约系统资源,提高效率;
  • 缺点:

    • 单例模式一般没有接口,扩展困难;
    • 不适用于变化的对象。

观察者模式(The Observer Pattern )

观察者组件:

{.line-numbers}
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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
// 通过主体维护的观察者列表
function ObserverList() {
this.observerList = [];
}

ObserverList.prototype.add = function(obj) {
return this.observerList.push(obj);
};

ObserverList.prototype.count = function() {
return this.observerList.length;
};

ObserverList.prototype.get = function(index) {
if(index > -1 && index < this.observerList.length) {
return this.observerList[index];
}
};

ObserverList.prototype.indexOf = function(obj, startIndex) {
var i = startIndex;
while(i < this.observerList.length) {
if(this.observerList[i] === obj) {
return i;
}
i++;
}
return -1;
};

ObserverList.prototype.removeAt = function(index) {
this.observerList.splice(index, 1);
};

// 主体
function Subject() {
this.observers = new ObserverList();
}

Subject.prototype.addObserver = function(observer) {
this.observers.add(observer);
};

Subject.prototype.removeObserver = function(observer) {
this.observers.removeAt(this.observers.indexOf(observer, 0));
};

Subject.prototype.notify = function(context) {
var observerCount = this.observers.count();
for(var i=0; i < observerCount; i++){
this.observers.get(i).update(context);
}
};

// 观察者
function Observer() {
this.update = function() {
// ...
};
}

观察者实例(Vue.js 的双向绑定实现):

{.line-numbers}
1
2
3
4
<div id="app">
<input type="text" name="">
<p class="log"></p>
</div>
{.line-numbers}
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
var oApp = document.getElementById('app');
var oInput = oApp.children[0];
var oLog = oApp.children[1];

// 扩展对象
function extend(obj, extension){
for ( var key in extension ){
obj[key] = extension[key];
}
}

// 注册输入框为主体
extend(oInput, new Subject());
// 指定广播事件
oInput.oninput = function(message) {
this.notify(this.value);
}

// 注册段落为观察者
extend(oLog, new Observer());
// 指定观察者更新事件
oLog.update = function(message) {
this.innerHTML = message;
}
// 添加观察者到主体
oInput.addObserver(oLog);
  • 优点:

    • 观察者模式对于在应用程序设计中解耦许多不同的场景非常有用;
    • 观察者模式可以在不使类紧密耦合的情况下保持相关对象之间的一致性;
    • 观察者和主体之间可以存在动态关系,这提供了很大的灵活性。
  • 缺点:

    • 观察者和主体相关联,具有依赖关系,不利于应用的解耦。

发布/订阅模式(The Publish/Subscribe Pattern)

作为观察者模式的变体,其与观察者模式的不同在于:

  • 订阅者不需要绑定发布者;
  • 允许任何订阅者注册和接收发布者的广播。

发布订阅组件:

{.line-numbers}
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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
var pubsub = {};

(function(myObject) {
// 广播/监听的主题
var topics = {};

// 主题标识符
var subUid = -1;

// 发布特定主题的广播
myObject.publish = function(topic, args) {
if (!topics[topic]) {
return false;
}

var subscribers = topics[topic],
len = subscribers ? subscribers.length : 0;

while (len--) {
subscribers[len].func(topic, args);
}

return this;
};

// 订阅特定主题事件并调用回调函数
myObject.subscribe = function(topic, func) {
if (!topics[topic]) {
topics[topic] = [];
}

var token = (++subUid).toString();
topics[topic].push({
token: token,
func: func
});

return token;
};

// 根据订阅的 token 实现特定主题订阅的取消
myObject.unsubscribe = function(token) {
for (var m in topics) {
if (topics[m]) {
for (var i = 0, j = topics[m].length; i < j; i++) {
if (topics[m][i].token === token) {
topics[m].splice(i, 1);
return token;
}
}
}
}
return this;
};
}(pubsub));

发布订阅实例:

{.line-numbers}
1
2
3
4
5
<div id="app">
<input type="text" name="">
<p class="log"></p>
<p class="log"></p>
</div>
{.line-numbers}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var oApp = document.getElementById('app');
var oInput = oApp.children[0];
var oLog_1 = oApp.children[1];
var oLog_2 = oApp.children[2];

// 新建一个主题为“输入事件”的订阅者,执行回调a
var subscriber = pubsub.subscribe("event_input", function(topic, data) {
oLog_1.innerHTML = data;
});

// 新建一个主题为“输入事件”的订阅者,执行回调b
var subscriber = pubsub.subscribe("event_input", function(topic, data) {
oLog_2.innerHTML = data.split("").reverse().join("");
});

// 发布者,发布主题为“输入事件”的广播
oInput.oninput = function() {
pubsub.publish("event_input", this.value);
}
  • 优点:

    • 发布订阅模式有效地将应用程序分解为更小,更松散耦合的块,以改善代码管理和重用;
    • 发布者和订阅者之间不存在依赖,一个发布者可以绑定多个不同类型的订阅者。
  • 缺点:

    • 由于订阅者和发布者之间的动态关系,更新依赖性可能难以跟踪,同时也不方便排查错误。

工厂模式(The Factory Pattern)

{.line-numbers}
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
27
28
29
30
31
32
33
34
35
36
37
38
39
// 定义汽车的构造函数
function Car(options) {
this.doors = options.doors || 4;
this.state = options.state || "brand new";
this.color = options.color || "silver";
}

// 定义货车的构造函数
function Truck( options){
this.state = options.state || "used";
this.wheelSize = options.wheelSize || "large";
this.color = options.color || "blue";
}

// 定义一个基本的交通工具工厂
function VehicleFactory() {}
VehicleFactory.prototype.vehicleClass = Car;
VehicleFactory.prototype.createVehicle = function (options) {
switch(options.vehicleType){
case "car":
this.vehicleClass = Car;
break;
case "truck":
this.vehicleClass = Truck;
break;
}

return new this.vehicleClass(options);
};

var carFactory = new VehicleFactory();

// 使用交通工具工厂生成一个汽车实例
var car = carFactory.createVehicle({vehicleType: "car", color: "yellow", doors: 6});
console.log(car instanceof Car); // true

// 使用交通工具工厂生成一个货车实例
var movingTruck = carFactory.createVehicle({vehicleType: "truck", state: "like new", color: "red", wheelSize: "small" });
console.log( movingTruck instanceof Truck ); // true
  • 优点:

    • 当对象或组件设置涉及高度复杂性时,有利于降低使用的复杂度;
    • 可以根据所处的环境轻松生成不同的对象实例;
    • 有利于处理许多共享相同属性的小对象或组件;
    • 对于需要调用不同实例的对象可以提供一个统一的入口,方便解耦。
  • 缺点:

    • 可能给应用程序带来不必要的大量复杂性;
    • 由于对象创建过程被抽象化,对于复杂度过大的对象,可能导致单元测试的问题。

抽象工厂模式(The Abstract Factory Pattern)

相比于工厂模式,抽象工厂将一组对象的实现细节与其一般用法分开。适用于系统必须独立于生成对象的方式,或者需要使用多种类型的对象的场景。

{.line-numbers}
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
27
var abstractVehicleFactory = (function () {
var types = {};

return {
getVehicle: function (type, customizations) {
var Vehicle = types[type];
return (Vehicle ? new Vehicle(customizations) : null);
},

registerVehicle: function (type, Vehicle) {
var proto = Vehicle.prototype;

if (proto.drive && proto.breakDown ) {
types[type] = Vehicle;
}

return abstractVehicleFactory;
}
};
})();

abstractVehicleFactory.registerVehicle("car", Car);
abstractVehicleFactory.registerVehicle("truck", Truck);

var car = abstractVehicleFactory.getVehicle("car", {color: "lime green", state: "like new" });

var truck = abstractVehicleFactory.getVehicle("truck", {wheelSize: "medium", color: "neon yellow"});

混入模式(The Mixin Pattern)

{.line-numbers}
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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// 一个简单的构造函数
var Car = function (settings) {
this.model = settings.model || "no model provided";
this.color = settings.color || "no colour provided";
};

// Mixin
var Mixin = function () {};
Mixin.prototype = {
driveForward: function () {
console.log("drive forward");
},
driveBackward: function () {
console.log("drive backward");
},
driveSideways: function () {
console.log("drive sideways");
}
};

// 使用另一个对象扩展现有对象方法
function augment(receivingClass, givingClass) {
if (arguments[2]) { // 只提供特定方法
for (var i = 2, len = arguments.length; i < len; i++) {
receivingClass.prototype[arguments[i]] = givingClass.prototype[arguments[i]];
}
} else { // 提供所有方法
for (var methodName in givingClass.prototype) {
// 检查重名方法
if (!Object.hasOwnProperty.call(receivingClass.prototype, methodName)) {
receivingClass.prototype[methodName] = givingClass.prototype[methodName];
}
// 可选:检查原型链
// if (!receivingClass.prototype[methodName]) {
// receivingClass.prototype[methodName] = givingClass.prototype[methodName];
// }
}
}
}

// 扩展 Car 构造函数以包含 "driveForward" 和 "driveBackward" 方法
augment(Car, Mixin, "driveForward", "driveBackward");
var myCar = new Car({model: "Ford Escort", color: "blue"});
myCar.driveForward(); // drive forward
myCar.driveBackward(); // drive backward

// 扩展 Car 构造函数以包含所有 Mixin 方法
augment(Car, Mixin);
var mySportsCar = new Car({model: "Porsche", color: "red"});
mySportsCar.driveSideways(); // drive sideways
  • 优点:

    • 有助于减少功能重复并同时增加系统中的功能重用。
  • 缺点:

    • 有可能导致原型污染和原函数的不确定性。

参考资料

  1. 这本书有对应的中文版,但是不推荐阅读中文版,因为翻译糟糕不易于理解。

JavaScript常用设计模式
https://infiniture.cn/2019/09/30/JavaScript常用设计模式/
作者
NickHopps
发布于
2019年9月30日
许可协议