js设计模式

原始

1
2
3
4
5
6
7
8
9
let e1 = {
name: "kerwin",
age: 18
}

let e2 = {
name: "tiechui",
age: 100
}

1.构造器模式

构造函数

1
2
3
4
5
6
7
8
9
10
function Employee(name, age) {
this.name = name
this.age = age
this.say = function() {
console.log(this.name + '-' + this.age)
}
}

let e1 = new Employee("kerwin", 100)
let e2 = new Employee("tiechui", 18)

优点:复用代码

缺点:每个对象的方法都是独立的,但其实没必要,可以复用的

2.原型模式

js 专用,只有 js 有原型

可以把方法挂在原型上,这样就可以复用了

1
2
3
4
5
6
7
8
9
function Employee(name, age) {
this.name = name
this.age = age
}
Employee.prototype.say = function() {
console.log(this.name + '-' + this.age)
}
let e1 = new Employee("kerwin", 100)
let e2 = new Employee("tiechui", 18)

es6 类

综合构造器模式和原型模式

1
2
3
4
5
6
7
8
9
10
11
12
13
class Employee {
constructor(name, age) {
this.name = name
this.age = age
}

// 方法会挂在原型上
say() {
console.log(this.name + '-' + this.age)
}
}

let e1 = new Employee("kerwin", 100)

3.工厂模式

由一个工厂对象决定创建某一种产品对象类的实例。主要用来创建同一类对象。

传一个参数进去,会根据这个参数,new一个对象返回。不用管过程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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"])
case "admin":
return new User("admin", ["home", "user-manage", "news-manage"])
case "editor":
return new User("editor", ["home", "news-manage"])
default:
throw new Error('参数错误')
}
}
}

let user = User.UserFactory("editor")

优点:只需要一个参数,就可以获取到需要的对象,无需知道创建的具体细节。

缺点:对象创建的逻辑写在类方法里,每次增加新的构造函数需要修改判断逻辑代码,需要维护。

使用场景:创建的对象数量较少,对象的创建逻辑不复杂。(如后台系统的权限管理)

4.抽象工程模式

相比工厂模式,返回的是类,而不是实例。

优点:更解耦,可以管理每一个类。方便给每一个类添加自己的东西。

缺点:比较复杂,大型项目才会使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
class User {
constructor(name, role, pages) {
this.name = name
this.role = role
this.pages = pages
}

welcome() {
console.log("欢迎回来", this.name)
}

// 抽象方法 需要被子类实现的方法,不同子类的实现不一样
// js 保留了关键字 abstract,但没有意义
// ts 中的抽象方法:使用abstract开头的方法叫做抽象方法,抽象方法没有方法体只能定义在抽象类中,子类继承抽象类时抽象方法必须要重写
dataShow() {

}
}

class SuperAdmin extends User {
constructor(name) {
super(name, "superadmin", ["home", "user-manage", "right-manage", "news-manage"])
}

dataShow() {
// abstract
console.log('superadmin-datashow')
}

addRight() {

}

addUser() {

}
}

class Admin extends User {
constructor(name) {
super(name, "admin", ["home", "user-manage", "news-manage"])
}

dataShow() {
// abstract
console.log('admin-datashow')
}

addUser() {

}
}

class Editor extends User {
constructor(name) {
super(name, "editor", ["home", "news-manage"])
}

dataShow() {
// abstract
console.log('editor-datashow')
}
}

function getAbstractUserFactory(role) {
switch(role) {
case "superadmin":
return SuperAdmin
case "admin":
return Admin
case "editor":
return Editor
default: throw new Error("参数错误")
}
}

let UserClass = getAbstractUserFactory("editor")
let user = new UserClass("kerwin")
user.dataShow()

5.建造者模式

建造者模式将一个复杂对象的构建层与其表示层相互分离,同样的构建过程可采用不同的表示。

工厂模式是为了创建对象实例或者类,关心的是创建的结果,而不关心创建的过程。

建造者模式关心的是创建这个对象的过程。

有一些元素构建的过程一致,可以创建一个类,在这个类中实现整个构建的过程,这样就可以复用构建的过程了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 假设有两个类navbar和list, 它们创建的过程一致。初始化、发请求获取数据、渲染。(实际上大部分组件都是这样的过程)
class Navbar {
init() {
console.log("Navbar-init")
}

getData() {
console.log("Navbar-getData")
// 异步
return new Promise(() => {
setTimeout((resolve) => {
resolve('list-111')
}, 1000);
})
}

render() {
console.log("Navbar-render")
}
}

