设计模式
设计模式
作用:针对特定问题的简洁/优雅的解决方案,将变化封装,提升 复用性、可维护性。
注意:
- 设计模式的滥用,导致不该使用的地方使用,适得其反
- 将可变的部分封装后,剩下的就是不变和稳定的部分
- 开发阶段可能会增加代码量,但好的设计模式能明显降低后期维护成本
- 模式应该用在正确的地方,设计模式是在发展的,不同语言特征下设计模式存在差异
关注模式的意图而不是结构,只有在具体的环境中才有意义
1995 年最初被提出时有 23 种设计模式是针对 cpp、java 等静态类型、传统面向对象编程语言设计,而在 JS 中有些模式可能已 经不再需要,而有些模式的实现会有变化。比如有些人为了实现 JS 版的工厂模式,生硬的将创建对象的步骤延迟到子类中,实际上在静态类 型语言中让子类来“决定”创建对象的原因是为了迎合“依赖倒置”原则,解开对象类型的耦合,让对象表现出多态性,而在 JS 这种动态类型语言中,多态是天生的, JS 不存在类型耦合,不需要将对象延迟到子类中创建,所以 JS 其实是不需要工厂模式的。应用设计模式是为了解决问题,像上述这种牵强的应用只会让人觉得设计模式既难懂又没什么用,影响设计模式在 JS 中的发展。这些年有更多的模式被发现并总结了出来,比如有些 JS 书籍会提到模块模式、沙箱模式,这些模式能否经受住时间考验还有待验证。
行为型模式
行为型设计模式通常用来解耦,它是对在不同的对象之间划分责任和算法的抽象化,行为型模式不仅仅关注类和对象的结构,而且重点关注它们之间的相互作用;行为型模式一共有以下 11 种
- 模板方法模式(Template Method)
- 策略模式(Strategy)
- 命令模式(Command)
- 中介者模式(Mediator)
- 观察者模式(Observer)
- 迭代器模式(Iteratior)
- 访问者模式(Visiter)
- 责任链模式(Chain of Responsibility)
- 备忘录模式(Memento)
- 状态模式(State)
- 解释器模式(Interpreter)
策略模式
针对一组算法,将每一个算法封装到具有共同接口的独立的类中,使得它们可以互换。

