· client · 12 min 阅读
常见的 JS 设计模式-上
学习常见的 11 种 JS 设计模式
11 种常见的 JS 设计模式-上
- 单例模式(Singleton Pattern)
- 工厂模式(Factory Pattern)
- 构造函数模式(Constructor Pattern)
- 模块模式(Module Pattern)
- 观察者模式(Observer Pattern)
- 代理模式(Proxy Pattern)
- 装饰器模式(Decorator Pattern)
- 适配器模式(Adapter Pattern)
- 命令模式(Command Pattern)
- 迭代器模式(Iterator Pattern)
- 发布-订阅模式(Publish-Subscribe Pattern)
1. 单例模式
用于确保一个类只有一个实例,并提供一个全局访问点。这在许多情况下都是有用的,例如全局状态管理、数据库连接、日志记录器等。
优点:
- 节省内存: 单例模式确保只有一个实例存在,这可以节省内存空间。
- 全局访问点: 可以通过单例模式提供的全局访问点来访问实例,简化了对象的访问。
- 避免重复实例化: 单例模式确保只有一个实例存在,避免了重复实例化带来的性能损耗。
缺点:
- 全局状态: 单例模式会引入全局状态,可能导致代码的耦合度增加,使得代码难以维护和测试。
- 隐藏依赖: 全局访问点可能会隐藏依赖关系,使得代码更难理解和测试。
- 并发风险: 如果单例模式没有考虑并发访问的情况,可能会引发并发风险,需要额外的同步机制来解决。
实现方式:
-
使用字面量方式创建单例:
const signleton = { property1: 'value1', property2: 'value2', method: function () { // do something } }
-
使用函数闭包创建单例:
const Singleton = (function () { let instance function init() { // private methods and variables return { // public methods and variables } } return { getInstance: function () { if (!instance) { instance = init() } return instance } } })()
-
使用类创建单例:
class Singleton { constructor() { if (!Singleton.instance) { // initialization code Singleton.instance = this } return Singleton.instance } // methods } // 使用 const singletonInstance = new Singleton()
2. 工厂模式
通过定义一个公告的接口来创建对象,但是具体创建哪种类型的对象则由工厂方法来决定。工厂模式隐藏了对象的创建逻辑,使得客户端代码不需要直接实例化对象,而是通过调用工厂的方法来获取所需的对象实例。
优点:
- 封装性高: 工厂模式封装了对象的创建过程,使得客户端不需要知道具体的创建细节,降低了代码的耦合度。
- 灵活性: 工厂模式可以根据需要动态创建不同类型的对象,从而实现灵活的对象创建。
- 可维护性: 由于对象的创建逻辑集中在工厂方法中,修改创建逻辑只需要修改工厂方法,不会影响客户端代码,提高了代码的可维护性。
- 利于拓展: 可以随时增加新的对象类型,而不需要修改客户端代码。
缺点:
- 增加复杂度: 引入工厂模式会增加代码的复杂度,因为需要额外定义工厂方法来创建对象。
- 难以追踪对象来源: 使用工厂模式创建的对象,很难追踪对象的具体来源,因为对象的创建过程被封装在工厂方法内部。
实现方式:
-
简单工厂模式:
function createCar(type) { if (type === 'SUV') { return new SUV() } else if (type === 'Sedan') { return new Sedan() } else if (type === 'Hatchback') { return new Hatchback() } else { throw new Error('Invalid car type') } } const myCar = createCar('SUV')
-
工厂方法模式:
class CarFactory { createCar(type) { if (type === 'SUV') { return new SUV() } else if (type === 'Sedan') { return new Sedan() } else if (type === 'Hatchback') { return new Hatchback() } else { throw new Error('Invalid cat type') } } } // 使用 const factory = new CarFactory() const myCar = factory.createCar('SUV')
-
抽象工厂模式:
class CarFactory { createSUV() { return new SUV() } createSedan() { return new Sedan() } createHatchback() { return new Hatchback() } } // 使用 const factory = new CarFactory() const mySUV = factory.createSUV() const mySedan = factory.createSedan() const myHatchback = factory.createHatchback()
3. 构造函数模式
一种用于创建对象的方式,它使用构造函数来定义对象的属性和方法,并通过 new
关键字来创建对象实例。
优点:
- 简单易懂: 构造函数模式是一种简单直观的创建对象方式,易于理解和使用。
- 支持继承: 可以通过在构造函数的原型对象上定义方法,实现对象之间的继承关系。
- 封装性: 可以利用构造函数内部的局部变量和方法实现对象的封装,保护数据安全性。
缺点:
- 每个实例都会创建新的方法: 每次使用构造函数创建对象实例时,都会重新创建一组新的方法,可能会占用较多内存。
- 无法复用方法: 每个实例都会拥有一组新的方法,无法实现方法的复用,可能会导致内存浪费。
实现方式:
// 构造函数模式的实现
function Person(name, age) {
this.name = name
this.age = age
this.sayHello = function () {
console.log('Hello my name is ' + this.name + ' and I am' + this.age + ' years old.')
}
}
// 使用构造函数创建对象实例
var person1 = new Person('Alice', 30)
var parson2 = new Person('Bob', 25)
// 调用对象实例的方法
person1.sayHello()
person2.sayHello()
它的实现非常简单,只需要定义一个构造函数,然后通过 new
关键字来创建对象实例。
但是需要注意的是,尽管在构造函数内部定义的方法可以访问构造函数内部的属性,但是每次创建对象实例时,都会重新创建一组新的方法,可能会占用较多内存。如果方法可以在所有实例之间共享,应该将它们定义在构造函数的原型对象上,以节省内存。
4. 模块模式
一种用于组织和封装代码的模式,它使用闭包来创建私有作用域,从而避免全局命名空间污染,并提供一种方式来导出公共接口供外部访问。模块模式旨在解决 JavaScript 中的模块化和封装性的问题,使得代码更易于维护和拓展。
优点:
- 封装性: 使用模块模式可以创建私有作用域,将变量和函数封装在模块内部,避免了全局命名空间的污染,提供了代码的可维护性和安全性。
- 代码组织: 模块模式可以将相关的功能组织在一起,使得代码结构更清晰、更易于理解。
- 可重用性: 模块模式可以将一组相关的功能封装在一个模块中,并在其他地方重复使用,提高了代码的可重用性。
- 适应性: 模块模式适用于各种规模的项目,可以根据需要创建不同大小的模块。
缺点:
- 可测试性: 私有作用域难以被测试,因为无法直接访问模块内部的私有变量和函数,需要借助特殊的测试工具或技术来测试模块的私有部分。
- 增加复杂度: 使用模块模式会增加一定的复杂度,特别是对于初学者来说,可能会增加理解和学习的难度。
- 内存占用: 每个模块都会创建一个闭包,可能会导致内存占用增加,特别是大型项目中。
实现方式:
const Module = (function () {
// 私有变量和函数
let privateVar = 0
function privateFunction() {
console.log('Private Function')
}
// 公共接口
return {
publicVar: 1,
publicFunction: function () {
console.log('Public Function')
}
}
})()
// 使用模块
Module.publicFunction()
console.log(Module.publicVar)
在上面的例子中,Module
是一个使用模块模式实现的模块,它包含了私有变量 privateVar
和私有函数 privateFunction
,以及公共接口 publicVar
和 publicFunction
。通过返回一个包含公共接口的对象,外部可以访问模块提供的公共方法和变量,但无法直接访问模块内部的私有吧部分。
5. 观察者模式
用于定义对象间的一种一对多的依赖关系,当一个对象的状态发生变化时,所有依赖于它的对象都会得到通知并自动更新。在观察者模式中,有两种角色:观察者(observer)和被观察者(Subject)。
优点:
- 解耦性: 观察者模式将目标和观察者之间的关系解耦,使得它们可以独立变化,降低了对象之间的耦合性。
- 拓展性: 可以根据需要动态地增加和删除观察者,而不影响被观察者或其他观察者。
- 灵活性: 被观察者只关注观察者的接口,而不需要知道具体观察者的实现,使得系统更加灵活。
缺点:
- 可能引起循环调用: 如果观察者之间存在循环依赖,可能会导致循环调用,增加程序的复杂性。
- 可能导致性能问题: 当被观察者对象有大量的观察者时,通知每个观察者可能会导致性能问题。
实现方式:
// 观察者对象
class Observer {
update() {
// 处理被观察者发出的通知
}
}
// 被观察者对象
class Subject {
constructor() {
this.observers = []
}
addObserver(observer) {
this.observers.push(observer)
}
removeObserver(observer) {
this.observers = this.observers.filter(obs => obs !== observer)
}
notifyObservers() {
this.observers.forEach(observer => observer.update())
}
}
// 使用示例
const subject = new Subject()
const observer1 = new Observer()
const observer2 = new Observer()
subject.addObserver(observer1)
subject.addObserver(observer2)
// 被观察者状态发生变化,通知观察者
subject.notifyObservers()