class List {
init() {
console.log("List-init")
}

getData() {
console.log("List-getData")
return "navbar-111"
}

render() {
console.log("List-render")
}
}
1
2
3
4
5
6
7
8
9
10
// 渲染这两个,可以实例化后依次调用方法
let navbar = new Navbar()
navbar.init()
navbar.getData()
navbar.render()

let list = new List()
list.init()
list.getData()
list.render()
1
2
3
4
5
6
7
8
9
10
11
12
13
// 如果使用建造者模式, 创建一个新的类, 在这个类中实现过程
class Creator {
// 就是在封装复用调用方法的过程
async startBuild(builder) {
await builder.init()
await builder.getData()
await builder.render()
}
}

const op = new Creator()
op.startBuild(new Navbar())
op.startBuild(new List())

6.单例模式

保证一个类只有一个实例,并提供一个访问它的全局访问点

主要解决一个全局使用的类频繁创建和销毁,占用内存

为什么不用全局变量,而要使用单例模式(因为全局变量容易被污染)

使用场景:vuex / redux;防抖节流

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// es5前 闭包实现
let Singleton = (function() {
let instance
function User(name, age) {
this.name = name
this.age = age
}

return function(name, age) {
if (!instance) {
instance = new User(name, age)
}
return instance
}
})()

let a = Singleton("kerwin", 100)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// es6后
class Singleton {
constructor(name, age) {
if (!Singleton.instance) {
this.name = name
this.age = age
Singleton.instance = this
}
return Singleton.instance
}
}

// true
console.log(new Singleton("kerwin", 100) === new Singleton("xm", 18))

7.装饰器模式

对已有功能进行拓展,不改变原有代码,对其他业务产生影响。常用于添加前置和后置函数。

使用场景:路由守卫;axios(请求/响应)拦截器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
Function.prototype.before = function(beforeFn) {
let _this = this
return function() {
// 先进行前置调用
beforeFn.apply(this, arguments)

// 执行原来的函数
return _this.apply(this, arguments)
}
}

Function.prototype.after = function(afterFn) {
let _this = this
return function() {
let result = _this.apply(this, arguments)
// 先进行后置调用
afterFn.apply(this, arguments)

// 执行原来的函数
return result
}
}

function test() {
console.log('函数本身')
}

let test1 = test.before(function() {
console.log('前置函数')
}).after(function() {
console.log('后置函数')
})

test1()

8.适配模式

将一个类的接口转换成客户希望的另一个接口。适配器模式让那些接口不兼容的类可以一起工作。

使用场景:axios库在web端和node端都可以使用,实际使用了适配器的理念。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class TencentMap {
show() {
console.log("开始渲染腾讯地图")
}
}

class BaiduMap {
display() {
console.log("开始渲染百度地图")
}
}

// 适配器
class TencentAdapator extends TencentMap{
constructor() {
super()
}

display() {
this.show()
}
}

function renderMap(map) {
map.display()
}

renderMap(new TencentMap())
renderMap(new BaiduMap())

9.策略模式

替代 if-else,if-else 难维护

策略模式将算法封装,把使用算法的责任和算法的实现分割开来。拓展性强。

1
2
3
4
5
6
7
8
9
10
11
12
// 把算法拎出来 拓展性强 直接在strategy中加就行
let strategy = {
"A": (salary) => salary * 4,
"B": (salary) => salary * 3,
"C": (salary) => salary * 2,
}

function calBonus(level, salary) {
return strategy[level](salary)
}

calBonus("A", 10000)

其实在项目中已经经常使用这种思想了。

10.代理模式

proxy,为其他对象提供一种代理以控制对这个对象的访问

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// es5前
class Star {
play() {
console.log("演戏");
}
}

class StarProxy {
constructor() {
this.superStar = new Star()
}

talk(price) {
if (price >= 10000) {
this.superStar.play()
} else {
throw new Error("价钱不合适")
}
}
}

let jr = new StarProxy()
jr.talk(10000)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// es6后 利用proxy
let star = {
name: 'tiechui',
workprice: 10000
}

