前端常用设计模式
0. 设计模式简介
根据设计模式的参考书 Design Patterns - Elements of Reusable Object-Oriented Software(中文译名:设计模式 - 可复用的面向对象软件元素) 中所提到的,总共有 23 种设计模式。这些模式可以分为三大类:创建型模式(Creational Patterns)、结构型模式(Structural Patterns)、行为型模式(Behavioral Patterns)。
本文主要简述了其中一些常用的设计模式
1. 设计模式目的
在代码封装性、可读性、重用性、可扩展性、可靠性等方面,使项目更易于开发、维护及扩展。
2. 设计模式分类
- 创建型模式:创建对象的同时隐藏创建逻辑的方式。
- 工厂模式
- 单例模式
- 结构型模式:关注类和对象的组合,简化系统的设计。
- 外观模式
- 代理模式
- 行为型模式:关注对象之间的通信,增加灵活性。
- 策略模式
- 迭代器模式
- 观察者模式
- 中介者模式
- 访问者模式
3. 创建型模式
3.1 工厂模式
在工厂模式中,我们在创建对象时不会对外部暴露创建逻辑,并且是通过使用一个共同的接口来指向新创建的对象。
-
目的:定义一个创建对象的接口,可以方便我们大量创建不同类型的对象,统一集中管理。
-
应用场景:在不同场景需要创建不同实例时。
-
应用实例:使用工厂类创建不同类型的产品。
class ProductA {
constructor(name) {
this.name = name
}
produce() {
console.log("produce A is producing..")
return `produce A: ${this.name}`
}
}
class ProductB {
constructor(name) {
this.name = name
}
produce() {
console.log("produce B is producing..")
return `product B: ${this.name}`
}
}
class Factory {
create(type, name) {
switch (type) {
case "A":
return new ProductA(name)
case "B":
return new ProductB(name)
default:
throw new Error("不存在的产品类型")
}
}
}
// 使用
const factory = new Factory()
const productA = factory.create("A", "productA")
const productB = factory.create("B", "productB")
productA.produce() // produce A is producing..
productB.produce() // produce B is producing..
3.2 单例模式
-
目的:确保全局只有一个实例对象
-
应用场景:为了避免重复新建,避免多个对象存在相互干扰。(当需要一个对象去贯穿整个系统执行任务时才会用到单例模式,除此之外的场景应避免单例模式的使用。)
-
应用实例
class Singleton {
constructor() {
if (typeof Singleton.instance === "object") {
return Singleton.instance
}
this.name = "Singleton"
Singleton.instance = this
return this
}
}
const singleton1 = new Singleton()
const singleton2 = new Singleton()
console.log("对比:", singleton1 === singleton2) // true
4. 结构性模式
4.1 外观模式
外观模式隐藏系统的复杂性,并向外部提供了一个可以访问系统的接口。它向现有的系统添加一个接口,来隐藏系统的复杂性。
-
目的:通过为多个复杂的子系统提供一个一致的接口,隐藏系统的复杂性
-
应用场景:
(1)为复杂的模块或子系统提供外界访问的模块。
(2)子系统相对独立。 -
应用实例
(1) 应用外观模式封装一个统一的 DOM 元素事件绑定/取消方法,用于兼容不同版本的浏览器和更方便的调用
// 绑定事件
function addEvent(element, event, handler) {
if (element.addEventListener) {
element.addEventListener(event, handler, false)
} else if (element.attachEvent) {
element.attachEvent("on" + event, handler)
} else {
element["on" + event] = fn
}
}
// 取消绑定
function removeEvent(element, event, handler) {
if (element.removeEventListener) {
element.removeEventListener(event, handler, false)
} else if (element.detachEvent) {
element.detachEvent("on" + event, handler)
} else {
element["on" + event] = null
}
}
(2) 组织方法模块细化多个接口,并由外观类去进行执行调用
function model1 () {
// do something...
}
function model2 () {
// do something...
}
function use () {
model1()
model2()
}
4.2 代理模式
在代理模式中,一个类代表另一个类的功能。
-
目的:用一个代理对象来控制对另一个对象的访问
-
应用场景:
(1)想在访问一个类时做一些控制
(2)由于某些原因(比如对象创建开销很大,或者某些操作需要安全控制,或者需要进程外的访问),直接访问会给使用者或者系统结构带来很多麻烦,我们可以在访问此对象时加上一个对此对象的访问层。 -
应用实例:
代理加载图片类,若缓存中有,则直接返回缓存数据;若没有,则调用加载图片类。
class Image {
constructor(url) {
this.url = url
this.loadImage()
}
loadImage() {
console.log(`Loading image from ${this.url}`)
}
}
class ProxyImage {
constructor(url) {
this.url = url
}
loadImage() {
if (!this.image) {
this.image = new Image(this.url)
}
console.log(`Displaying cached image from ${this.url}`)
}
}
const image1 = new Image('https://example.com/image1.jpg')
const proxyImage1 = new ProxyImage('https://example.com/image1.jpg')
proxyImage1.loadImage(); // Loading image from https://example.com/image1.jpg
proxyImage1.loadImage(); // Displaying cached image from https://example.com/image1.jpg
5. 行为型模式
5.1 策略模式
在策略模式中,一个类的行为或其算法可以在运行时更改。我们创建表示各种策略的对象和一个行为随着策略对象改变而改变的 context 对象。策略对象改变 context 对象的执行算法。
-
目的:定义一系列的算法,把它们一个个封装起来, 并且使它们可相互替换。优化 if-else 分支。
-
应用场景:一个系统有许多许多类,而区分它们的只是他们直接的行为。
-
应用实例:
用策略模式将多种运算整合并判断
function Strategy (type,a,b) {
const Strategyer = {
add: function (a, b) {
return a + b
},
subtract: function (a, b) {
return a - b
},
multip: function (a, b) {
return a / b
},
}
return Strategyer[type](a, b)
}
5.2 迭代器模式
迭代器模式用于顺序访问集合对象的元素,不需要知道集合对象的底层表示。
迭代器模式解决了此些问题:
- 提供一致的遍历各种数据结构的方式,而不用了解数据的内部结构
- 提供遍历容器(集合)的能力而无需改变容器的接口
一个迭代器通常需要实现以下接口:
- hasNext():判断迭代是否结束,返回Boolean
- next():查找并返回下一个元素
-
目的:提供一种方法顺序访问一个聚合对象中各个元素, 而又无须暴露该对象的内部表示
-
应用场景:需要对某个对象进行操作,但是又不能暴露内部
-
应用实例
(1) 为 js 数组实现一个迭代器
const item = [1, 2, 3, 4, 5]
function Iterator(items) {
this.items = items;
this.index = 0;
}
Iterator.prototype = {
hasNext: function () {
return this.index < this.items.length;
},
next: function () {
return this.items[this.index++];
}
}
// use
const iterator = new Iterator(item);
while(iterator.hasNext()){
console.log('迭代器:',iterator.next()); // 1, 2, 3, 4, 5
}
(2)实现一个 Range 类用于在某个数字区间进行迭代
ES6 提供了更简单的迭代循环语法 for...of,使用该语法的前提是操作对象需要实现 可迭代协议(The iterable protocol),简单说就是该对象有个 Key 为
Symbol.iterator
的方法,该方法返回一个iterator
对象。
function Range(start, end) {
return {
[Symbol.iterator]: function () {
return {
next() {
if (start < end) {
return { value: start++, done: false }
}
return { value: end, done: true }
}
}
}
}
}
// use
for (const el of Range(1, 5)) {
console.log('el:', el) // 1, 2, 3, 4
}
5.3 观察者模式
观察者模式主要是定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。
被观察对象(subject)维护一组观察者(observer),当被观察对象状态改变时,通过调用观察者的某个方法将这些变化通知到观察者。
比如给 DOM 元素绑定事件的 addEventListener()
方法:
dom.addEventListener(type, listener [, options])
dom 就是被观察对象 Subject,listener 就是观察者 Observer。
观察者模式中 Subject 对象一般需要实现以下 API:
- subscribe(): 接收一个观察者 observer 对象,使其订阅自己
- unsubscribe(): 接收一个观察者 observer 对象,使其取消订阅自己
- fire(): 触发事件,通知到所有观察者
-
目的:一个对象状态改变给其他对象通知的问题,而且要考虑到易用和低耦合,保证高度的协作
-
应用场景:当两个模块直接沟通会增加它们的耦合性时
-
应用实例:
手动实现观察者模式
// 被观察者
function Subject() {
this.observers = []
}
Subject.prototype = {
// 订阅
subscribe: function (observer) {
this.observers.push(observer)
},
// 取消订阅
unsubscribe: function (observerToRemove) {
this.observers = this.observers.filter(observer => {
return observer !== observerToRemove
})
},
// 事件触发
fire: function () {
this.observers.forEach(observer => {
observer.call()
})
}
}
// use
const subject = new Subject()
const observer1 = () => {
console.log('observer1 触发了...')
}
const observer2 = () => {
console.log('observer2 触发了...')
}
subject.subscribe(observer1)
subject.subscribe(observer2)
subject.fire() // observer1 触发了... observer2 触发了...
5.4 中介者模式
用来降低多个对象和类之间的通信复杂性。这种模式提供了一个中介类,该类通常处理不同类之间的通信,并支持松耦合,使代码易于维护。
中介者模式和观察者模式有一定的相似性,都是一对多的关系,也都是集中式通信,不同的是中介者模式是处理
同级对象
之间的交互,而观察者模式是处理Observer
和Subject
之间的交互。
-
目的:用一个中介对象来封装一系列的对象交互,中介者使各对象不需要显式地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互。
-
应用场景:多个类相互耦合,形成了网状结构。
-
应用实例:
通过聊天室实例来演示中介者模式。实例中,多个用户可以向聊天室发送消息,聊天室向所有的用户显示消息。
// 聊天室成员类
class Member {
constructor(name) {
this.name = name
this.chatroom = null
}
// 发送消息
send (message, toMember) {
this.chatroom.send(message, this, toMember)
}
// 接收消息
receive (message, fromMember) {
console.log(`${fromMember.name} to ${this.name}: ${message}`)
}
}
// 聊天室类
class Chatroom {
constructor() {
this.members = {}
}
// 增加成员
addMember (member) {
this.members[member.name] = member
member.chatroom = this
}
// 发送消息
send (message, fromMember, toMember) {
toMember.receive(message, fromMember)
}
}
// use
const chatroom = new Chatroom()
const John = new Member('John')
const Tom = new Member('Tom')
chatroom.addMember(John)
chatroom.addMember(Tom)
John.send('Hi Tom!', Tom) // John to Tom: Hi Tom!
5.5 访问者模式
使用了一个访问者类,它改变了元素类的执行算法。通过这种方式,元素的执行算法可以随着访问者改变而改变。
-
目的:解耦数据结构与数据操作。
-
应用场景:需要对一个对象结构中的对象进行很多不同的并且不相关的操作,而需要避免让这些操作"污染"这些对象的类,使用访问者模式将这些封装到类中。
-
应用实例:对于公司财务数据,财务人员关心收入与支出数据,而老板关心盈利数据
// 财务报表类
class Report {
constructor(income, cost, profit) {
this.income = income
this.cost = cost
this.profit = profit
}
}
// 老板类
class Boss {
get (data) {
console.log(`老板访问报表数据,盈利:${data}`)
}
}
// 财务人员类
class Account {
get (num1, num2) {
console.log(`财务人员访问报表数据,收入:${num1},支出: ${num2}`)
}
}
// 访问者类
function vistor(data, person) {
const handle = {
Boss: function (data) {
person.get(data.profit)
},
Account: function (data) {
person.get(data.income, data.cost)
}
}
handle[person.constructor.name](data)
}
// use
const report = new Report(1000, 500, 200);
vistor(report, new Account()) // 财务人员访问报表数据,收入:1000,支出: 500
vistor(report, new Boss()) // 老板访问报表数据,盈利:200
以上是一些常见的设计模式,但仅冰山一角,还有很多的设计模式可应用于不同的场景。了解或熟悉这些设计模式或许可以潜移默化地提升我们的开发水平和效率。