一. 设计模式介绍
- 设计模式是我们在 解决问题的时候针对特定问题给出的简洁而优化的处理方案
- 在 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 = type
this.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 = null
return 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]) return
const event = {
type: type,
arg: arg || {}
}
// 循环执行为当前事件类型订阅的所有事件处理函数
this.message[type].forEach(item => {
item.call(this, event)
})
},
unsubscribe(type,fn){
if (!this.message[type]) return
if(!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);