let proxy = new Proxy(star, {
// target: star
get(target, key) {
if (key === 'workprice') {
console.log("访问了")
}
return target[key]
},
set(target, key, value) {
if (key === "workprice") {
console.log("设置了")
if (value > 10000) {
console.log("可以合作")
} else {
throw new Error("价钱不合适")
}
}
}
})

11.观察者模式

包含观察目标和观察者两类对象

一个目标可以有任意与之相依赖的观察者

一旦观察目标的状态发生改变,所有的观察者都将得到通知并被自动更新性能

解决了主体对象与观察者之间功能的耦合,即一个对象状态改变给其他对象通知的问题

使用场景:导航栏?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
class Subject {
constructor() {
this.observers = []
}

add(observer) {
this.observers.push(observer)
}

notify() {
this.observers.forEach((item) => {
item.update()
})
}

remove(observer) {
this.observers = this.observers.filter((item) => {
return item !== observer
})
}
}

class Observer {
constructor(name) {
this.name = name
}
update() {
console.log(this.name)
}
}

const subject = new Subject()
const observer1 = new Observer('kerwin')
const observer2 = new Observer('tiechui')

subject.add(observer1)
subject.add(observer2)
setTimeout(() => {
subject.remove(observer1)
}, 1000);
setTimeout(() => {
subject.notify()
}, 2000);

优点:目标者与观察者,功能耦合度降低,专注自身功能逻辑;观察者被动接受更新,时间上解耦,实时接受目标者更新状态 。

缺点:不能对事件通知进行细分管控,如”筛选通知“,”指定主题事件通知“。

12.发布订阅模式

观察者模式中,观察者和目标要相互知道

发布订阅模式中,发布者和订阅者不用互相知道,通过第三方实现调度,属于经过解耦合的观察者模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
class Event {
events = {}

// 触发事件
emit(type, ...args) {
const listeners = this.events[type]
for (const listener of listeners) {
listener(...args)
}
}

// 绑定事件
on(type, listener) {
this.events[type] = this.events[type] || []
this.events[type].push(listener)
}

// 只触发一次
once(type, listener) {
const callback = (...args) => {
this.off(type, callback)
listener(...args)
}
this.on(type, callback)
}

// 事件解绑
off(type, listener) {
this.events[type] = this.events[type] || []
this.events[type] = this.events[type].filter((item) => {
item !== listener
})
}
}

// 发布订阅
const event = new Event()
event.subscribe("event1", function(data){
console.log(data)
})
// 在需要的时候调用
event.publish("event1", '123')

比较观察者和发布订阅

观察者

观察者模式创建两个类,一个观察目标(subject)、一个观察者(observe),观察目标需要添加观察者,所以并没有解耦。

触发通知时要调用观察者类中预先定义好的更新方法(update)

由于update只有一个,所以无法实现细分管控。要实现的话需要多定义一些Observer类,这样维护起来很麻烦。

耦合度高,即需要知道每个观察者,又需要知道观察者类的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
class Subject {
constructor() {
this.observers = []
}

add(observer) {
this.observers.push(observer)
}

notify() {
// 触发通知时要调用观察者类中预先定义好的更新方法(update)
// 由于update只有一个,所以无法实现细分管控。要实现的话需要多定义一些Observer类,这样维护起来很麻烦
this.observers.forEach((item) => {
item.update()
})
}

remove(observer) {
this.observers = this.observers.filter((item) => {
return item !== observer
})
}
}

class Observer {
constructor(name) {
this.name = name
}
// 更新方法(耦合)
update() {
console.log(this.name)
}
}

const subject = new Subject()
const observer1 = new Observer('kerwin')
const observer2 = new Observer('tiechui')

// 观察目标添加观察者, 并没有解耦(耦合)
subject.add(observer1)
subject.add(observer2)
setTimeout(() => {
subject.remove(observer1)
}, 1000);
setTimeout(() => {
subject.notify()
}, 2000);

发布订阅

发布订阅模式只有一个Event类(PubSub),作为中间控制平台,只需要在这个平台上绑定事件,在需要使用的时候利用事件名触发事件即可。所以耦合度很低,只需知道事件名。

