JavaScript 常见10种设计模式
一. 设计模式介绍
- 设计模式是我们在 解决问题的时候针对特定问题给出的简洁而优化的处理方案
- 在 JS 设计模式中,最核心的思想:封装变化。
- 将变与不变分离,确保变化的部分灵活、不变的部分稳定。
- 本文介绍以下10种常见js设计模式
- 构造器模式
- 原型模式
- 单例模式
- 工厂模式
- 建造者模式
- 适配器模式
- 观察者模式
- 装饰者模式
- 策略模式
- 发布订阅模式
设计模式就是套路
没有一种设计模式可以解决所有问题。设计模式是针对特定问题出现的简洁优化的解决方案
二. 构造器模式
js特有。
var employee1 = {name:"kerwin",age:100}var employee2 = {name:"tiechui",age:18}
以上写法,如果数据量变多,代码重复并且臃肿。
es6之前,通过构造器函数创建对象。
Employee里的this指向的是最终生成的实例employee1, employee2.
function Employee(name,age){this.name = name;this.age =age;this.say = function(){console.log(this.name+"-",this.age)}
}
var employee1 = new Employee("kerwin",100)
var employee2 = new Employee("tiechui",18)
弊端:每次new实例,say方法都会开辟内存创建此方法(构造器模式每次创建实例都会重复创建方法)。——但是不同对象的say方法是一样的。
三. 原型模式
3.1 原型模式
js 特有。
基于构造器模式改造, 使得代码复用性增加。
js特有,将方法放到函数的原型中。
函数的原型是唯一的,在内存中只有一份。
function Employee(name,age){this.name = name;this.age =age;}
Employee.prototype.say = function(){console.log(this.name+"-",this.age)
}
new Employee("kerwin",100)
new Employee("tiechui",18)
3.2 补充:类语法兼顾构造器&原型模式
类语法是es6出现的。es5用构造函数创建对象。
构造器函数: constructor()
类是实例的抽象,实例是类的实现。
类中的方法是在构造器里还是挂载在原型上?后者 —— es6类语法兼顾构造器和原型模式。
class Employee {constructor(name, age) {this.name = name;this.age = age;}say() {console.log(this.name);}
}
var employee1 = new Employee("kerwin",100);
var employee2 = new Employee("tiechui",18);
3.3 案例
<!--* @作者: kerwin
--><!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title><style>* {margin: 0;padding: 0;}ul {list-style: none;}.header {display: flex;width: 500px;}.header li {flex: 1;height: 50px;line-height: 50px;text-align: center;border: 1px solid black;}.box {position: relative;height: 200px;}.box li {position: absolute;left: 0;top: 0;width: 500px;height: 200px;background-color: yellow;display: none;}.header .active {background-color: red;}.box .active {display: block;}</style>
</head><body><div class="container1"><ul class="header"><li class="active">1</li><li>2</li><li>3</li><li>4</li><li>5</li><li>6</li></ul><ul class="box"><li class="active">111</li><li>222</li><li>333</li><li>444</li><li>555</li><li>666</li></ul></div><div class="container2"><ul class="header"><li class="active">1</li><li>2</li><li>3</li><li>4</li><li>5</li><li>6</li></ul><ul class="box"><li class="active">111</li><li>222</li><li>333</li><li>444</li><li>555</li><li>666</li></ul></div><script>function Tabs(selector, type) {this.selector = document.querySelector(`${selector}`)this.type = typethis.headers = this.selector.querySelectorAll(".header li ")this.boxs = this.selector.querySelectorAll(".box li ")// new完实例,调用自己的change方法,绑定事件this.change()}Tabs.prototype.change = function () {for (let i = 0; i < this.headers.length; i++) {this.headers[i].addEventListener(this.type, () => {for (let m = 0; m < this.headers.length; m++) {this.headers[m].classList.remove("active")this.boxs[m].classList.remove("active")}this.headers[i].classList.add("active")this.boxs[i].classList.add("active")}, false)}}new Tabs('.container1', "click")new Tabs('.container2', "mouseover")</script>
</body></html>
四. 工厂模式
由一个工厂对象决定创建某一种产品对象类的实例。主要用来创建同一类对象。
注意switch不是方法,是分支语句,方法是UserFactory,里面有一个User构造函数。
// es5写法
function UserFactory(role){function User(role, pages){this.role = role;this.pages = pages;}switch(role){case "superadmin":return new User("superadmin",["home","user-manage","right-manage","news-manage"])break;case "admin":return new User("admin",["home","user-manage","news-manage"])break;case "editor":return new User("editor",["home","news-manage"])break;default:throw new Error('参数错误')}
}
var user1 = UserFactory('editor');
简单工厂的优点在于,你只需要一个正确的参数,就可以获取到你所需要的对象,而无需知道其创建的具体细节。
但是在函数内包含了所有对象的创建逻辑和判断逻辑的代码,每增加新的构造函数还需要修改判断逻辑代码。
当我们的对象不是上面的3个而是10个或更多时,这个函数会成为一个庞大的超级函数,便得难以维护。
所以,简单工厂只能作用于创建的对象数量较少,对象的创建逻辑不复杂时使用。
以上是es5的写法,下面是es6的写法
// es6类写法
class User {constructor(role, pages){this.role = role;this.pages = pages;}static UserFactory(role) {switch(role){case "superadmin":return new User("superadmin",["home","user-manage","right-manage","news-manage"])break;case "admin":return new User("admin",["home","user-manage","news-manage"])break;case "editor":return new User("editor",["home","news-manage"])break;default:throw new Error('参数错误')}}
}
static方法又称为是类方法,不需要实例化、即可调用的方法。通过类名.方法名()即可调用,如User.UserFactory().
如果不加static,又想访问UserFactory(),则需要创建一个对象实例来调用。
因此此时调用UserFactory()可以通过var user1 = User.UserFactory('editor')
实现。
五. 建造者模式
建造者模式(builder pattern)属于创建型模式的一种,提供一种创建复杂对象的方式。它将一个复杂的对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。
建造者模式是一步一步创建一个复杂的对象,它允许用户只通过指定复杂的对象的类型和内容就可以构建它们,用户不需要指定内部的具体构造细节。
class Navbar {init() {console.log("navbar-init");}getData() {return new Promise((resolve) => {setTimeout(() => {resolve();console.log("navbar-getData");}, 1000);})}render() {console.log("navbar-render");}
}
class List {init() {console.log("List-init");}getData() {return new Promise((resolve) => {setTimeout(() => {resolve();console.log("List-getData");}, 1000);})}render() {console.log("List-render");}
}
// 建造者
class Operator {async startBuild(builder) {await builder.init();await builder.getData();await builder.render();}
}const op = new Operator();
const navbar = new Navbar();
const list = new List();
op.startBuild(navbar);
op.startBuild(list);
建造者模式将一个复杂对象的构建层与其表示层相互分离,同样的构建过程可采用不同的表示。
工厂模式主要是为了创建对象实例或者类簇(抽象工厂),关心的是最终产出(创建)的是什么,而不关心创建的过程。
而建造者模式关心的是创建这个对象的整个过程,甚至于创建对象的每一个细节。
六. 单例模式
1、保证一个类仅有一个实例,并提供一个访问它的全局访问点
2、主要解决一个全局使用的类频繁地创建和销毁,占用内存
如果是全局变量实现,容易造成命名空间污染和变量覆盖问题。
6.1 es5 闭包实现单例模式
闭包:在函数内部,return 函数,被外界变量Singleton引用,导致函数里的变量无法被释放,如此构建出了闭包。
var Singleton= (function () {return function () {}})()
es5闭包实现单例模式。
var Singleton = (function (name, age) {let instance = null;function User(name, age) {this.name = name;this.age = age;}return function (name, age) {if (!instance) {instance = new User(name, age);}return instance;}})()Singleton('kerwin', 100);
注意:第一次调用Singleton方法,创建一个instance,再次调用Singleton方法,由于这是闭包,此时instance没有被回收,此时直接return 之前创建的instance.
Singleton(‘kerwin’, 100) === Singleton(‘kerwin’, 100); // true
6.2 es6写法
class Singleton {constructor(name, age){if(Singleton.instance) {this.name = name;this.age = age; Singleton.instance = this; }return Singleton.instance;}}new Singleton('kerwin', 100);
new Singleton(‘kerwin’, 100) === new Singleton(‘kerwin’, 100); // true
每次new 类()获得的都是第一次实例化的对象。
6.3 示例-单一对话框
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8">
</head>
<style>.kerwin-modal{height: 200px;width: 200px;line-height: 200px;position: fixed;left: 50%;top: 50%;transform: translate(-50%, -50%);background-color: yellow;text-align: center;}
</style><body><button id='open'>打开弹框</button><button id='close'>关闭弹框</button>
</body>
<script>const Modal = (function () {let modal = nullreturn function () {if (!modal) {modal = document.createElement('div')modal.innerHTML = '登录对话框'modal.className = 'kerwin-modal'modal.style.display = 'none'document.body.appendChild(modal)}return modal;}})()document.querySelector('#open').addEventListener('click', function () {const modal = new Modal()modal.style.display = 'block'})document.querySelector('#close').addEventListener('click', function () {const modal = new Modal()modal.style.display = 'none'})
</script></html>
七. 装饰器模式
装饰器模式能够很好地对已有功能进行拓展,这样不会更改原有的代码,对其他的业务产生影响,这方便我们在较少的改动下对软件功能进行拓展。
将不核心的功能抽离出来。
Function是js的原生的function构造函数。
Function.prototype.before = function (beforeFn) {var _this = this;return function () {beforeFn.apply(this, arguments);return _this.apply(this, arguments);};};
Function.prototype.after = function (afterFn) {var _this = this;return function () {var ret = _this.apply(this, arguments);afterFn.apply(this, arguments);return ret;};
};function test() {console.log("11111")
}
var test1 = test.before(() => {console.log("00000")
}).after(()=>{console.log("22222")
})test1()
在test执行前,注入before前置和after后置函数。
然后有一个卖座的数据,点击埋码,不写了。p10
八. 适配器模式
将一个类的接口转换成客户希望的另一个接口。适配器模式让那些接口不兼容的类可以一起工作。
//按照官网代码复制
class TencentMap {show() {console.log('开始渲染腾讯地图');}
}
//按照官网代码复制
class BaiduMap {display() {console.log('开始渲染百度地图');}
}
// 适配器1
class BaiduMapAdapter extends BaiduMap {constructor() {super();}render() {this.display();}
}
// 适配器2
class TencentMapAdapter extends TencentMap {constructor() {super();}render() {this.show();}
}
// 外部调用者
function renderMap(map) {map.render(); // 统一接口调用
}
renderMap(new TencentMapAdapter());
renderMap(new BaiduMapAdapter());
适配器不会去改变实现层,那不属于它的职责范围,它干涉了抽象的过程。外部接口的适配能够让同一个方法适用于多种系统。
九. 策略模式
策略模式定义了一系列算法,并将每个算法封装起来,使它们可以相互替换,且算法的变化不会影响使用算法的客户。策略模式属于对象行为模式,它通过对算法进行封装,把使用算法的责任和算法的实现分割开来,并委派给不同的对象对这些算法进行管理。
该模式主要解决在有多种算法相似的情况下,使用 if...else
所带来的复杂和难以维护。它的优点是算法可以自由切换,同时可以避免多重if...else
判断,且具有良好的扩展性。
let strategy = {"A": (salary )=>{return salary * 4;},"B": (salary )=>{return salary * 3;},"C": (salary )=>{return salary * 2;},
}function calBonus(level, salary) {return strategy[level](salary);
}calBonus("A", 10000);
calBonus("B", 5000);
应用场景
- 适用于开发node路由匹配策略
- 适用于后台返回数组,前端进行映射显示到页面上,示例如下。
<!--* @作者: kerwin
-->
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title><style>li {display: flex;justify-content: space-between;}.reditem {background-color: red;}.yellowitem {background-color: yellow;}.greenitem {background-color: green;}</style>
</head><body><ul id="mylist"></ul><script>var list = [{title: "男人看了沉默",type: 1},{title: "震惊",type: 2},{title: "kerwin来了",type: 3},{title: "tiechui离开了",type: 2}]let obj = {1: {content: "审核中",className: "yellowitem"},2: {content: "已通过",className: "greenitem"},3: {content: "被驳回",className: "reditem"}}mylist.innerHTML = list.map(item =>`<li><div>${item.title}</div> <div class="${obj[item.type].className}">${obj[item.type].content}</div> </li>`).join("")</script>
</body></html>
- 可以有效地避免多重条件选择语句
- 代码复用性高,避免了很多粘贴复制的操作。
- 策略模式提供了对开放封闭原则的支持,将算法独立封装在strategies中,使得它们易于切换,易于扩展。
十. 观察者模式
观察者模式包含观察目标Subject和观察者Observer两类对象,
一个目标可以有任意数目的与之相依赖的观察者
一旦观察目标的状态发生改变,所有的观察者都将得到通知。
当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新,解决了主体对象与观察者之间功能的耦合,即一个对象状态改变给其他对象通知的问题。
// 基本写法
class Sub {constructor() {this.observers = [];}add(observer) {this.observers.push(observer);}remove(observer) {this.observers = this.observers.filter(item => item !== observer);}notify() {this.observers.forEach(item => item.update());}
}class Observer {constructor(name) {this.name = name}update() {console.log("通知了", this.name)}
}
const observer1 = new Observer("kerwin")
const observer2 = new Observer("tiechui")const sub = new Sub()
sub.add(observer1)
sub.add(observer2)setTimeout(() => {sub.notify()
}, 2000)
优势:目标者与观察者,功能耦合度降低,专注自身功能逻辑;观察者被动接收更新,时间上解耦,实时接收目标者更新状态。
缺点:观察者模式虽然实现了对象间依赖关系的低耦合,但却不能对事件通知进行细分管控,如 “筛选通知”,“指定主题事件通知” 。
应用场景:后台系统的通信功能。示例p14,不看了。
十一. 发布订阅模式
1.观察者Observer和目标Subject要相互知道
2.发布者和订阅者不用互相知道,通过第三方实现调度,属于经过解耦合的观察者模式
const PubSub = {list: [],publish() {this.list.forEach(item => item());},subscribe(cb) {this.list.push(cb);}
}
// 订阅者1
function testA() {console.log("testA");
}
// 订阅者2
function testB() {console.log("testB");
}
// 订阅
PubSub.subscribe(testA);
PubSub.subscribe(testB);
// 发布
PubSub.publish(); // 俩回调函数被执行
希望订阅的事件可以细分。
案例改造
const SubPub = {message = {},subscribe(type, fn) {if (!this.message[type]) {this.message[type] = [fn]} else {this.message[type].push(fn)}},publish(type, ...arg) {if (!this.message[type]) returnconst event = {type: type,arg: arg || {}}// 循环执行为当前事件类型订阅的所有事件处理函数this.message[type].forEach(item => {item.call(this, event)})},unsubscribe(type,fn){if (!this.message[type]) returnif(!fn){this.message[type] && (this.message[type].length = 0)}else{this.message[type] = this.message[type].filter(item=>item!==fn)}}}// 订阅者1
function testA() {console.log("testA");
}
// 订阅者2
function testB() {console.log("testB");
}
SubPub.subscribe("A", testA);
SubPub.subscribe("B", testB);