观察者模式
定义:观察者模式定义了对象间的一种
一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知,并自动更新。观察者模式属于行为型模式。
理解:一个目标对象对应多个观察者,当目标对象发生改变时,就通知所由的观察者
步骤
- 在目标对象中维护了一个观察者的数组,新增时将观察者向数组中 push;
- 然后通过 notify 通知所有的观察者;
- 每个观察者只有一个 update 函数,用来接收观察者更新后的一个回调;
观察者模式把观察者对象维护在目标对象中的,需要发布消息时直接发消息给观察者。在观察者模式中,目标对象本身是知道观察者存在的
- js
// 定义一个目标对象 class Subject { constructor() { this.Observers = []; } add(observer) { //添加 this.Observers.push(observer); } remove(observer) { //移除 this.Observers.filter((item) => item === observer); } notify() { //通知所有观察者 this.Observers.forEach((item) => { item.update(); }); } } //定义观察者对象 class Observer { constructor(name) { this.name = name; } update() { console.log(`my name is:${this.name}`); } } let sub = new Subject(); let obs1 = new Observer("observer11"); let obs2 = new Observer("observer22"); sub.add(obs1); // 添加到观察者队列 sub.add(obs2); sub.notify(); // 触发所有的观察者
设计模式
订阅发布模式
发布订阅模式是基于一个事件(主题)通道,希望接收通知的对象
Subscriber通过自定义事件订阅主题,被激活事件的对象Publisher通过发布主题事件的方式通知各个订阅该主题的Subscriber对象。
- 与观察者模式的不同:增加了第三方即
事件中心;目标对象状态的改变并非直接通知观察者,而是通过第三方的事件中心来派发通知。 - 订阅者订阅主题,发布者推送某个主题时,订阅该主题的所有读者都会被通知到;避免了观察者模式无法进行过滤筛选的缺陷。
- 而发布/订阅模式中,发布者并不维护订阅者,也不知道订阅者的存在,所以也不会直接通知订阅者,而是通知调度中心,由调度中心
(公用的对象实例)通知订阅者。 - 例:
- Vue2 中的模板语法,实现数据与界面的双向绑定,数据与页面相互更新
单例模式
单例就是保证一个类只有一个实例,并提供一个访问它的全局访问点
- 实现方法
- 一般是先判断实例存在与否,如果存在直接返回,
- 如果不存在就创建了再返回,这就确保了一个类只有一个实例对象。
- 在 JavaScript 里,单例作为一个命名空间提供者,从全局命名空间里提供一个唯一的访问点来访问该对象
- 使用场景
- 团队开发中,可能会产生命名冲突,这时候单例模式就能很好的解决这个问题。
- 全局模态框,同一时间只允许弹出一个模态框,内容可以不一样,但都是同一个框,elementUI
代理模式
为一个对象提供一个代用品或者占位符,以便控制对它的访问
- 常用虚拟代理形式:某一个花销很大的操作,可以使用虚拟代理的方式延迟到需要它的时候再去创建它,(虚拟代理实现图片懒加载)
- 图片懒加载方式,使用 loading 图占位,然后通过异步的方式加载图片,等图片记载好了再替换掉
中介者模式
- 通过抽离配置项,进行代码的控制,实现各个事件的解耦,仅仅维护好中介者就行了
装饰器模式
- 不改变原对象的基础上,在程序运行时给对象动态的添加方法。
JS设计模式
参考《JavaScript 设计模式》2015年8月第一版,作者:张容铭
面向对象编程
避免污染全局
将多个
函数/变量放在一个变量(对象)中保存,减少覆盖或被覆盖的风险jslet Obj1 = {} Obj.Fn1 = function(){} // 对象的另一种形式:js中函数也是对象 let Obj2 = function(){} Obj2.Fn1 = function(){ ... } Obj2.Fn2 = function(){ ... } // 为方便复用,通过函数返回值的形式创建,每次调用都是全新的,使用上互不影响 let Obj3 = function(){ return { Fn1:function(){}, Fn2:function(){}, } } let a = Obj3(); a.Fn1(); // 真正意义上的类,通过new关键字创建 let Obj4 = function(){ this.fn1=function(){...} this.fn2=function(){...} } let a = new Obj4(); // 这样每次new创建都会产生一个新的对象,但this.创建方法会造成很多开销 // 优化:利用原型链的继承关系,创建一次供后代持续使用 Obj4.prototype.fn3=function(){} // 或者 obj4.prototype={ fn4:function(){}, fn5:function(){} } // 注意:以上两种方法不要混合使用,避免覆盖之前的赋值
链式编程
// 链式调用的核心,就是函数在执行完成后,返回了this,即当前对象
let Obj = {
fn1:function(){
... return this
}
fn2:function(){
... return this
}
}
obj.fn1().fn2()
// 放到类的原型上时使用,需要先new一下再使用
let Obj2 =function(){}
Obj2.prototype = {
fn1:function(){
... return this
}
fn2:function(){
... return this
}
}
let obj2= new Obj2()
obj2.fn1().fn2()利用原始类型
// 对 Function的原型进行操作,就可以让所有的函数都具有某个方法/属性
// 但依旧保持不能污染其他人使用的原则,可以单独创建一个使用
Function.prototype.addMethod=function(name,fn){
this[name]=fn
return this // 返回this,可以实现链式添加
}
// 创建属于自己的函数
var methods = new Function()
methods.addMethod("checkName",function(){
...
return this // 每个添加的函数都返回this,可实现方法的链式调用
})类
知识卡:
- 通过this添加的属性方法与prototype添加的属性方法有什么不同?
- this定义的属性或方法是该对象自身拥有,每次new新的对象都会重新创建
- prototype继承的属性在创建新的对象时,不会再次创建
- 概念:
- 私有属性/方法:由于js函数作用域,声明在函数内部的属性和方法外界访问不到(
类.属性也拿不到)- 共有属性/方法:在函数内部通过this创建的属性和方法 或 通过类通过prototype创建的属性/方法,在new创建对象时都会拥有一份并且可以被外界访问
- 特权方法:通过this创建的方法,可以访问到函数内部的 私有属性和私有方法!
- 静态共有属性/方法:在函数外部通过点语法创建的属性/方法,在new创建对象时并没有被执行到,new创建的对象也无法获取他们,但能通过类来使用。
- 静态私有属性/方法:通常借助闭包实现,
- 构造器:在new创建对象时,调用特权方法初始化实例对象的一些属性,因此也叫类的构造器
- new关键字:通过new关键字创建对象,实质是对新对象this的不断赋值,并将prototype指向类的prototype所指向的对象。
- 总结:
- 静态-new创建的对象中无法通过this访问
- 私有-在类的外部无法直接访问但可以借助实例的特权方法操控
- 共有-this或prototype创建的属性/方法,在创建的实例中可以被访问
// 1.无静态私有方法/属性时:
var Book = function(id,name,price){
// 私有属性
var num = 1
// 私有方法
function checkId(){}
// 公有属性
this.id = id
// 特权方法-可以操作私有属性/方法 也是公有方法
this.getName = function(){}
this.setName = function(){}
this.getPrice = function(){}
this.getPrice = function(){}
// 其他公有方法
this.copy = function(){}
// 构造器-new创建实例时可以初始化对象的一些属性
this.setName(name)
this.setPrice(price)
}
// 静态公有属性(new创建的对象无法访问)
Book.isChinese = true
// 静态公有方法(new创建的对象无法访问)
Book.restTime = function(){}
Book.prototype={
// 公有属性
isJsBook:false,
// 公有方法
display:function(){}
}
// 测试:
var b = new Book(11,"我的心只悲伤七次",66)
console.log(Book.num , Book.isChinese) // undefined true
console.log(b.num) // undefined
console.log(b.isJsBook) // false
console.log(b.id) // 11
console.log(b.isChinese) // undefined// 2.借助闭包实现,实现静态私有属性/方法
let Book = (function(){
// 静态私有变量
var BookNum = 0
// 静态私有方法
function checkBook(name){}
// 创建类
function _book(newId,newName,newPrice){
// 私有变量
var name,price
// 私有方法
function checkId(){}
// 特权方法-可以操作私有属性/方法 也是公有方法
this.getName = function(){}
this.setName = function(){}
this.getPrice = function(){}
this.getPrice = function(){}
//公有属性
this.id=newId
//公有方法
this.copy=function(){}
bookNum++
if(bookNum>100){ ... }
// 构造器
this.setName(name);
this.setPrice(price);
}
// 构建原型
_book.prototype={
isJsBook:false, // 静态公有方法
display:function(){} // 静态公有属性
}
return _book; // 返回类
})()安全模式-检察长
- 在使用函数类的时候容易出现 创建实例时 遗忘 new关键字而导致问题
- 解决方法:使用不同情况下的不同现象,使用判断做区分并手动解决异常下的问题
var Book = function(){
if(this instanceof Book){
... // 正常代码
}else{
return new Book()
}
}
var book1 = new Book()
var book2 = Book()