Every developer strives to write maintainable, readable, and reusable code.
很多开发人员在学习设计模式的时候都特别的 被动,为了学习设计模式而学习,学完就忘记很少实践在工作中,因此在谈JavaScript设计模式之前,首先我们要明确以下几个问题:
- Context: 什么情况下使用设计模式?
- Problem: 我们要解决的问题是什么?
- Solution: 设计模式是怎么解决这个问题的?
- Implementation: 要怎么实现?
带着这些问题,本文介绍了四种JavaScript开发人员必知必会的设计模式。
Module Design Pattern
模块设计模式与 面向对象(object-oriented) 的语言非常相似,可以将一个模块看作是一个class, 其中优点之一就是 封装的特性,可以提高我们代码的可维护性和易读性,在模块的外部无法操作模块内部的变量,只能通过模块提供的接口来访问。该设计模式是JavaScript最常见的设计模式之一,下面我们来实现该设计模式。
在JavaScript中实现“封装”,我们可以用立即执行函数(IIFE)创建一个闭包的结构
(function() {
// declare private variables and/or functions
return {
// declare public variables and/or functions
}
})();
下面让我们来完整这个设计模式。
var Exposer = (function() {
var privateVariable = 10;
var privateMethod = function() {
console.log('Inside a private method!');
privateVariable++;
}
var methodToExpose = function() {
console.log('This is a method I want to expose!');
}
var otherMethodIWantToExpose = function() {
privateMethod();
}
return {
first: methodToExpose,
second: otherMethodIWantToExpose
};
})();
Exposer.first(); // Output: This is a method I want to expose!
Exposer.second(); // Output: Inside a private method!
Exposer.methodToExpose; // undefined
上面的代码我们定一个来一个模块Exposer
, 我们可以通过该模块提供的接口first
和 second
来操作这个模块而无需关心其内部实现,当试图直接访问其内部变量的时候则是无效的,得到的值是undefined
。很多JavaScript库中都可以看到该设计模式的影子。
Singleton Design Pattern
单例模式,顾名思义只有一个实例。该设计模式的用途也非常的广泛并且实现起来非常简单。比如现在有一个办公室,有五名员工公用一台打印机,那么我们就可以使用单例模式来处理这种情况。
根据前面讨论的模块设计模式,我们先创建一个打印机模块printer
,该模块提供一个外部接口让五名使用者可以访问这台打印机。为了确保这五个人用的是同一台打印机,单例模式实现的关键是: 当有人第一次访问实例时创建打印机实例printerInstance
,并且保存起来。当其他人再次访问该实例时直接返回这个保存起来的实例。
var printer = (function () {
var printerInstance;
function create() {
return {
turnOn: function turnOn() {
console.log('working')
}
}
}
return {
getInstance: function () {
if (!printerInstance) {
printerInstance = create();
}
return printerInstance;
}
}
})()
printer.getInstance().turnOn() // output: working
从上面代码我们可以看到printer
模块提供了一个唯一外部可以访问的接口getInstance
,当第一次访问该接口时,我们先判断实例是否被创建,如果没有创建则使用create()
创建,如果已经创建则返回唯一的实例printerInstance
。大家可以使用单例模式设计一个计数器练练手,定义一个计数器模块,提供增加、减少、查看三个接口。
Observer Design Pattern
观察者模式,当应用的一个部分变动时,其他部分也会更新。比如Model-View-Controller(MVC)架构,当model变化的时候views也会更新。在流行的Web前端Vue、Angular中也是通过观察者模式来通知状态变化的。
实现一个观察者模式至少要包含2个角色如下图UML图中所示: Subject
和Observer
对象 。
下面我们使用JavaScript来实现上图的观察者模式。
首先我们要定义一个Subject
对象,包含以下方法:
- 注册观察者对象
- 卸载观察者对象
- 通知观察者对象
然后我们需要定义一个观察者对象,并且实现通知notify
方法。也就是实现当Subject
对象通知观察者对象时,观察者对象要做什么?
var Observer = function() {
return {
notify: function(index) {
console.log("Observer " + index + " is notified!");
}
}
}
下面让我们实现完整
var Subject = function() {
var observers = [];
return {
subscribeObserver: function(observer) {
observers.push(observer);
},
unsubscribeObserver: function(observer) {
var index = observers.indexOf(observer);
if(index > -1) {
observers.splice(index, 1);
}
},
notifyObserver: function(observer) {
var index = observers.indexOf(observer);
if(index > -1) {
observers[index].notify(index);
}
},
notifyAllObservers: function() {
for(var i = 0; i < observers.length; i++){
observers[i].notify(i);
};
}
};
};
var Observer = function() {
return {
notify: function(index) {
console.log("Observer " + index + " is notified!");
}
}
}
上面的代码我们实现了Subject
对象,在其内部声明了一个observers
数组用来存储注册的observer
对象。下面让我们来使用这两个对象
var subject = new Subject();
var observer1 = new Observer();
var observer2 = new Observer();
var observer3 = new Observer();
var observer4 = new Observer();
subject.subscribeObserver(observer1);
subject.subscribeObserver(observer2);
subject.subscribeObserver(observer3);
subject.subscribeObserver(observer4);
subject.notifyObserver(observer2); // Observer 2 is notified!
subject.notifyAllObservers();
// Observer 1 is notified!
// Observer 2 is notified!
// Observer 3 is notified!
// Observer 4 is notified!
以上我们实现了一个简单的设计者模式,先使用subjectsubscribeObserver(observer)
注册以后要通知的观察者对象,当我们想通知注册好的观察者对象时,只需要使用subject.notifyObserver(observer)
即可
Constructor Pattern
在典型的面向对象语言中,constructor 是一个特殊的初始化方法,在JavaScript中几乎任何东西都是一个对象,我们同样也可以在JavaScript中使用constructor pattern 来初始化对象。
不使用constructor pattern时,我们通常是这样初始化一个对象的:
var Pastry = {
// initialize the pastry
init: function (type, flavor, levels, price, occasion) {
this.type = type;
this.flavor = flavor;
this.levels = levels;
this.price = price;
this.occasion = occasion;
}
}
var cake = Object.create(Pastry);
cake.init("cake", "vanilla", 3, "$10", "birthday");
我们先在对象内部添加一个init
方法,然后在初始化时我们使用Object.create(object)
创建这个对象并调用其init
方法,下面让我们像面向对象语言一样使用new
关键字初始化一个对象通过 constructor pattern。
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);
civic.toString();
上面代码我们用constructor pattern
初始化了2个对象:civic
和mondeo
,我们首先定义了一个函数Car
并在其内部使用关键字this
设置了该对象所需要的参数,此外我们还通过在Car
的prototype
上添加toString
来扩展该对象。
Mixin Pattern
Mixin Pattern 实现了面向对象中的 继承, 继承的好处可以提高代码的复用性,比如我们现在有以下一个对象
function Person(firstname, lastname){
this.firstname = firstname;
this.lastname = lastname;
}
var max = new Person('max', 'lee');
当我们想创建一个新的对象,这个对象比Person
对象多了一个power
的属性
function Superman(firstname, lastname, power){
this.firstname = firstname;
this.lastname = lastname;
this.power = power;
}
var max = new Superman('max', 'lee', 'fly');
我们可以看到 Person
和 Superman
对象基本完全一样,只是Superman
多了一个power
属性, 这样代码重复率很高,为了解决这个问题,我们可以使用 mixmin pattern 实现继承来避免过多的重复代码。
实现 mixmin pattern有2个关键点
- 调用superclass的constructor使用
.call
- 将superclass的prototype赋值给sub-class通过
Object.create(superclass.prototype)
具体实现如下:
function Person(firstname, lastname){
this.firstname = firstname;
this.lastname = lastname;
}
function Superman(firstname, lastname, power){
Person.call(this, firstname, lastname);
this.power = power;
}
Superman.prototype = Object.create(Person.prototype);
var max = new Superman('max', 'lee', 'fly');
Facade Pattern
Facade Pattern是为了让接口更加简单易用的一种设计模式, 比如jQuery用起来就是比原生的方法操作DOM更便捷。该设计模式通常和module pattern 结合使用。
让我们实现一个$
接口来更便捷的操作DOM
var $ = (function () {
return {
query: function(element){
return document.querySelector(element);
}
}
})();
var titleElement = $.query('title'); // <title> title here </title>
Command Pattern
命令模式是把actions封装成对象(command objects),比如我们把计算器的加减乘除这些action封装在object里面
var Calculator = {
// addition function
add: function (num1, num2) {
return num1 + num2;
},
// subtraction function
substract: function (num1, num2) {
return num1 - num2;
},
// multiplication function
multiply: function (num1, num2) {
return num1 * num2;
},
// division function
divide: function (num1, num2) {
return num1 / num2;
},
};
我们可以像Calculator.add(1, 1)
这样来计算加法,但是我们不想增加对象之间的依赖性,以后如果Calculator
里面这些方法改变的时候,那么调用它的地方也都需要改动。那么我们就可以不直接操作Calculator
而是通过“命令”来操作加减乘除。
先定义一个计算接口来接收命令
Calculator.execute = function(command){
return Calculator[command.type](command.num1, command.num2)
}
然后我们就可以这样把命令当作一个参数伴随着要计算的数字传给calc
方法
console.log(Calculator.execute({type: "divide" ,num1:1,num2:6}));
console.log(Calculator.execute({type: "multiply" ,num1:3,num2:5}));
这样以后就算Calculator
的内部方法以后改变了,但是调用提供的接口不需要改变。