什么是模式
模式是一种可复用的解决方案,可用于解决软件设计中遇到的常见问题
什么是反模式
反模式是:
- 描述一种针对某个特定问题的不良解决方案,该方案会导致糟糕的情况发生
- 描述如何摆脱前述的糟糕情况以及如何创造好的解决方案
如果一个”完美的“的设计,应用于错误的上下文中,那么它就可能是一种反模式
设计模式的类别
创建型设计模式
创建型设计模式专注于处理对象创建机制,以适合给定情况的方式来创建对象。这个类别的设计模式包括:Constructor
(构造器),Factory
(工厂),Abstract
(抽象),Prototype
(原型),Singleton
(单例)和 Builder
(生成器)
结构型设计模式
结构型设计模式与对象组合有关,通常可以用于找出在不同对象之间建立关系的简单方法。这种模式有助于确保在系统某一块发生变化时,系统的整个结构不需要同时改变,也就是不需要因为某个特殊目的而去改变整个系统,而是可以将功能进行重组。这个类别的设计模式包括:Decorator
(装饰者),Facade
(外观),Flyweight
(享元),Adapter
(适配器),Proxy
(代理)
行为设计模式
行为模式专注于改善或简化系统中不同对象之间的通信。这个类别的设计模式包括:Iterator
(迭代器),Mediator
(中介者),Observer
(观察者),Visitor
(访问者)
JavaScript 设计模式
开发人员通常想知道他们是否应该在工作中使用一种“理想”的模式或者模式集,这个问题没有明确的唯一答案,我们需要思考的是:模式的哪些方面能够为实现提供实际价值。
总的来说 js 中常见的设计模式有:Constructor
(构造器模式),Prototype
(原型模式),Factory
(工厂模式),Module
(模块模式),Revealing Module
(揭示模块模式),Singleton
(单例模式),Mediator
(中介者模式),Observer
(观察者模式),Decorator
(装饰者模式),Facade
(外观模式),Flyweight
(享元模式),Mixin
(混入模式),Command
(命令模式)
Constructor
(构造器模式)
在经典面向对象编程语言中,Constructor 是一种在内存已经分配给该对象的情况下,用于初始化新创建对象的特殊方法。
JavaScript 不支持类的概念,但它支持与对象一起用的特殊 Constructor 函数,通过在构造器前面加 new 关键字,告诉 JavaScript 可以像使用构造器一样实例化一个新对象,并且对象成员由该函数定义。在构造器内,this 引用新创建的对象。
js 中有一个名为 Prototype 的属性(每个函数都有一个 Prototype 属性,指向自己的原型对象),在调用 Constructor 创建一个对象实例后,对象实例会有一个 __proto__
属性(每个实例都有一个 __proto__
属性)指向构造函数的原型对象。js 中一切皆对象,Object 函数的原型对象的__proto__
最终会指向一个 null
。
Object.prototype.__proto__ === null
在 JavaScript 中内置了一些构造函数,使我们能方便的调用,我们自己也能根据实际需要创建构造函数。es6 的 class
类实际上是构造函数的语法糖,本质上还是一个函数。新的class
写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。class
Module
(模块模式)
模块模式能帮我们清晰地分离和组织项目中的代码单元,在 JavaScript 中实现模块的方式有:
- 对象字面量表示法
- Module 模式
- AMD 模块
- CommonJs 模块
- ECMAScript Harmony 模块
对象字面量
即 {}
包裹的一系列键值对的组合,将变量作为一个对象的属性,有助于封装和组织代码。
const cat = {
name: 'xiaopeng',
type: 'electric'
}
Module 模块模式
在 js 中,Module 模式用于进一步模拟类的概念,通过这种方式,能够使一个单独的对象拥有公有/私有方法和变量,从而屏蔽来自全局作用域的特殊部分。产生的结果是函数名与在页面上其他脚本定义的函数冲突的可能性降低。
- 私有 Module 模式通过闭包封装私有状态和组织。它提供了一种包装混合公有/私有方法和变量的方式,防止其泄漏至全局作用域。使用返回一个对象的立即执行函数可以来模拟这种模式,原理是利用了 js 中的函数作用域来隔离变量。
var testModule = (function(){、
// 私有变量
var counter = 0;
return {
// 公有变量
publicCounter: counter,
// 调用私有变量和方法的公有函数
incrementCounter: function(){
return ++counter;
},
resetCounter:function(){
console.log('counter value prior to reset:'+counter);
counter = 0
}
}
})()
// 增加计数器
testModule.incrementCounter();
testModule.resetCounter();
在这里,代码的其他部分无法直接读取 incrementCounter() 或者 resetCounter()。counter 实际上是完全与全局作用域隔离的,因此它表现的就像是一个私有变量,它的存在被局限在于一个闭包内,因此唯一能够访问其作用域的代码的就是这两个函数。
- 引入混入(Mixin) 可以将一个全局参数传递给模块的匿名函数,这允许我们引入它们,并按照我们所希望的为它们去一个本地别名。
var myModule = (function (jQ){
function privateMethod1(){
jQ(".container").html('test')
}
return {
publicMethod: function(){
return privateMethod1();
}
}
})(jQuery)
AMD
异步模块定义(Async Module Define)的整体目标是提供模块化的 JavaScript 解决方案,以便开发人员使用。AMD 模块格式本身就是对定义模块的建议,其模块和依赖都可以异步加载,具有高度灵活性,消除了代码和模块之间可能惯有的紧耦合。AMD 最开始是 CommonJS 中模块格式的草案规范,但由于他没有达到广泛一致,这种格式的进一步发展就转移到了 amdjs 社区。
AMD 有两个关键概念:用于模块定义的 define 方法和用于处理依赖加载的 require 方法。
define 可以用于定义已命名或者未命名模块:
define(
// 可选
module_id,
// 可选
dependencies,
// 实例化模块或对象的函数
definition function,
)
module_id 是一个可选参数,当遗漏这个参数的时候称这个模块为 匿名 的。dependencies 参数表示我们定义模块所需的依赖数组,第三个参数是用于执行实例化模块的函数。一个准系统模块可以定义如下:
define('myModule',['foo','bar'],function(foo,bar){
var myModule = {
doStuff: function(){
console.log('Yay! Stuff')
}
}
return myModule
})
require 通常用于加载顶级 Javascript 文件或模块的代码,并且可动态获取依赖。
define(function(require){
var isReady = false;
var foobar;
require(['foo','bar'],function(foo,bar){
isReady = true;
foobar = foo()+bar();
})
return {
isReady: isReady,
foobar: foobar
}
})
CommonJS
Common JS 起初在 2009 年由 Kevin dangoor 启动的一个项目中被称为 serverJS,该格式在一个志愿者工作小组 CommonJS 的努力下已变得更加证实话,该小组旨在设计,标准化和规范 JavaScript API。
CommonJS 模块建议指定一个简单的 API 来声明在浏览器外部工作的模块,与 AMD 不同,它试图包含更广泛的引人关注的问题,如 IO,文件系统,promises 等等。
从结构的角度来看, CommonJS 模块是 JavaScript 中的可复用部分,导出特定对象,以便可以用于r任何依赖代码,与 AMD 不同,在这种模块周围通常是没有函数封装器的(所以我们在这里看不见 define)
CommonJS 模块基本上包含两个主要部分:exports(希望其他模块能够使用的对象),require 函数,模块可以使用该函数导入其他模块的导出
var lib = require('package/lib');
function foo(){
lib.log('hello, world!');
}
exports.foo = foo;
AMD 形式:
define(function(){
var lib = require('package/lib');
function foo(){
lib.log('hello,world!')
}
return {
foobar: foo
}
})
Es Harmony
es6 中提出使用 import 关键字指定依赖,使用 export 声明一个外部可见模块的的本地绑定(静态引入),其他模块能够引入它们但不能修改它们。
Revealing 揭示模块模式
揭示模式的代码模版为:
let myRevealingModule = function(){
let privateVar = 'Ben Cherry';
let publicVar = 'Hey there!';
function privateFunction(){
console.log('my name'+privateVar)
}
function publicSetName(name){
privateVar = name
}
function publicGetName(){
privateFunction()
}
return {
setName: publicSetName,
greeting: publicVar,
getName: publicGetName,
}
}
在命名上将私有的方法和变量与公有(暴露给外部调用)的部分区分开。在模版代码底部,很容易能区分出来哪些变量能够被公开访问,提高了可读性
Single 单例模式
单例模式限制了类的实例化次数只能一次。从经典意义上来说,单例模式,在实例不存在的情况下,可以通过一个方法创建一个类来实现创建类的新实例,如果实例已经存在,它会简单返回该对象的引用。 单例模式的适用在这些时候:
- 只能有一个实例,并且需要在很多地方被引用
- 初始化的时候需要一些信息,因此需要被延迟初始化 单例模式代码模版为:
let mySingleton = (function(){
var instance;
function init(){
function privateMethod(){
//...
};
let privateVariable = 'private';
return{
publicMethod: function (){
},
publicProperty: 'public'
}
}
return {
getInstance: function(){
if(!instance){
instance = init()
}
return instance
}
}
})()
Observer 观察者模式
观察者是一种设计模式,与语言无关。基本定义是:
一个或多个观察者对目标的状态感兴趣,它们通过将自己依附在目标对象上以便注册所感兴趣的内容,目标状态发生改变并且观察者可能对这些改变感兴趣,就会发送一个通知消息,调用每个观察者的更新方法,当观察者不再对目标状态感兴趣时,它们可以简单地将自己从中分离。
Subject 目标
维护一系列观察者,方便添加或删除观察者
Observer 观察者
为那些在目标状态发生改变时需获得通知的对象提供一个更新接口
ConcreteSubject 具体目标
状态发生改变时,向 Observer 发出通知,存储 ConcreteObserver 的状态
ConcreteObject 具体观察者
存储一个指向 ConcreteSubject 的引用,实现 Observer 的更新接口,使得自身状态与目标状态保持一致
Publish/Subscribe 发布订阅模式
观察者模式要求希望收到通知的观察者必须订阅内容改变的事件;观察者和目标直接通信。
发布订阅模式使用了一个主题/事件通道,这个通道介于希望接收到通知的对象和激活事件的对象之间,可以避免订阅者和发布者之间产生依赖关系,当任务或活动发生时获得通知,而不是单个对象直接调用其他对象的方法。
var pubsub = {};
(
function(q){
let topic = {};
let subId = -1;
q.publish = function(topic, args){
if(!topics[topic]){
return false
}
let subscribers = topics[topic];
const len = subscribers ? subscribers.length : 0;
while(len--){
subscribers[len].func(topic,args);
}
return this
}
q.subscribe = function(topic,func){
if(!topics[topic]){
topics[topic] = []
}
let token = (++ subUid).toString();
topics[topic].push({
token,
func
})
return token;
}
q.unsubscribe = function(token){
for(let m in topics){
if(topics[m]){
for(let i = 0,j = topics[m].length;i<j;i++){
topics[m].splice(i,1);
return token
}
}
}
return this
}
}
)(pubsub)
//使用
let logger = function(topics,data){
console.log('loging:'+topics + ':'+data)
}
var subscription = pubsub.subscribe('inbox/newMessage',logger);
pubsub.publish('inbox/newMessage','hello,world')
Mediator 中介者模式
Mediator 模式本质上是 Observer 模式的共享目标,它假设该系统中对象或模块之间的订阅和发布关系被牺牲掉了,从而维护中心联络点 实现方式与发布订阅模式类似,只不过是由一个 Mediator 类维护了所有的 topic 以及 callback
Prototype 原型模式
我们可以认为 Prototype 模式是基于原型继承的模式,可以在其中创建对象,作为其他对象的原型。 js 实现原型继承的方式有:
-
Object.create(prototype,OptionalDescriptorObjects) 创建一个以 prototype 为原型对象的对象,将对象的 proto 属性指向 prototype,OptionalDescriptorObjects 可以让我们实现差异继承
-
构造函数的方式
var beget = (function(){
function F(){};
return function(proto){
F.prototype = proto;
return new F()
}
})()
Command 命令模式
Command 模式旨在将方法调用,请求或操作封装到单一对象中,从而根据我们不同的请求对客户进行参数化和传递可供执行的方法调用。这种模式将调用操作的对象与知道该操作的对象解耦,用基于类的编程语言解释具体类是最恰当的,它们与抽象类的思想相关。一个抽象类定义一个接口,但不一定为它所有的成员函数提供实现。他作为基类派生出其他类。实现缺失功能的派生类被称为一个具体的类。例如:
(function (){
const CarManager = {
requestInfo:function(model,id){},
byVehicle: function(model,id){},
arrangeViewing: function(model,id){}
}
})()
这样实现的对同一功能的函数的集合,看起来没什么问题。但假如,其中一个核心 API 的名字发生变化时,就需要在每个使用到的地方都去修改一便,因此我们可以提供一个执行操作的API:
CarManager.excute = function(name){
return CarManager[name] && CarManager[name].apply(CarManager,[].slice.call(arguments,1))
}
将调用函数的名字作为参数传入到 excute 函数中,间接调用执行