此外,事件名就可以完美地实现事件筛选。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class Event {
events = {}

emit(type, ...args) {
const listeners = this.events[type]
for (const listener of listeners) {
listener(...args)
}
}

on(type, listener) {
this.events[type] = this.events[type] || []
this.events[type].push(listener)
}

once(type, listener) {
const callback = (...args) => {
this.off(type, callback)
listener(...args)
}
this.on(type, callback)
}

off(type, listener) {
this.events[type] = this.events[type] || []
this.events[type] = this.events[type].filter((item) => {
item !== listener
})
}
}

const event = new Event()
// 事件名(耦合)
event.subscribe("event1", function(data){
console.log(data)
})
// 在需要的时候调用
event.publish("event1", '123')

13.模块(化)模式

模块化:能够使一个单独的对象拥有公共/私有的方法和变量,减少函数名冲突的可能;拆分大文件。

以前使用闭包实现变量私有化

1
2
3
4
5
6
7
8
9
10
11
12
13
const obj = (function() {
// 写在这的是私有的属性和方法;外部无法访问,但也不会被回收,因为闭包
let count = 0
return {
// 返回的是公有的属性和方法
increase() {
return ++count
},
decrease() {
return --count
}
}
})

==es6模块化==

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 导出
let count = 0

function increase() {
return ++count
}

function decrease() {
return --count
}

// export
export default {
increase,
decrease
}
1
2
3
4
<!-- 导入 -->
<script type="module">
import myObj from './test.js'
</script>

ES13新增了#,可以在类中直接声明私有变量。

1
2
3
4
5
6
class Person {
#name = "kerwin"
}

// undefined
console.log(new Person().#name)

14.桥接模式

桥接模式:将抽象部分与它的实现部分分离,使它们都可以独立的变化

使用场景:一个类存在两个或多个独立变化的维度,且这两个维度都需要进行拓展

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// 维度1:厂商
// 奥迪
function Audi(engine) {
this.engine = engine
}

Audi.prototype.loadEngine = function() {
// 抽象
this.engine.run()
}

// 奔驰
function Benz(engine) {
this.engine = engine
}

Benz.prototype.loadEngine = function() {
this.engine.run()
}

// 维度2:发动机
// 发动机v6
function V6() {
this.run = function() {
// 实现
console.log("V6发动机")
}
}

// 发动机v8
function V8() {
this.run = function() {
console.log("v8发动机");
}
}

let audi1 = new Audi(new V6())
let audi2 = new Audi(new V8())
let benz1 = new Benz(new V8())
audi1.loadEngine()
audi2.loadEngine()
benz1.loadEngine()

优点:把抽象与实现隔离开,有助于独立管理各个组成部分

缺点:每使用一个桥接元素都要增加一次函数调用,系统的复杂程度提高

15.组合模式

组合模式在对象间形成树形结构

组合模式中基本对象和组合对象被一致对待

无须关心对象有多少层,调用时只需在根部进行调用

在树形结构的问题中,模糊了简单元素和复杂元素的概念。程序可以向处理简单元素一样来处理复杂元素,从而使客户程序与复杂元素的内部结构解耦

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
const Folder = function(folder) {
this.folder = folder
this.list = [] // 保存 子文件夹 或者 文件
}

Folder.prototype.add = function(res) {
this.list.push(res)
}

Folder.prototype.scan = function() {
console.log("扫描文件夹", this.folder)
for (let i = 0; i < this.list.length; i++) {
this.list[i].scan()
}
}

const File = function(file) {
this.file = file
}
File.prototype.scan = function() {
console.log("扫描文件", this.file)
}

// 根
let rootFolder = new Folder("root")
// 子文件夹
let htmlFolder = new Folder("html")
let cssFolder = new Folder("css")
let jsFolder = new Folder("js")
// 文件
let html1 = new File("html1")
let html2 = new File("html2")
let css1 = new File("css1")
let css2 = new File("css2")
let js1 = new File("js1")
let js2 = new File("js2")

rootFolder.add(htmlFolder)
rootFolder.add(cssFolder)
rootFolder.add(jsFolder)

htmlFolder.add(html1)
htmlFolder.add(html2)
cssFolder.add(css1)
cssFolder.add(css2)
jsFolder.add(js1)
jsFolder.add(js2)

// 调用
rootFolder.scan()

js设计模式
http://example.com/2023/01/03/js设计模式/
Author
John Doe
Posted on
January 3, 2023
Licensed under