Js设计模式与开发实践
设计模式主要分为下面三大类
- 创建型模式,共五种:工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式。
- 结构型模式,共七种:适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式。
- 行为型模式,共十一种:策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。
在JS中比较重要的设计模式有9种,分别是:工厂模式、单例模式、适配器模式、装饰器模式、代理模式、外观模式、观察者模式、迭代器模式、状态模式。特别地,因为受限于JS的使用场景和特定语法,所以将工厂方法模式、抽象工厂模式、建造者模式都归于工厂模式。因此,这篇文章只介绍21种设计模式。
面向对象
继承,封装和多态
- js实现继承。(react中有应用)
作用:继承可以将公共方法抽离出来,提高复用率,减少冗余
class Person{
constructor(name,age) {
this.name = name;
this.age = age;
}
getPerson(){
return `${this.name}今年${this.age}岁了!`;
}
}
class Student extends Person {
constructor(name,age,lesson){
super(name,age); //继承
this.lesson = lesson
}
getLesson(){
return `${this.name}最喜欢${this.lesson}`
}
}
let p = new Student('陈拉拉',21,'英语');
alert(p.getPerson());
alert(p.getLesson());
2.js实现封装。public,private,protected。只能在ts(typescriptlang.org/play)中实现。
作用:减少耦合,不该外露数据和方法不会外露;有利于数据和接口的权限管理。
虽然es6虽然不支持,但是一般认为_开头的属性为私有属性。
class Person{
public name;
protected age;
constructor(name,age) {
this.name = name;
this.age = age;
}
getPerson(){
return `${this.name}今年${this.age}岁了!`;
}
}
class Student extends Person {
private lesson;
constructor(name,age,lesson){
super(name,age); //继承
this.lesson = lesson
}
getLesson(){
return `${this.name}最喜欢${this.lesson}`
}
}
let p = new Student('陈拉拉',21,'英语');
alert(p.getPerson());
alert(p.getLesson());
alert(p.name);
alert(p.age); //有问题
alert(p.lesson); //有问题
3.多态:同一个接口有多种不同实现方式,js中应用极少,面向接口的编程。
作用:保证子类的开放性和灵活性
class Person{
constructor(name,lesson) {
this.name = name;
this.lesson = lesson;
}
getPerson(){
}
}
class Student extends Person{
constructor(name,lesson) {
super(name,lesson);
}
getPerson(){
return `${this.name}最喜欢${this.lesson}`
}
}
class Teacher extends Person{
constructor(name,lesson) {
super(name,lesson);
}
getPerson(){
return `${this.name}最不喜欢${this.lesson}`
}
}
const student = new Student('李娜','数学');
const teacher = new Teacher('李老师','英语');
alert(student.getPerson());
alert(teacher.getPerson());
jquery就是应用面向对象三要素的一个很好的例子。其中,$()代表着new一个class。
class JQuery {
constructor(selector){
let slice = Array.prototype.slice;
//让document.querySelectorAll(selector)拥有数组的slice方法,
//当slice方法被调用,其作用目标为document.querySelectorAll(selector),
//由于没有传入其他参数,slice()默认返回所有下标的元素并返回新数组
let dom = slice.call(document.querySelectorAll(selector));
debugger;
for(let i=0;i<dom.length;i++){
this[i] = dom[i]
}
this.len = dom.length;
this.selector = selector || ''
}
append(node){
// ...
}
addClass(name){
// ...
}
html(data){
// ...
}
}
// 工厂模式
window.$ = function(selector){
return new JQuery(selector)
}
let $p = $('p');
console.log($p);
console.log($p.addClass);
UML类图
UML,统一建模语言「Unified Modeling Language」,是一种开放的方法,用于说明、可视化、构建和编写一个正在开发的、面向对象的、软件密集系统的制品的开放方法。在www.processon.com或者visio可实现绘制。一般都建议在写代码之前先绘制UML类图。类的书写格式如下:
// ------------------------------------
// | 类名 |
// ------------------------------------
// | + public属性名A:类型 |
// | # protected属性名B:类型 |
// | - private属性名C:类型 |
// ------------------------------------
// | + public方法名A(参数):返回值类型 |
// | # protected方法名B(参数):返回值类型 |
// | - private方法名C(参数):返回值类型 |
// ------------------------------------
UML类图中有六种关系,分别是依赖关系,关联关系,聚合关系,组合关系,实现关系,泛化关系。示例图如下。详细内容可以参考终于明白六大类UML类图关系了。
5大设计原则
SOLID是面向对象软件开发中最流行的设计原则之一。它是以下五个设计原则的助记符缩写:
- 单一责任原则 (JS中常用)
- 开放/封闭原则 (JS中常用)
- 李氏替代原则
- 接口隔离原理
- 依赖倒置
23种设计模式(实际只有21种)
第一种到第九种为js中常用的设计模式。
1. 工厂模式(符合开放封闭原则)
我们在创建对象时不会对客户端直接暴露创建逻辑,而是通过使用一个共同的接口根据不同的条件来指向具体想要创建的对象。
使用场景:执行new操作的时候,考虑是否需要使用工厂模式。
class Product{
constructor(name){
this.name = name;
}
func1(){
return `func1的名字:${this.name}`
}
func2(){
return `func2的名字:${this.name}`
}
}
class Creator{
create(name){
return new Product(name);
}
}
//下面是测试代码
let creator = new Creator();
let getName = creator.create('小明');
alert(getName.func1());
alert(getName.func2());
经典的使用场景有:jquery的$('div),React.createElement()和vue的异步组件。
2. 单例模式(单一职责原则)
单例模式,顾名思义就是只有一个实例,并且她自己负责创建自己的对象,这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。常用场景:购物车,登录框,vuex和redux中的store。
使用js模拟单例模式。
class SingleObject {
login(){
console.log('正在登陆中')
}
}
SingleObject.getInstance = (function(){
let instance;
return function(){
if(!instance){
instance = new SingleObject();
}
return instance;
}
})()
let obj1 = SingleObject.getInstance();
obj1.login()
let obj2 = SingleObject.getInstance();
obj2.login()
console.log('obj1=obj2吗?',obj1===obj2);
// 跟java不同的是,js没有private,所以js new SingleObject()实现的单例模式不会报错
let obj3 = new SingleObject();
console.log('obj3和obj1是否相同:',obj1===obj3,obj3);
3. 适配器模式(开关封闭原则)
适配器模式(Adapter Pattern):将一个接口转换成客户希望的另一个接口,使接口不兼容的那些类可以一起工作,其别名为包装器(Wrapper)。
class Adapter{
request(){
return '德国的标准插头'
}
}
class NewAdapter{
constructor(){
this.adapter = new Adapter();
}
newRequest(){
return `${this.adapter.request()}-->转换成-->中国标准插头`;
}
}
let newAdapter = new NewAdapter();
let newRequest = newAdapter.newRequest();
console.log(newRequest);
4. 装饰器模式
装饰器模式(Decorator Pattern)允许向一个现有的对象添加新的功能,同时又不改变其结构。
class Boy{
@run //给类属性方法(speak)加的一个装饰器 ,扩展了类Boy的speak(在讲话的同时跑步)
speak (){
console.log('I can speak')
}
}
function run () {
console.log('I can run')
}
let tj = new Boy()
tj.speak()
// I can run
// I can speak
可安装babel-plugin-transform-decorators-legacy 插件或者core-decorators.js来实现装饰器。
5. 代理模式(符合开放封闭原则)
为其他对象提供一种代理以控制对这个对象的访问。
class ReadImg {
constructor(fileName){
this.fileName = fileName;
this.load();
}
load(){
console.log('正在从硬盘中读取图片数据~');
}
displayImg(){
console.log('图片数据已经展示,啦啦啦');
}
}
class ProxyAddr{
constructor(fileName){
this.readImg = new ReadImg(fileName);
}
displayImg(){
this.readImg.displayImg();
}
}
let proxy = new ProxyAddr('1.png');
proxy.displayImg();
6. 外观模式
目的是为了让子系统中的一组接口提供一个高级接口,让外部使用者调用该高级接口。不符合单一职责原则和开放封闭原则以及接口独立原则。
实现代码如下
function bindEvent(elem, type, selector, fn){
if(fn===null){
fn = selector;
selector = null;
}
// *****
}
bindEvent(elem, 'click', '#div', fn);
bindEvent(elem, 'click', fn)
7. 观察者模式( 符合开放封闭原则)
当对象间存在一对多关系时,则使用观察者模式(Observer Pattern)。比如,当一个对象被修改时,则会自动通知它的依赖对象。是最重要的一个设计模式。
// 主题和观察者分离,不是主动触发,而是被动监听,两者是解耦的
class Subject{
constructor(){
this.state = 0;
this.observers = [];
}
getState(){
return this.state;
}
setState(state){
this.state = state;
this.notifyAllobservers();
}
notifyAllobservers(){
this.observers.forEach(observer=>{
observer.update();
})
}
append(obr){
this.observers.push(obr);
}
}
class Observers{
constructor(name,sub){
this.name = name;
this.subject = sub;
this.subject.append(this);
}
update(){
console.log(`${this.name}的state为:${this.subject.getState()}`)
}
}
//测试
let sub = new Subject();
let obs1 = new Observers('observers1', sub);
let obs2 = new Observers('observers2', sub);
let obs3 = new Observers('observers3', sub);
sub.setState(234)
应用场景有:jquery,node,vue和react
8. 迭代模式(开放封闭原则)
让用户通过特定的接口访问容器的数据,不需要了解容器内部的数据结构。
使用上面的方法实现迭代器模式,而不是下面这种。
9. 状态模式
在状态模式(State Pattern)中,类的行为是基于它的状态改变的。这种类型的设计模式属于行为型模式。在状态模式中,我们创建表示各种状态的对象和一个行为随着状态对象改变而改变的 context 对象。下面举两个状态模式的例子。第一个是有限状态机。
// javascript-state-machine库
import StateMachine from 'javascript-state-machine';
let fsm = new StateMachine({
init: '收藏',
transition: [{
name:'deleteStore',
from:'收藏',
to:'取消收藏'
},{
name:'store',
from:'取消收藏',
to:'收藏'
}],
method:{
onDeleteStore(){
alert('收藏->取消收藏');
updateButText();
},
onStore(){
alert('取消收藏->收藏');
updateButText();
}
}
})
let but = document.getElementById('but');
// 点击按钮,实现切换按钮文本内容
but.onclick = function(){
if(fsm.is('收藏')){
fsm.onDeleteStore()
}else{
fsm.onStore()
}
}
function updateButText(){
but.innerHTML = fsm.state;
}
// 初始化
updateButText();
第二个是使用状态模式简单地实现promise。
import StateMachine from 'javascript-state-machine';
let fsm = new StateMachine({
init: 'pending',
transitions: [{
name:'resolve',
from:'pending',
to:'fullfilled'
},{
name:'reject',
from:'pending',
to:'rejected'
}],
methods:{
onResolve:function(state,data){
data.successList.forEach(fn=>fn())
},
onReject:function(state,data){
data.failureList.forEach(fn=>fn())
}
}
})
// 自定义的promise
class MyPromise {
constructor(fn){
let _this = this;
this.successList = [];
this.failureList = [];
fn(function(){
fsm.resolve(_this);
},function(){
fsm.reject(_this);
})
};
then(success,failure){
this.successList.push(success);
this.failureList.push(failure);
}
}
// 加载图片
function loadImg(src){
let promise = new MyPromise(function(resolve,reject){
let imgEle = document.createElement('img');
//图片加载完成之后触发
imgEle.onload = function(){
resolve(imgEle);
}
//图片加载失败之后触发
imgEle.onerror = function(){
reject();
}
imgEle.src= src;
})
return promise;
}
//测试
let result = loadImg('http://yuhui7pm.cn/picture/xiongbenxiong.jpg');
result.then(function(){
console.log('第一次图片加载成功');
},function(){
console.log('第一次图片加载失败');
})
result.then(function(){
console.log('第二次图片加载成功');
},function(){
console.log('第二次图片加载失败');
})
// 结果:
// 第一次图片加载成功
// 第二次图片加载成功
10. 原型模式
原型模式(Prototype Pattern)是用于创建重复的对象,同时又能保证性能。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
// 原型对象
const person = {
isHuman: false,
printIntroduction: function () {
console.log(`My name is ${this.name}. Am I human? ${this.isHuman}`);
}
};
// 基于原型对象创造的对象
const David = Object.create(person);
David.name = "David"; // "name" is a property set on "me", but not on "person"
David.isHuman = true; // inherited properties can be overwritten
David.printIntroduction();
// expected output: "My name is David. Am I human? true"
// 基于原型对象创造的对象
const Lili = Object.create(person);
Lili.name = "Lili"; // "name" is a property set on "me", but not on "person"
Lili.isHuman = true; // inherited properties can be overwritten
Lili.printIntroduction();
// expected output: "My name is Lili. Am I human? true"
11. 桥接模式(开放封闭原则)
桥接模式即将抽象部分与它的实现部分分离开来,使他们都可以独立变化。为什么需要该模式呢?
假设要绘制矩形、圆形、椭圆、正方形,我们至少需要4个形状类,但是如果绘制的图形需要具有不同的颜色,如红色、绿色、蓝色等,此时至少有如下两种设计方案:
- 第一种设计方案是为每一种形状都提供一套各种颜色的版本。
- 第二种设计方案是根据实际需要对形状和颜色进行组合。
对于有两个变化维度(即两个变化的原因)的系统,采用方案二来进行设计系统中类的个数更少,且系统扩展更为方便。设计方案二即是桥接模式的应用。桥接模式将继承关系转换为关联关系,从而降低了类与类之间的耦合,减少了代码编写量。实例代码如下:
12. 组合模型
组合模式(Composite Pattern) 也称为 整体-部分(Part-Whole)模式,它的宗旨是通过将单个对象(叶子节点)和组合对象(树枝节点)用相同的接口进行表示,使得客户对单个对象和组合对象的使用具有一致性。常见的应用为虚拟节点vnode。
<div id="app">
<span>{{ message }}</span>
<ul>
<li v-for="item of list" class="item-cls">{{ item }}</li>
</ul>
</div>
将上述html代码转换成vnode:
{
"tag": "div",
"data": {
"attr": { "id": "app" }
},
"children": [
{
"tag": "span",
"children": [
{ "text": "hello Vue.js" }
]
},
{
"tag": "ul",
"children": [
{
"tag": "li",
"data": { "staticClass": "item-cls" },
"children": [
{ "text": "jack" }
]
},
{
"tag": "li",
"data": { "staticClass": "item-cls" },
"children": [
{ "text": "rose" }
]
},
{
"tag": "li",
"data": { "staticClass": "item-cls" },
"children": [
{ "text": "james" }
]
}
]
}
],
"context": "$Vue$3",
"elm": "div#app"
}
13. 享元模式
享元模式(Flyweight Pattern)主要是为了减少创建对象的数量,以减少内存占用和提高性能。这种类型的设计模式属于结构型模式,它提供了减少对象数量从而改善应用所需的对象结构的方式。该种模式在JS中没有应用场景,但是有预期思想类似的应用场景:
14.策略模式
策略这个词应该怎么理解,打个比方说,我们出门的时候会选择不同的出行方式,比如骑自行车、坐公交、坐火车、坐飞机、坐火箭等等,这些出行方式,每一种都是一个策略。
再比如我们去逛商场,商场现在正在搞活动,有打折的、有满减的、有返利的等等,其实不管商场如何进行促销,说到底都是一些算法,这些算法本身只是一种策略,并且这些算法是随时都可能互相替换的,比如针对同一件商品,今天打八折、明天满100减30,这些策略间是可以互换的。
策略模式(Strategy),定义了一组算法,将每个算法都封装起来,并且使它们之间可以互换。
15. 模板方法模式
模板方法模式(Template Method Pattern) 实际上是封装了一个固定流程,该流程由几个步骤组成,具体步骤可以由子类进行不同实现,从而让固定的流程产生不同的结果。
class Client {
public static void main(String[] args) {
AbstractClass abc = new ConcreteClassA();
abc.templateMehthod();
abc = new ConcreteClassB();
abc.templateMehthod();
}
// 抽象模板类
static abstract class AbstractClass {
protected void step1() {
System.out.println("AbstractClass:step1");
}
protected void step2() {
System.out.println("AbstractClass:step2");
}
protected void step3() {
System.out.println("AbstractClass:step3");
}
// 声明为final方法,避免子类覆写
public final void templateMehthod() {
this.step1();
this.step2();
this.step3();
}
}
// 具体实现类A
static class ConcreteClassA extends AbstractClass {
@Override
protected void step1() {
System.out.println("ConcreateClassA:step1");
}
}
// 具体实现类B
static class ConcreteClassB extends AbstractClass {
@Override
protected void step2() {
System.out.println("ConcreateClassB:step2");
}
}
}
16. 责任链模式(开放封闭原则)
责任链模式(Chain of Responsibility Pattern)为请求创建了一个接收者对象的链。这种模式给予请求的类型,对请求的发送者和接收者进行解耦。这种类型的设计模式属于行为型模式。
class Action {
constructor(name){
this.name = name;
this.nextAction = null
}
setNextAction(action){
this.nextAction = action
}
handle(){
console.log(`${this.name}审批`);
if(this.nextAction != null){
this.nextAction.handle();
}
}
}
//测试代码
let a1 = new Action('组长');
let a2 = new Action('经理');
let a3 = new Action('总监');
a1.setNextAction(a2);
a2.setNextAction(a3);
a1.handle();
// output:
// 组长审批
// 经理审批
// 总监审批
在Jquery的链式操作和Promise.then的链式操作中都有所体现。
17. 命令模式
什么是命令模式?假设现在有一个请求处理类(低层类/第三方类),如果客户端拿到这个类之后直接调用它,那么客户端和这个请求处理类之间的藕合度过高。
这时候我们在客户端的请求发送类和请求处理类之间增加一个Invoker类,再将请求发送类发送的所有请求封装成对象,然后让Invoker类去管理这些请求对象,并决定这些请求是否允许执行、何时执行、按什么顺序执行。
由于在请求发送类和请求处理类之间增加了请求转发者,因此这两个类之间的藕合度就大大降低。
class Receiver {
exec(){
console.log('3.接收命令并执行');
}
}
class Invoker {
constructor(receiver){
this.receiver = receiver;
}
invoke(){
console.log('2.转发命令');
this.receiver.exec();
}
}
class Client{
constructor(invoker){
this.invoker = invoker;
}
command(){
console.log('1.发布命令')
this.invoker.invoke();
}
}
// 测试
let receiver = new Receiver();
let invoker = new Invoker(receiver);
let client = new Client(invoker);
client.command();
// output:
// 1.发布命令
// 2.转发命令
// 3.接收命令并执行
应用场景:在js中常见于富文本编辑器。
命令模式优点:
- 命令模式将请求发送者和请求处理者分离开,从而降低了这两个类之间的藕合;
- 通过在请求发送者和请求处理者之间增加转发类的方式,从而客户端发出的请求可以在被处理之前都存放在Invoker类的容器中,请求在被执行前就有了一个缓冲,能起到以下作用:
a)Invoker能够对客户端发出的请求进行排序;
b)Invoker能够决定是否需要驳回请求;
c)客户端可以在请求被执行前选择撤销某个请求;
d)在需要的情况下,客户端的请求可以被记录成日志; - 增加新的命令时只需增加新的命令子类即可。
18.备忘录模式
备忘录模式(Memento Pattern)保存一个对象的某个状态,以便在适当的时候恢复对象。备忘录模式属于行为型模式。
// 备忘录的某一项
class Item {
constructor(item){
this.item = item;
}
getItem(){
return this.item;
}
}
// 备忘录列表
class Memo {
constructor(){
this.list = []
}
add(item){
this.list.push(item);
}
get(index){
return this.list[index]
}
}
// 编辑器
class Editor {
constructor(){
this.memonto = null;
}
setContent(item){
this.memonto = item;
}
getContent(){
return this.memonto;
}
saveContentToMomento(){
return new Item(this.memonto);
}
getContentFromMemento(item){
this.memonto = item.getItem();
}
}
// 测试
let editor = new Editor();
let memo = new Memo();
editor.setContent('1111');
editor.setContent('2222');
memo.add(editor.saveContentToMomento()); // 备份
editor.setContent('3333');
memo.add(editor.saveContentToMomento()); // 备份
editor.setContent('4444');
console.log(editor.getContent()); // output: 444
editor.getContentFromMemento(memo.get(1));
console.log(editor.getContent()); // output: 333
editor.getContentFromMemento(memo.get(0));
console.log(editor.getContent()); // output: 222
19. 中介者模式
定义一个中介者对象, 封装一系列对象的交互关系, 使得各对象不必显示的相互引用, 从而使其耦合松散, 而且可以独立的改变它们的交互。
// 要求实现:A通过中介修改B的价格,B通过中介修改A的价格
// 中介
class Medium {
constructor(clientA, clientB){
this.clientA = clientA;
this.clientB = clientB;
}
setAPrice(){
let newPrice = this.clientA.price;
this.clientA.revisePrice(newPrice + 200);
}
setBPrice(){
let newPrice = this.clientB.price;
this.clientB.revisePrice(newPrice + 400);
}
}
// 用户A
class ClientA {
constructor(){
this.price = 0;
}
revisePrice(price, medium){
this.price = price;
medium && medium.setAPrice();
}
getPrice(){
console.log("clicentA's price:", this.price)
}
}
// 用户B
class ClientB {
constructor(){
this.price = 0;
}
revisePrice(price, medium){
this.price = price;
medium && medium.setBPrice();
}
getPrice(){
console.log("ClientB's price:", this.price)
}
}
// 测试
let clientA = new ClientA();
let clientB = new ClientB();
let medium = new Medium(clientA, clientB);
clientA.revisePrice(200, medium);
clientB.revisePrice(200, medium);
clientA.getPrice(); // output: clicentA's price: 400
clientB.getPrice(); // output: ClientB's price: 600
20. 访问者模式
访问者模式是一种将数据操作和数据结构分离的设计模式。几乎没有使用场景,所以不多加介绍。感兴趣的小伙伴自行google。
21. 解释器模式
解释器模式(Interpreter),给定一个语言,定义它的文法的一种表示,并定义一个解释器,这个解释器使用该表示来解释语言中的句子。主要使用在babel,sass,less中。普通开发者的使用场景不多,这里不多加说明。
以上内容主要学习自双越老师的网课"JavaScript设计模式与开发实践",以及参考了其它博主的文章!