设计模式
设计模式总的来说是⼀个抽象的概念, 通过无数次的实践总结出的⼀套写代码的方式, 通过这种方式写的代码可以让别⼈更加容易阅读 、维护以及复用。
设计模式(Design pattern)是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。
使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性、程序的重用性。
更多详细内容,请微信搜索“前端爱好者
“, 戳我 查看 。
工厂模式
在 JavaScript 中,工厂模式是一种创建对象的常见方式。
工厂模式是对象创建型模式,它提供了创建对象的接口,但具体的对象创建逻辑由子类决定。
class Man {
constructor(name) {
this.name = name
}
alertName() {
alert(this.name)
}
}
class Factory {
static create(name) {
return new Man(name)
}
}
Factory.create( 'yck').alertName()
当然工厂模式并不仅仅是用来 new 出实例。
可以想象⼀个场景。
假设有⼀份很复杂的代码需要用户去调用,但是用户并不关⼼这些复杂的代码, 只需要你提供给我⼀个接口去调用,用户只负责传递需要的参数, 至于这些参数怎么使用, 内部有什么逻辑是不关⼼的, 只需要你最后返回我⼀个实例 。这个构造过程就是工厂。
工厂起到的作用就是隐藏了创建实例的复杂度, 只需要提供⼀个接口, 简单清晰。
在 Vue 源码中,你也可以看到工厂模式的使用, 比如创建异步组件
export function createComponent (
Ctor: Class<Component> | Function | Object | void,
data: ?VNodeData,
context: Component,
children: ?Array<VNode>,
tag?: string
): VNode | Array<VNode> | void {
// 逻辑处理 ...
const vnode = new VNode(
`vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
data, undefined, undefined, undefined, context,
{ Ctor, propsData, listeners, tag, children },
asyncFactory
)
return vnode
}
在上述代码中, 我们可以看到我们只需要调用createComponent 传⼊参数就能创建⼀个组件实例
但是创建这个实例是很复杂的⼀个过程, 工厂帮助我们隐藏了这个复杂的过程, 只需要⼀句代码调用就能实现功能
function CarMaker(model) {
var car;
this.produce = function () {
if (typeof car === 'undefined') {
car = new model();
console.log("A " + model.name + " is created.");
} else {
console.log("This " + model.name + " has already been created.");
}
return car;
};
}
var audi = {
name: 'Audi',
color: 'blue',
speed: 120
};
var bmw = {
name: 'BMW',
color: 'black',
speed: 180
};
var makerAudi = new CarMaker(audi);
makerAudi.produce(); // 输出:"A Audi is created."
makerAudi.produce(); // 输出:"This Audi has already been created."
var makerBmw = new CarMaker(bmw);
makerBmw.produce(); // 输出:"A BMW is created."
makerBmw.produce(); // 输出:"This BMW has already been created."
在这个例子中,CarMaker 是一个工厂类,它接收一个模型参数(model),并有一个 produce 方法用于创建模型对象。
当 produce 方法被调用时,如果还没有创建对象,就会创建一个新的模型对象,并且打印出一条消息说明创建了什么对象。
如果对象已经被创建,就会打印出一条消息说明该对象已经被创建。
我们可以创建 CarMaker 的子类来根据需要创建不同类型的对象。
这就是工厂模式的基本思想。
单例模式
单例模式很常用, 比如 全局缓存 、全局状态管理 等等这些只需要⼀个对象,就可以使用单例模式。
单例模式的核心就是保证全局只有⼀个对象可以访问
。
因为 JS 是门无类的语言,所以别的语言实现单例的方式并不能套⼊ JS 中, 我们只需要用⼀个变量确保实例只创建⼀次就行
以下是如何实现单例模式的例⼦
class Singleton {
constructor() {}
}
Singleton.getInstance = (function() {
let instance
return function() {
if (!instance) {
instance = new Singleton()
}
return instance
}
})()
let s1 = Singleton.getInstance()
let s2 = Singleton.getInstance()
console.log(s1 === s2) // true
在 Vuex 源码中,你也可以看到单例模式的使用, 虽然它的实现方式不大⼀样, 通过⼀个外部变量来控制只安装⼀次 Vuex
let Vue / / bind on install
export function install (_Vue) {
if (Vue && _Vue === Vue) {
// 如果发现 Vue 有值,就不重新创建实例了
return
}
Vue = _Vue
applyMixin(Vue)
}
适配器模式
适配器用来解决两个接口不兼容的情况,不需要改变已有的接口, 通过包装⼀层的方式实现两个接口的正常协作。
以下是如何实现适配器模式的例子
class Plug {
getName() {
return '港版插头 '
}
}
class Target {
constructor() {
this.plug = new Plug()
}
getName() {
return this.plug.getName() + ' 适配器转⼆脚插头 '
}
}
let target = new Target()
target.getName() // 港版插头 适配器转⼆脚插头
在 Vue 中, 我们其实经常使用到适配器模式 。
比如父组件传递给子组件⼀个时间戳属性,组件内部需要将时间戳转为正常的日期显示,⼀般会使用computed 来做转换这件事情, 这个过程就使用到了适配器模式
一个基本的适配器模式的JavaScript实现:
class Adaptee {
specificRequest() {
console.log('Adaptee specific request');
}
}
class Target {
request() {
console.log('Target request');
}
}
class Adapter extends Adaptee {
constructor(adaptee) {
this.adaptee = adaptee;
}
request() {
console.log('Adapter request');
this.adaptee.specificRequest();
}
}
let adaptee = new Adaptee();
let adapter = new Adapter(adaptee);
adapter.request(); // Outputs: "Adapter request" and "Adaptee specific request"
在这个例子中,Adaptee类有一个特定的specificRequest方法,而Target类需要一个与其request方法兼容的adaptee对象。\
Adapter类继承自Adaptee,并且有一个request方法,该方法调用Adaptee的specificRequest方法以满足Target的request方法的需求。
这样,我们就创建了一个适配器,将Adaptee的接口转换为Target所期望的接口。
在JavaScript中,我们也可以使用对象组合的方式
来实现适配器模式:
class Adaptee {
specificRequest() {
console.log('Adaptee specific request');
}
}
class Target {
request(adaptee) {
console.log('Target request');
adaptee.specificRequest();
}
}
let adaptee = new Adaptee();
let target = new Target();
target.request(adaptee); // Outputs: "Target request" and "Adaptee specific request"
在这个例子中,我们直接将Adaptee对象传递给Target的request方法。
这种实现方式更灵活,因为我们可以动态地改变Adaptee对象。
装饰模式
装饰模式是一种设计模式,它允许你在不改变对象自身的基础上,动态地给对象添加新的功能。
这主要是通过创建一个装饰类,该类继承了原始对象,并包含原始对象的一个实例。
然后,这个装饰类可以添加额外的功能,最后调用原始对象的方法。
在JavaScript中,装饰模式可以通过以下方式实现:
function Car(make, model, year) {
this.make = make;
this.model = model;
this.year = year;
}
Car.prototype.honk = function() {
console.log('Beep Beep!');
};
function CarDecorator(car) {
this.car = car;
}
CarDecorator.prototype.honk = function() {
console.log('Decorated Honk: ');
this.car.honk();
};
let myCar = new Car('Toyota', 'Corolla', 2020);
let decoratedCar = new CarDecorator(myCar);
decoratedCar.honk(); // Outputs: Decorated Honk: Beep Beep!
在这个例子中,Car类有一个honk方法,这是一个简单的对象创建和功能调用。
然后我们创建了一个CarDecorator类,它接受一个Car对象并在原有功能上添加额外的功能。
最后我们创建了一个新的CarDecorator实例,并调用了它的honk方法。
这个方法首先打印出"Decorated Honk: ",然后调用原始Car对象的honk方法。
这样我们就成功地在不改变Car类的基础上添加了新的功能。
因此,在JavaScript中,装饰器函数需要接受一个函数作为参数,并返回一个新的函数。
这使得在JavaScript中使用装饰器模式相比其他一些语言更为复杂。
代理模式
代理是为了控制对对象的访问,不让外部直接访问到对象 。在现实生活中,也有很多代理的场景
比如你需要买⼀件国外的产品, 这时候你可以通过代购来购买产品。
在实际代码中其实代理的场景很多,也就不举框架中的例子了, 比如事件代理就用到了代理模式
<ul id="ul">
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
<li>5</li>
</ul>
<script>
let ul = document.querySelector( '#ul')
ul.addEventListener( 'click', (event) => {
onsole.log(event.target);
})
</script>
因为存在太多的 li,不可能每个都去绑定事件。
这时候可以通过给父节点绑定⼀个事件,让父节点作为代理去拿到真实点击的节点
发布-订阅模式
发布-订阅模式(Publish-Subscribe Pattern)是一种常见的设计模式,广泛应用于前端开发中。
这种模式定义了一种一对多的依赖关系,让多个订阅者(Subscribers)能够接收到发布者(Publishers)发布的信息。
在发布-订阅模式中,发布者负责发布消息,而订阅者则订阅感兴趣的主题。当发布者发布消息时,所有订阅了该主题的订阅者都会收到相应的消息。
下面是一个简单的发布-订阅模式实现示例:
class Publisher {
constructor() {
this.subscribers = [];
}
subscribe(subscriber) {
this.subscribers.push(subscriber);
}
publish(message) {
this.subscribers.forEach((subscriber) => {
subscriber.receive(message);
});
}
}
class Subscriber {
constructor(name) {
this.name = name;
}
receive(message) {
console.log(`${this.name} received message: ${message}`);
}
}
const publisher = new Publisher();
const subscriber1 = new Subscriber("Subscriber 1");
const subscriber2 = new Subscriber("Subscriber 2");
const subscriber3 = new Subscriber("Subscriber 3");
publisher.subscribe(subscriber1);
publisher.subscribe(subscriber2);
publisher.subscribe(subscriber3);
publisher.publish("Hello, subscribers!");
在这个例子中,我们创建了一个Publisher
类和Subscriber
类。Publisher
类负责管理订阅者列表,并提供subscribe
和publish
方法。Subscriber
类表示订阅者,每个订阅者都有自己的名字,并且可以接收并处理消息。
首先,我们创建了一个Publisher
实例和三个Subscriber
实例。然后,我们让这三个订阅者订阅发布者,当发布者发布消息时,所有订阅了该消息的订阅者都会接收到消息,并打印出自己的名字和收到的消息。
在现代前端框架(如React、Vue等)中,发布-订阅模式通常被用于实现组件间通信、状态管理和响应式编程等功能。
这些框架都提供了相应的工具和机制来简化发布-订阅模式的实现,例如事件总线、观察者模式、Redux等。
外观模式
外观模式提供了⼀个接⼝,隐藏了内部的逻辑,更加方便外部调用。
个例⼦来说, 我们现在需要实现⼀个兼容多种浏览器的添加事件方法
function addEvent(elm, evType, fn, useCapture) {
if (elm.addEventListener) {
elm.addEventListener(evType, fn, useCapture)
return true
} else if (elm.attachEvent) {
var r = elm.attachEvent("on" + evType, fn)
return r
} else {
elm["on" + evType] = fn
}
}
对于不同的浏览器,添加事件的方式可能会存在兼容问题 。
如果每次都需要去这样写⼀遍的话肯定是不能接受的,所以我们将这些判断逻辑统⼀封装在⼀个接口中,外部需要添加事件只需要调用 addEvent 即可。