前端设计模式大全
1. 工厂模式
工厂模式(Factory Pattern):将对象的创建和使用分离,由工厂类负责创建对象并返回。在前端开发中,可以使用工厂模式来动态创建组件。
前端中的工厂模式是一种创建对象的设计模式,它可以让我们封装创建对象的细节,我们使用工厂方法而不是直接调用 new 关键字来创建对象,使得代码更加清晰、简洁和易于维护。在前端开发中,工厂模式通常用于创建多个相似但稍有不同的对象,比如创建一系列具有相同样式和行为的按钮或者表单。
在实现工厂模式时,通常需要创建一个工厂函数(或者叫做工厂类),该函数可以接受一些参数,并根据这些参数来创建对象。例如,我们可以创建一个ButtonFactory函数,它接受一个type参数,用于指定按钮的类型,然后根据type参数创建不同类型的按钮对象。示例代码如下:
function ButtonFactory(type) { switch (type) { case 'primary': return new PrimaryButton(); case 'secondary': return new SecondaryButton(); case 'link': return new LinkButton(); default: throw new Error('Unknown button type: ' + type); } } function PrimaryButton() { this.type = 'primary'; this.text = 'Click me!'; this.onClick = function() { console.log('Primary button clicked!'); }; } function SecondaryButton() { this.type = 'secondary'; this.text = 'Click me too!'; this.onClick = function() { console.log('Secondary button clicked!'); }; } function LinkButton() { this.type = 'link'; this.text = 'Click me as well!'; this.onClick = function() { console.log('Link button clicked!'); }; }
在上面的示例中,ButtonFactory函数接受一个type参数,根据这个参数来创建不同类型的按钮对象。例如,如果type为primary,则返回一个PrimaryButton对象,该对象具有type、text和onClick属性,表示一个主要按钮。其他类型的按钮也类似。
使用工厂模式可以让我们将对象创建的过程与具体的业务逻辑分离开来,从而提高代码的可重用性和可维护性。
2. 单例模式
单例模式(Singleton Pattern):保证一个类只有一个实例,并提供一个访问它的全局访问点。在前端开发中,可以使用单例模式来管理全局状态和资源。
在JavaScript中,单例模式可以通过多种方式实现,以下是一些常见的实现方式:
- 对象字面量
使用对象字面量可以轻松地创建单例对象,例如:
const singleton = { property1: "value1", property2: "value2", method1: function () { // ... }, method2: function () { // ... }, };
上述代码中,使用了一个对象字面量来创建单例对象,该对象包含了一些属性和方法。由于JavaScript中对象字面量本身就是单例的,因此不需要额外的代码来保证单例。
- 构造函数
在JavaScript中,每个构造函数都可以用于创建单例对象,例如:
function Singleton() { // 判断是否存在实例 if (typeof Singleton.instance === "object") { return Singleton.instance; } // 初始化单例对象 this.property1 = "value1"; this.property2 = "value2"; Singleton.instance = this; } const instance1 = new Singleton(); const instance2 = new Singleton(); console.log(instance1 === instance2); // 输出 true
上述代码中,使用了一个构造函数来创建单例对象。在构造函数中,首先判断是否存在单例实例,如果存在则直接返回该实例,否则创建单例对象并将其保存在 Singleton.instance
属性中。由于JavaScript中每个构造函数本身就是一个单例,因此不需要额外的代码来保证单例。
- 模块模式
使用模块模式可以创建一个只有单个实例的对象,例如:
const Singleton = (function () { let instance; function init() { // 创建单例对象 const object = new Object("I am the instance"); return object; } return { getInstance: function () { if (!instance) { instance = init(); } return instance; }, }; })(); const instance1 = Singleton.getInstance(); const instance2 = Singleton.getInstance(); console.log(instance1 === instance2); // 输出 true
上述代码中,使用了一个立即执行函数来创建单例对象。在该函数中,定义了一个私有变量 instance
用于存储单例实例,而 init
函数则是用于创建单例实例的方法。最后,返回一个对象,该对象包含一个 getInstance
方法,该方法用于获取单例实例。
通过上述方式实现的单例模式,可以确保在程序运行期间,某个类只有一个实例,并且该实例可以在任何地方访问。
3. 发布-订阅模式
发布-订阅模式(Publish-Subscribe Pattern):也叫消息队列模式,它是一种将发布者和订阅者解耦的设计模式。在前端开发中,可以使用发布-订阅模式来实现组件之间的通信。
JavaScript中的发布/订阅模式(Pub/Sub)是一种常用的设计模式。它允许在应用程序中定义对象之间的一对多的依赖关系,当一个对象的状态发生变化时,所有依赖于它的对象都会被通知和更新。
在发布/订阅模式中,有两种类型的对象:发布者和订阅者。发布者是事件的发出者,它通常维护一个事件列表,并且可以向列表中添加或删除事件。当某个事件发生时,它会将这个事件通知给所有订阅者。订阅者则是事件的接收者,它们订阅感兴趣的事件,并且在事件发生时接收通知。。
发布订阅模式可以帮助我们实现松耦合的设计,让对象之间的依赖关系变得更加灵活。它在前端开发中的应用非常广泛,例如 Vue.js 中的事件总线、Redux 中的 store 等。
以下是一个简单的实现发布/订阅模式的示例代码:
// 定义一个发布者对象 var publisher = { // 定义一个事件列表 events: {}, // 添加事件到列表中 addEvent: function(event, callback) { if (!this.events[event]) { this.events[event] = []; } this.events[event].push(callback); }, // 从事件列表中删除事件 removeEvent: function(event, callback) { if (this.events[event]) { for (var i = 0; i < this.events[event].length; i++) { if (this.events[event][i] === callback) { this.events[event].splice(i, 1); break; } } } }, // 发布事件 publishEvent: function(event, data) { if (this.events[event]) { for (var i = 0; i < this.events[event].length; i++) { this.events[event][i](data); } } } }; // 定义一个订阅者对象 var subscriber = { // 处理事件的回调函数 handleEvent: function(data) { console.log(data); } }; // 订阅一个事件 publisher.addEvent('event1', subscriber.handleEvent); // 发布一个事件 publisher.publishEvent('event1', 'Hello, world!'); // 取消订阅一个事件 publisher.removeEvent('event1', subscriber.handleEvent);
在这个例子中,发布者对象维护了一个事件列表(events),并且提供了添加、删除和发布事件的方法。订阅者对象则提供了一个处理事件的回调函数(handleEvent),它可以被添加到发布者对象的事件列表中。当发布者发布一个事件时,所有订阅了这个事件的订阅者都会收到通知,并执行相应的处理函数。
4. 观察者模式
观察者模式(Observer Pattern):当对象间存在一对多的关系时,使用观察者模式。当被观察的对象发生变化时,其所有的观察者都会收到通知并进行相应的操作。在JavaScript中,可以使用回调函数或事件监听来实现观察者模式。
在前端开发中,观察者模式常被用来实现组件间的数据传递和事件处理。比如,当一个组件的状态发生改变时,可以通过观察者模式来通知其他组件更新自身的状态或视图。
在观察者模式中,通常会定义两种角色:
- Subject(主题):它是被观察的对象,当其状态发生改变时会通知所有的观察者。
- Observer(观察者):它是观察主题的对象,当主题状态发生改变时会接收到通知并进行相应的处理。
以下是一个简单的实现示例:
class Subject { constructor() { this.observers = [] } addObserver(observer) { this.observers.push(observer) } removeObserver(observer) { this.observers = this.observers.filter(obs => obs !== observer) } notify(data) { this.observers.forEach(obs => obs.update(data)) } } class Observer { update(data) { console.log(`Received data: ${data}`) } } // Usage const subject = new Subject() const observer1 = new Observer() const observer2 = new Observer() subject.addObserver(observer1) subject.addObserver(observer2) subject.notify('Hello, world!') // Output: // Received data: Hello, world! // Received data: Hello, world! subject.removeObserver(observer1) subject.notify('Goodbye, world!') // Output: // Received data: Goodbye, world!
在上面的示例中,我们定义了一个 Subject 类和一个 Observer 类。Subject 类有三个方法,addObserver 用于添加观察者,removeObserver 用于移除观察者,notify 用于通知所有观察者。
Observer 类只有一个方法 update,用于接收主题传递的数据。我们创建了两个 Observer 实例并将它们添加到了 Subject 实例中,然后调用了 notify 方法来通知它们更新数据。
在实际开发中,我们通常会使用现成的库或框架来实现观察者模式,比如 React 中的状态管理库 Redux 和事件处理库 EventEmitter。
5. 中介者模式
中介者模式(Mediator Pattern):通过一个中介对象来封装一系列对象之间的交互。在JavaScript中,可以使用事件调度器来实现中介者模式。
在前端开发中,中介者模式常常被用于管理复杂的用户界面或组件之间的交互,比如 GUI 组件、聊天室、游戏等等。通过引入一个中介者对象,各个组件可以向中介者对象发送消息或事件,而不需要知道消息或事件的接收者是谁。中介者对象负责接收并分发消息或事件,从而实现组件之间的解耦和统一管理。
下面是一个简单的例子,展示了如何在前端中使用中介者模式:
// 中介者对象 const Mediator = { components: [], addComponent(component) { this.components.push(component); }, broadcast(source, message) { this.components .filter(component => component !== source) .forEach(component => component.receive(message)); } }; // 组件对象 class Component { constructor() { this.mediator = Mediator; this.mediator.addComponent(this); } send(message) { this.mediator.broadcast(this, message); } receive(message) { console.log(`Received message: ${message}`); } } // 使用中介者模式进行组件之间的通信 const componentA = new Component(); const componentB = new Component(); componentA.send("Hello from Component A"); componentB.send("Hi from Component B"); // Received message: Hello from Component A // Received message: Hi from Component B
在上面的例子中,我们定义了一个中介者对象 Mediator
和两个组件对象 ComponentA
和 ComponentB
。当组件对象发送消息时,它会将消息发送给中介者对象,中介者对象负责将消息分发给其他组件对象。这样,我们就实现了组件之间的解耦和统一管理。
需要注意的是,在实际开发中,我们可能需要使用不同的中介者对象来管理不同的组件之间的交互行为。此外,我们还可以使用其他方式来实现中介者模式,比如使用观察者模式来实现。
6. 装饰者模式
装饰者模式(Decorator Pattern):动态地给一个对象添加额外的职责。在前端开发中,可以使用装饰者模式来动态修改组件的行为和样式。
JavaScript 中的装饰者模式可以通过以下几种方式实现:
- 通过扩展对象的属性或方法来实现装饰者模式
// 定义一个原始对象 const obj = { foo() { console.log('foo'); } } // 定义一个装饰函数,用于扩展原始对象的方法 function barDecorator(obj) { obj.bar = function() { console.log('bar'); } return obj; } // 使用装饰函数来扩展原始对象 const decoratedObj = barDecorator(obj); decoratedObj.foo(); // 输出 "foo" decoratedObj.bar(); // 输出 "bar"
在上面的示例中,我们首先定义了一个原始对象 obj
,它包含一个方法 foo
。然后,我们定义了一个装饰函数 barDecorator
,它接收一个对象作为参数,用于为这个对象添加一个新的方法 bar
。最后,我们使用 barDecorator
函数来扩展原始对象 obj
,并得到了一个新的对象 decoratedObj
,它包含了原始对象的方法 foo
和新的方法 bar
。
- 通过扩展对象的原型来实现装饰者模式
// 定义一个原始对象 function Foo() {} // 在原型上定义一个方法 Foo.prototype.foo = function() { console.log('foo'); } // 定义一个装饰函数,用于扩展原型的方法 function barDecorator(clazz) { clazz.prototype.bar = function() { console.log('bar'); } } // 使用装饰函数来扩展原型 barDecorator(Foo); // 创建一个新的对象,并使用扩展后的方法 const obj = new Foo(); obj.foo(); // 输出 "foo" obj.bar(); // 输出 "bar"
在上面的示例中,我们首先定义了一个原始对象 Foo
,它是一个构造函数,用于创建一个对象。然后,我们在原型上定义了一个方法 foo
。接着,我们定义了一个装饰函数 barDecorator
,它接收一个构造函数作为参数,用于在原型上添加一个新的方法 bar
。最后,我们使用 barDecorator
函数来扩展原始对象的原型,然后创建一个新的对象 obj
,并使用扩展后的方法 foo
和 bar
。
需要注意的是,装饰者模式可以嵌套使用,也就是说,我们可以通过多个装饰函数来依次为一个组件添加多个不同的功能。同时,装饰者模式也可以用于对已有的组件进行扩展,使得我们可以在不改变原有代码的情况下,给组件添加新的行为和样式。
7. 策略模式
策略模式(Strategy Pattern):定义一系列的算法,将每一个算法都封装起来,并且使它们可以相互替换。在前端开发中,可以使用策略模式来动态切换组件的算法和行为。
它可以让我们在不改变对象本身的情况下,通过修改其内部的算法实现不同的行为。策略模式常常被用于实现一些复杂的业务逻辑,特别是需要根据不同的条件进行处理的情况。
下面是一个简单的示例,演示了如何使用策略模式来实现一个计算器:
// 定义一个策略对象 const strategies = { add: function(num1, num2) { return num1 + num2; }, subtract: function(num1, num2) { return num1 - num2; }, multiply: function(num1, num2) { return num1 * num2; }, divide: function(num1, num2) { return num1 / num2; } }; // 定义一个计算器对象 const Calculator = function(strategy) { this.calculate = function(num1, num2) { return strategy(num1, num2); } }; // 使用策略模式来创建一个计算器对象 const addCalculator = new Calculator(strategies.add); const subtractCalculator = new Calculator(strategies.subtract); const multiplyCalculator = new Calculator(strategies.multiply); const divideCalculator = new Calculator(strategies.divide); // 使用计算器对象进行计算 console.log(addCalculator.calculate(10, 5)); // 输出 15 console.log(subtractCalculator.calculate(10, 5)); // 输出 5 console.log(multiplyCalculator.calculate(10, 5)); // 输出 50 console.log(divideCalculator.calculate(10, 5)); // 输出 2
在上面的示例中,我们首先定义了一个策略对象,其中包含了四个不同的算法:加、减、乘和除。然后我们定义了一个计算器对象,它接收一个策略对象作为参数,并将其保存在内部。最后,我们使用策略模式来创建四个不同的计算器对象,每个对象使用不同的算法进行计算。
这个示例展示了如何使用策略模式来实现一个简单的计算器,但实际上它可以应用于许多其他的场景中,例如表单验证、图表绘制等。策略模式可以让我们通过修改策略对象来改变对象的行为,从而实现更加灵活和可扩展的代码。
8. 适配器模式
适配器模式(Adapter Pattern):将一个类的接口转化为客户端所期望的接口,使得原本不兼容的类可以一起工作。在前端开发中,可以使用适配器模式来处理不同浏览器之间的兼容性问题。
适配器模式通常包含三个角色:客户端、目标对象和适配器对象。客户端调用适配器对象的接口,适配器对象再调用目标对象的接口,将目标对象的接口转换为客户端需要的接口,从而实现兼容性。
另外,适配器模式也可以用于将不同的第三方组件或插件进行整合和兼容。例如,当一个网站需要使用不同的图表库来绘制图表时,可以使用适配器模式将这些图表库进行封装,从而实现统一的调用接口,方便使用和维护。
下面是一个简单的例子,演示如何使用适配器模式将不同的 API 接口进行统一封装:
// 目标接口 class Target { request() { return 'Target: 请求完成!'; } } // 需要适配的对象 class Adaptee { specificRequest() { return 'Adaptee: 请求完成!'; } } // 适配器对象 class Adapter extends Target { constructor(adaptee) { super(); this.adaptee = adaptee; } request() { const result = this.adaptee.specificRequest(); return `Adapter: ${result}`; } } // 使用适配器模式 const adaptee = new Adaptee(); const adapter = new Adapter(adaptee); console.log(adapter.request()); // 输出:Adapter: Adaptee: 请求完成!
在上面的代码中,我们定义了一个目标接口 Target
和一个需要适配的对象 Adaptee
,它们之间的接口不兼容。然后我们使用适配器模式,将 Adaptee
对象适配为 Target
接口,从而实现了兼容性。
适配器对象 Adapter
继承了目标接口 Target
,并在其内部使用了需要适配的对象 Adaptee
。在 Adapter
的 request
方法中,我们调用了 Adaptee
的 specificRequest
方法,将其返回值包装为符合 Target
接口的形式。
通过适配器模式,我们可以将不同接口的对象进行统一封装,从而方便我们使用和维护代码。
9. 职责链模式
职责链模式(Chain of Responsibility Pattern):为了避免请求发送者与多个请求处理者耦合在一起,将所有请求的处理者通过前一对象记住其下一个对象的方式连成一条链,然后请求沿着链传递,直到有一个对象处理它为止。在JavaScript中,可以使用函数对象或对象字面量来实现职责链模式。
职责链模式通常涉及一系列处理对象,每个对象都负责处理请求的一部分,并将请求传递给下一个对象,直到请求得到满足或者处理结束。这种方式可以将系统中的不同操作解耦,从而提高系统的灵活性和可维护性。
在 JavaScript 中,职责链模式的实现通常涉及使用一个处理对象的链表,其中每个对象都有一个指向下一个对象的引用。当请求进入系统时,它首先被传递给链表中的第一个对象。如果这个对象不能处理请求,则将请求传递给链表中的下一个对象,直到找到能够处理请求的对象为止。
下面是一个简单的 JavaScript 职责链模式的示例:
class Handler {
constructor() {
this.nextHandler = null;
}
setNextHandler(handler) {
this.nextHandler = handler;
}
handleRequest(request) {
if (this.nextHandler) {
this.nextHandler.handleRequest(request);
}
}
}
class ConcreteHandler1 extends Handler {
handleRequest(request) {
if (request === 'request1') {
console.log('ConcreteHandler1 handles the request');
} else {
super.handleRequest(request);
}
}
}
class ConcreteHandler2 extends Handler {
handleRequest(request) {
if (request === 'request2') {
console.log('ConcreteHandler2 handles the request');
} else {
super.handleRequest(request);
}
}
}
const handler1 = new ConcreteHandler1();
const handler2 = new ConcreteHandler2();
handler1.setNextHandler(handler2);
handler1.handleRequest('request1'); // Output: "ConcreteHandler1 handles the request"
handler1.handleRequest('request2'); // Output: "ConcreteHandler2 handles the request"
handler1.handleRequest('request3'); // Output: Nothing is printed
在上面的示例中,Handler
类是职责链模式的基类,它包含一个指向下一个处理对象的引用。ConcreteHandler1
和 ConcreteHandler2
类是具体的处理对象,它们根据请求的类型来决定是否能够处理请求。如果不能处理,则将请求传递给下一个处理对象。最后,我们将 handler1
对象的下一个处理对象设置为 handler2
对象,然后依次调用 handleRequest
方法来模拟不同类型的请求。
10. 代理模式
代理模式(Proxy Pattern):前端设计模式中的代理模式是一种结构型模式,它允许在不改变原始对象的情况下,通过引入一个代理对象来控制对原始对象的访问。代理对象充当原始对象的中介,客户端与代理对象交互,代理对象再将请求转发给原始对象。
代理模式在前端开发中经常被用来处理一些复杂或者耗时的操作,例如图片的懒加载、缓存等。代理对象可以在加载图片时显示占位符,当图片加载完成后再替换占位符,从而提高页面加载速度和用户体验。
另外,代理模式还可以用来实现一些权限控制的功能。例如,在用户登录后,代理对象可以检查用户的权限,只有具有相应权限的用户才能够访问某些功能或者页面。
在 JavaScript 中,代理模式通常使用 ES6 中新增的 Proxy 对象来实现。Proxy 对象允许拦截对对象的各种操作,包括读取、赋值、函数调用等。通过使用 Proxy 对象,我们可以在不改变原始对象的情况下,控制对原始对象的访问。
当我们需要为某个类或者对象添加一些额外的行为或者控制访问时,可以使用代理模式。下面是一个简单的示例,使用代理模式实现图片懒加载的功能。
// 原始对象 - 图片
class Image {
constructor(url) {
this.url = url;
}
// 加载图片
load() {
console.log(Image loaded: <span class="hljs-subst">${<span class="hljs-variable language_">this</span>.url}</span>
);
}
}
// 代理对象 - 图片
class ProxyImage {
constructor(url) {
this.url = url;
this.image = null; // 延迟加载
}
// 加载图片
load() {
if (!this.image) {
this.image = new Image(this.url); // 延迟加载图片
console.log(Placeholder loaded for <span class="hljs-subst">${<span class="hljs-variable language_">this</span>.url}</span>
);
}
this.image.load(); // 显示图片
}
}
// 客户端代码
const img1 = new ProxyImage('https://example.com/image1.jpg');
const img2 = new ProxyImage('https://example.com/image2.jpg');
img1.load(); // Placeholder loaded for https://example.com/image1.jpg, Image loaded: https://example.com/image1.jpg
img1.load(); // Image loaded: https://example.com/image1.jpg
img2.load(); // Placeholder loaded for https://example.com/image2.jpg, Image loaded: https://example.com/image2.jpg
在上面的示例中,原始对象是 Image
类,代理对象是 ProxyImage
类。当客户端代码调用 load()
方法时,代理对象会首先加载占位符,并延迟加载图片。如果图片已经被加载过了,代理对象会直接显示图片,否则代理对象会加载图片并显示。通过使用代理模式,我们可以在不影响原始对象的情况下,实现了图片的懒加载功能,提高了页面加载速度和用户体验。
11. 命令模式
命令模式(Command Pattern):它允许你将操作封装成对象。这些对象包括了被调用的方法及其参数。这些命令对象可以被存储、传递和执行。
在前端开发中,命令模式可以被用于实现可撤销和重做的操作。例如,在一个文本编辑器中,可以使用命令模式来实现撤销和重做操作。对于每一个编辑操作,可以创建一个命令对象来表示这个操作,然后将这个命令对象存储在一个历史列表中。当需要撤销操作时,可以从历史列表中取出最近的命令对象并执行它的反向操作。
命令模式还可以被用于实现菜单和工具栏等用户界面元素。例如,可以创建一个菜单项对象来表示一个命令,并将这个对象添加到菜单中。当用户点击这个菜单项时,菜单项对象将执行对应的操作。
下面是一个简单的实现可撤销操作的例子:
// 定义一个命令对象
class Command {
constructor(receiver, args) {
this.receiver = receiver;
this.args = args;
this.executed = false;
}
execute() {
if (!this.executed) {
this.receiver.execute(this.args);
this.executed = true;
}
}
undo() {
if (this.executed) {
this.receiver.undo(this.args);
this.executed = false;
}
}
}
// 定义一个接收者对象
class Receiver {
constructor() {
this.value = 0;
}
execute(args) {
this.value += args;
console.log(执行操作,value = <span class="hljs-subst">${<span class="hljs-variable language_">this</span>.value}</span>
);
}
undo(args) {
this.value -= args;
console.log(撤销操作,value = <span class="hljs-subst">${<span class="hljs-variable language_">this</span>.value}</span>
);
}
}
// 创建一个接收者对象和一些命令对象
const receiver = new Receiver();
const command1 = new Command(receiver, 1);
const command2 = new Command(receiver, 2);
const command3 = new Command(receiver, 3);
// 创建一个历史列表并将命令对象添加到其中
const history = [command1, command2, command3];
// 依次执行命令对象
history.forEach((command) => {
command.execute();
});
// 撤销最后一个操作
history.pop().undo(); // 撤销操作,value = 3
在这个例子中,Command
类表示一个命令对象,它包含了一个接收者对象、命令参数以及一个 executed
属性,用于标记命令是否已经被执行过。Receiver
类表示接收者对象,它实现了具体的操作。在这个例子中,命令对象执行的操作是将 args
参数加到 Receiver
对象的 value
属性上。命令对象的 execute
和 undo
方法分别执行和撤销这个操作,并通过 executed
属性来避免重复执行操作。
在实际的前端开发中,命令模式的应用还有很多,比如实现动画效果的控制器、网络请求的队列等。命令模式可以让代码更加灵活和可扩展,同时也可以更好地实现代码的解耦。
12. 迭代器模式
迭代器模式(Iterator Pattern):提供一种方法顺序访问一个聚合对象中的各个元素,而不需要暴露该对象的内部表示。在JavaScript中,可以使用迭代器模式来操作数组或类数组对象。
在迭代器模式中,集合对象包含一个方法,用于返回一个迭代器,该迭代器可以按顺序访问该集合中的元素。迭代器提供了一种通用的接口,使得可以使用相同的方式遍历不同类型的集合对象。
在前端开发中,迭代器模式经常用于处理集合数据,例如数组、列表等。通过使用迭代器模式,可以轻松地遍历集合对象的元素,而不必担心它们的实现方式。
以下是一个使用迭代器模式的示例:
// 定义一个集合类
class Collection {
constructor() {
this.items = [];
}
add(item) {
this.items.push(item);
}
[Symbol.iterator]() {
let index = 0;
const items = this.items;
return {
next() {
if (index < items.length) {
return { value: items[index++], done: false };
} else {
return { done: true };
}
}
};
}
}
// 创建一个集合对象
const collection = new Collection();
collection.add('item 1');
collection.add('item 2');
collection.add('item 3');
// 使用迭代器遍历集合对象
const iterator = collectionSymbol.iterator;
let result = iterator.next();
while (!result.done) {
console.log(result.value);
result = iterator.next();
}
// item 1
// item 2
// item 3
// {done: true}
在上面的示例中,定义了一个名为 Collection 的集合类,该类包含一个 add 方法,用于向集合中添加元素。该类还实现了一个名为 [Symbol.iterator] 的特殊方法,用于返回一个迭代器对象。迭代器对象包含一个 next 方法,用于返回集合中的下一个元素,直到集合的所有元素都被遍历完毕。
通过使用迭代器模式,我们可以轻松地遍历集合对象的元素,而不必担心它们的实现方式。
13. 组合模式
组合模式(Composite Pattern):它允许将对象组合成树形结构,并且可以像操作单个对象一样操作整个树形结构。
组合模式(Composite Pattern)是一种结构型设计模式,它允许将对象组合成树形结构,并且可以像操作单个对象一样操作整个树形结构。
组合模式的核心思想是将对象组织成树形结构,其中包含组合对象和叶子对象两种类型。组合对象可以包含叶子对象或其他组合对象,从而形成一个树形结构。
组合模式可以应用于以下场景:
- UI组件库:例如在一个复杂的UI组件库中,一个复杂的组件可以由多个子组件组成,而每个子组件又可以由更小的组件组成。这种情况下,可以使用组合模式将每个组件看作一个节点,从而构建一个树形结构。
- 树形结构数据的处理:例如在一个文件管理器中,文件夹和文件可以看作是组合对象和叶子对象。通过组合模式,可以轻松地处理文件夹和文件的层级关系,同时可以对整个文件夹进行操作,比如复制、粘贴和删除等。
实现组合模式通常有两种方式:
- 使用类继承:通过定义一个抽象的 Component 类和两个具体的 Composite 和 Leaf 类来实现。Composite 类继承自 Component 类,并且拥有一个子节点列表。Leaf 类继承自 Component 类,并且没有子节点。这种方式的实现比较传统,但是需要使用类继承,可能会导致类层次结构比较复杂。
- 使用对象组合:通过使用对象字面量和原型继承等技术来实现。这种方式可以不需要类继承,而是使用对象字面量和原型链来模拟组合模式的结构,比较灵活,但是代码可能比较冗长。
下面是一个使用对象字面量和原型继承的组合模式实现示例:
// Component
const Component = {
add: function () {},
remove: function () {},
getChild: function () {},
};
// Composite
function createComposite() {
const composite = Object.create(Component);
composite.children = [];
composite.add = function (child) {
this.children.push(child);
};
composite.remove = function (child) {
const index = this.children.indexOf(child);
if (index !== -1) {
this.children.splice(index, 1);
}
};
composite.getChild = function (index) {
return this.children[index];
};
return composite;
}
// Leaf
function createLeaf() {
const leaf = Object.create(Component);
// Leaf 类实现自己的 add、remove、getChild 方法
return leaf;
}
// 使用示例
const root = createComposite();
const branch1 = createComposite();
const branch2 = createComposite();
const leaf1 = createLeaf();
const leaf2 = createLeaf();
const leaf3 = createLeaf();
root.add(branch1);
root.add(branch2);
branch1.add(leaf1);
branch2.add(leaf2);
branch2.add(leaf3);
console.log(root); // 输出树形结构
上述示例中,通过使用对象字面量和原型继承,模拟了组合模式的结构,从而实现了树形结构的对象。在实际应用中,根据具体的需求和代码架构,可以选择适合自己的实现方式。
14. 原型模式
原型模式(Prototype Pattern):使用原型实例指定创建对象的种类,并通过拷贝这些原型创建新的对象。
在JavaScript中,所有的对象都有一个原型链。原型链是一种机制,它允许我们在对象上定义属性和方法,并且可以从它的原型中继承属性和方法。当我们访问一个对象的属性或方法时,JavaScript会在原对象上查找,如果找不到,它会继续查找原型链上的对象,直到找到该属性或方法或者到达原型链的末端。
原型模式是一种利用原型链的设计模式,它允许我们通过克隆现有对象来创建新对象。JavaScript中的原型模式使用Object.create()
方法来创建一个对象,并且可以通过修改原型链上的属性和方法来修改新对象的行为。
使用原型模式的主要优点是它可以减少对象创建的时间和成本。它避免了在创建对象时需要执行许多计算或调用其他对象的构造函数的开销。相反,它使用现有对象作为基础,并通过克隆来创建新对象,从而提高了性能和效率。
下面是一个使用原型模式创建表单对象的示例代码:
// 定义一个表单对象的原型
var formPrototype = {
fields: [],
addField: function(field) {
this.fields.push(field);
},
getFields: function() {
return this.fields;
},
clone: function() {
// 克隆表单对象并返回新对象
var newForm = Object.create(this);
newForm.fields = Object.create(this.fields);
return newForm;
}
};
// 创建一个表单对象
var form = Object.create(formPrototype);
// 添加表单字段
form.addField('name');
form.addField('email');
form.addField('phone');
// 克隆表单对象并修改其中的字段
var newForm = form.clone();
newForm.addField('address');
// 打印表单对象和新表单对象的字段
console.log(form.getFields()); // ["name", "email", "phone"]
console.log(newForm.getFields()); // ["name", "email", "phone", "address"]
在这个示例中,我们首先定义了一个表单对象的原型,它包含一个空的字段数组、添加字段和获取字段的方法以及一个克隆方法。然后,我们创建了一个表单对象,并向其添加了三个字段。接下来,我们使用原型模式克隆表单对象并添加一个新字段。最后,我们打印了表单对象和新表单对象的字段,以验证克隆方法是否正常工作。
15. 桥接模式
桥接模式(Bridge Pattern):用于将一个大类或一系列紧密相关的类拆分为抽象和实现两个独立的层次结构,从而能够更好地组合和扩展这些类。
在前端开发中,桥接模式通常用于处理 UI 组件的复杂性,将组件的抽象与实现分离,使得它们能够独立地变化。通过桥接模式,我们可以让组件的行为和样式分别独立变化,从而避免在代码中出现过多的重复和复杂度。
具体来说,桥接模式包含两个关键部分:
- 抽象部分(Abstraction):定义了组件的抽象接口和行为,它依赖于一个实现部分的对象。
- 实现部分(Implementation):定义了组件的实现接口和样式,它被抽象部分所依赖。
通过将抽象部分与实现部分解耦,我们可以在不影响原有代码的情况下,方便地扩展和修改组件的行为和样式。同时,桥接模式也可以提高代码的可读性和可维护性,使得代码更加清晰、简洁和易于维护。
以下是一个简单的前端桥接模式示例,假设我们需要实现一个 UI 组件库,其中包含多种样式的按钮。
首先,我们创建一个抽象部分(Button)和两个实现部分(DefaultButton 和 OutlineButton),它们分别定义了按钮的抽象接口和样式,然后,我们可以创建不同类型的按钮,并将其与不同样式的按钮相结合:
// 抽象部分
class Button {
constructor(implementation) {
this.implementation = implementation;
}
click() {
this.implementation.onClick();
}
render() {
return this.implementation.render();
}
}
// 实现部分 - 默认样式
class DefaultButton {
onClick() {
console.log("DefaultButton clicked");
}
render() {
return "<button class='default'>Default Button</button>";
}
}
// 实现部分 - 轮廓样式
class OutlineButton {
onClick() {
console.log("OutlineButton clicked");
}
render() {
return "<button class='outline'>Outline Button</button>";
}
}
// 创建不同类型的按钮
const primaryButton = new Button(new DefaultButton());
const secondaryButton = new Button(new OutlineButton());
// 渲染并绑定按钮事件
document.body.innerHTML = <span class="hljs-subst">${primaryButton.render()}</span> <span class="hljs-subst">${secondaryButton.render()}</span>
;
document.querySelector(".default").addEventListener("click", () => {
primaryButton.click();
});
document.querySelector(".outline").addEventListener("click", () => {
secondaryButton.click();
});
最后,当用户点击按钮时,会触发相应的行为,同时也会根据按钮的样式渲染出不同的外观效果。
这是一个非常简单的示例,但是它展示了如何使用桥接模式来处理 UI 组件的复杂性,通过将抽象和实现分离,可以方便地扩展和修改组件的行为和样式,从而提高代码的可维护性和可读性。
16. 状态模式
状态模式(State Pattern):将对象的行为和状态分离,使得对象可以根据不同的状态来改变自己的行为。在前端开发中,可以使用状态模式来管理页面的状态和响应用户的交互。
在状态模式中,对象的行为取决于其内部状态,当状态发生变化时,对象的行为也会相应地发生改变。这种模式通过将状态抽象为独立的类来实现,每个状态类都有其自己的行为和相应的方法。
在前端开发中,状态模式通常用于处理复杂的用户交互逻辑,例如根据用户的操作更改页面上的状态。以下是状态模式的一些常见应用场景:
- 表单验证:在用户提交表单之前,可以使用状态模式来验证表单中的输入是否符合规定。
- 游戏开发:在游戏中,对象的状态可能会随着游戏的进程而发生变化。使用状态模式可以帮助开发者轻松管理和处理这些状态变化。
- 状态切换:在前端应用中,一些操作会导致页面状态的改变,例如打开或关闭侧边栏,展开或折叠列表等。使用状态模式可以帮助开发者管理这些状态变化。
总之,状态模式是一种灵活而有用的设计模式,可以使代码更加清晰和可维护,它可以帮助开发者轻松管理和处理复杂的用户交互逻辑。
以下是一个简单的状态模式示例,假设我们正在开发一个购物车页面,用户可以向购物车中添加或删除商品。购物车的状态可以是“空的”或“非空的”,当购物车为空时,用户需要被提示添加商品,当购物车非空时,用户可以查看商品列表和删除商品。
首先,我们定义一个购物车状态的抽象类:
class CartState {
addToCart() {}
removeFromCart() {}
showMessage() {}
}
接下来,我们定义购物车的两种状态:空状态和非空状态。空状态下用户需要被提示添加商品,非空状态下用户可以查看商品列表和删除商品。
class EmptyCartState extends CartState {
addToCart() {
console.log('Product added to cart');
// 将购物车状态设置为非空状态
this.cart.setState(new NonEmptyCartState(this.cart));
}
showMessage() {
console.log('Your cart is empty, please add some products.');
}
}
class NonEmptyCartState extends CartState {
removeFromCart() {
console.log('Product removed from cart');
// 如果商品列表为空,则将购物车状态设置为空状态
if (this.cart.products.length === 0) {
this.cart.setState(new EmptyCartState(this.cart));
}
}
showMessage() {
console.log(Your cart contains <span class="hljs-subst">${<span class="hljs-variable language_">this</span>.cart.products.length}</span> products:
);
console.log(this.cart.products.join(', '));
}
}
最后,我们定义购物车类,并在购物车类中使用状态模式:
class ShoppingCart {
constructor() {
// 初始状态为购物车为空
this.state = new EmptyCartState(this);
this.products = [];
}
setState(state) {
this.state = state;
}
addToCart(product) {
this.products.push(product);
this.state.addToCart();
}
removeFromCart(product) {
const index = this.products.indexOf(product);
if (index !== -1) {
this.products.splice(index, 1);
this.state.removeFromCart();
}
}
showMessage() {
this.state.showMessage();
}
}
在上面的代码中,我们创建了一个ShoppingCart
类,它包含了两个主要方法addToCart
和removeFromCart
,这些方法将商品添加到购物车中或从购物车中删除。当这些方法被调用时,购物车的状态将会根据商品列表的状态自动更新。另外,我们还提供了一个showMessage
方法,用于显示购物车当前的状态信息。
现在,我们可以使用这个购物车类来管理我们的购物车状态了:
const cart = new ShoppingCart();
cart.showMessage(); // Your cart is empty, please add some products.
cart.addToCart('apple');
cart.addToCart('banana');
cart.showMessage(); // Your cart contains 2 products: apple, banana.
cart.removeFromCart('apple');
cart.showMessage(); // Your cart contains 1 products: banana.
cart.removeFromCart('banana');
cart.showMessage(); // Your cart is empty, please add some products.
通过使用状态模式,我们可以轻松管理购物车的状态,并且代码更加清晰和可维护。
17. 模板方法模式
模板方法模式(Template Method Pattern):定义一个行为的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以不改变一个行为的结构即可重定义该行为的某些特定步骤。
这些步骤被称为“具体操作”(Concrete Operations),而整个行为的结构和顺序则被称为“模板方法”(Template Method)。
模板方法模式的核心思想是封装行为中的不变部分,同时允许可变部分通过子类来进行扩展。这样做的好处是可以避免重复代码,提高代码的复用性和可维护性。
在前端开发中,模板方法模式通常用于处理页面的渲染和事件处理。例如,我们可以定义一个基础的页面渲染算法,并在其中定义一些抽象方法,如初始化数据、绑定事件、渲染模板等,然后在子类中实现这些具体操作。这样可以使得我们在开发页面时,只需要关注具体的业务逻辑,而不用过多关注页面的渲染细节。
下面是一个简单的模板方法模式的示例代码:
class Algorithm {
templateMethod() {
this.stepOne();
this.stepTwo();
this.stepThree();
}
stepOne() {
throw new Error("Abstract method 'stepOne' must be implemented in subclass.");
}
stepTwo() {
throw new Error("Abstract method 'stepTwo' must be implemented in subclass.");
}
stepThree() {
throw new Error("Abstract method 'stepThree' must be implemented in subclass.");
}
}
class ConcreteAlgorithm extends Algorithm {
stepOne() {
console.log('ConcreteAlgorithm: step one.');
}
stepTwo() {
console.log('ConcreteAlgorithm: step two.');
}
stepThree() {
console.log('ConcreteAlgorithm: step three.');
}
}
const algorithm = new ConcreteAlgorithm();
algorithm.templateMethod();
在这个示例中,我们定义了一个 Algorithm 类,其中包含了一个模板方法 templateMethod()
和三个基本方法 stepOne()
、stepTwo()
和 stepThree()
。这些基本方法都是抽象方法,需要在子类中进行实现。
我们还定义了一个 ConcreteAlgorithm 类,它继承自 Algorithm 类,并实现了父类中的三个基本方法。然后,我们创建了一个 ConcreteAlgorithm 的实例,并调用了其 templateMethod()
方法,该方法会按照父类定义的顺序执行三个基本方法。
总的来说,模板方法模式是一种非常实用的设计模式,在 JavaScript 中也同样适用。它可以帮助我们将代码的结构和行为进行分离,从而提高代码的可读性和可维护性。
18. 过滤器模式
过滤器模式(Filter Pattern):定义一个过滤器函数,该函数可以接受一个数据集合和一个过滤条件,返回符合条件的数据集合。
在过滤器模式中,我们有一个包含多个对象的列表,需要根据一些条件来筛选出符合条件的对象。通常情况下,可以使用多个过滤器来实现这个功能。每个过滤器都是一个独立的类,它实现了一个过滤条件,我们可以将这些过滤器组合在一起,来实现复杂的过滤操作。
在实际开发中,可以使用过滤器模式来实现诸如搜索、过滤、排序等功能。例如,在一个商品列表页面中,我们可以使用过滤器模式来根据价格、品牌、类型等条件来筛选出用户感兴趣的商品。
以下是一个简单的 JavaScript 示例代码,演示了如何使用过滤器模式来筛选数组中的元素:
class Filter {
constructor(criteria) {
this.criteria = criteria;
}
meetCriteria(elements) {
return elements.filter(element => this.criteria(element));
}
}
class PriceFilter extends Filter {
constructor(price) {
super(element => element.price <= price);
}
}
class BrandFilter extends Filter {
constructor(brand) {
super(element => element.brand === brand);
}
}
const products = [
{ name: 'Product A', price: 10, brand: 'Brand A' },
{ name: 'Product B', price: 20, brand: 'Brand B' },
{ name: 'Product C', price: 30, brand: 'Brand C' },
];
const priceFilter = new PriceFilter(20);
const brandFilter = new BrandFilter('Brand A');
const filteredProducts = priceFilter.meetCriteria(products);
const finalProducts = brandFilter.meetCriteria(filteredProducts);
console.log(finalProducts);
// Output: [{ name: 'Product A', price: 10, brand: 'Brand A' }]
在上面的示例代码中,我们定义了一个 Filter
类作为过滤器模式的基类,然后我们定义了两个子类 PriceFilter
和 BrandFilter
,它们分别根据价格和品牌来过滤商品。我们还定义了一个商品数组 products
,然后使用这两个过滤器来筛选出价格低于等于 20 并且品牌为 'Brand A' 的商品,最后打印出符合条件的商品列表。
19. 备忘录模式
备忘录模式(Memento Pattern):是一种行为型设计模式,在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。在JavaScript中,可以使用闭包来实现备忘录模式。
在前端设计中,备忘录模式通常用于处理用户界面的状态。当用户与应用程序交互时,应用程序会根据用户的输入更改其状态。它可以使您保存和还原应用程序状态的快照,以便用户可以随时返回以前的状态。
以下是备忘录模式的几个关键组件:
- Originator(发起人):负责创建要保存状态的对象,并在需要时将其状态存储到 Memento 中。
- Memento(备忘录):存储发起人对象的状态。
- Caretaker(管理者):负责存储和管理 Memento 对象。Caretaker 对象不会直接访问 Memento 对象,而是通过发起人对象来获取它。
在前端中,备忘录模式的一个实际应用是浏览器历史记录。当用户在浏览器中导航时,浏览器将当前页面的状态存储到历史记录中。用户可以随时返回以前的状态,并恢复页面的先前状态。
在 JavaScript 中,备忘录模式的实现和其他编程语言非常相似。下面是一个简单的 JavaScript 实现示例:
// Originator
class Editor {
constructor() {
this.content = '';
}
type(words) {
this.content += words;
}
save() {
return new EditorMemento(this.content);
}
restore(memento) {
this.content = memento.getContent();
}
}
// Memento
class EditorMemento {
constructor(content) {
this.content = content;
}
getContent() {
return this.content;
}
}
// Caretaker
class History {
constructor() {
this.states = [];
}
push(state) {
this.states.push(state);
}
pop() {
return this.states.pop();
}
}
// Usage
const editor = new Editor();
const history = new History();
// 编辑器输入内容
editor.type('Hello, ');
editor.type('World!');
// 将当前状态保存到备忘录中,并将备忘录添加到历史记录中
history.push(editor.save());
// 继续编辑器输入内容
editor.type(' How are you today?');
// 输出当前编辑器内容
console.log(editor.content); // 'Hello, World! How are you today?'
// 从历史记录中恢复上一个状态
editor.restore(history.pop());
// 输出恢复的编辑器内容
console.log(editor.content); // 'Hello, World!'
在这个例子中,Editor
类是发起人,它具有保存和恢复状态的方法。EditorMemento
类是备忘录,它存储发起人对象的状态。History
类是管理者,它存储和管理备忘录对象。
在使用备忘录模式时,我们首先创建一个编辑器对象 editor
,并将其状态保存到历史记录中。然后我们继续输入一些内容,并再次将状态保存到历史记录中。接着,我们从历史记录中恢复先前的状态,并输出恢复的编辑器内容。
需要注意的是,在 JavaScript 中,我们可以直接访问对象的属性,而不需要使用 getter 和 setter 方法,因此在上面的示例中,我们可以直接使用 editor.content
来访问编辑器的内容。
20. 外观模式
外观模式(Facade Pattern):它提供了一个简单的接口,用于访问复杂的系统或子系统。通过外观模式,客户端可以通过一个简单的接口来访问复杂的系统,而无需了解系统内部的具体实现细节。
在前端开发中,外观模式常常被用于封装一些常用的操作,以简化代码复杂度和提高代码可维护性。比如,一个用于处理数据的模块可能包含很多复杂的代码逻辑和 API 调用,但是我们可以使用外观模式将这些复杂的操作封装到一个简单的接口中,让其他部分的代码可以通过这个接口来操作数据,而无需关心具体的实现细节。
外观模式的优点在于它可以将系统的复杂性隐藏起来,从而降低代码的复杂度和耦合度。同时,外观模式也可以提高代码的可读性和可维护性,因为它可以将一些常用的操作封装到一个统一的接口中,让代码更加清晰易懂。
下面是一个外观模式的示例代码:
// 复杂的系统或子系统
const moduleA = {
method1: () => {
console.log('method1 from moduleA');
},
method2: () => {
console.log('method2 from moduleA');
},
method3: () => {
console.log('method3 from moduleA');
}
};
const moduleB = {
method4: () => {
console.log('method4 from moduleB');
},
method5: () => {
console.log('method5 from moduleB');
},
method6: () => {
console.log('method6 from moduleB');
}
};
// 外观对象,封装了底层的操作,提供了一个简单的接口
const facade = {
method1: () => {
moduleA.method1();
},
method2: () => {
moduleA.method2();
},
method3: () => {
moduleA.method3();
},
method4: () => {
moduleB.method4();
},
method5: () => {
moduleB.method5();
},
method6: () => {
moduleB.method6();
}
};
// 客户端调用外观对象的方法
facade.method1(); // 输出:method1 from moduleA
facade.method2(); // 输出:method2 from moduleA
facade.method4(); // 输出:method4 from moduleB
facade.method6(); // 输出:method6 from moduleB
在这个例子中,我们定义了两个模块 moduleA
和 moduleB
,它们都包含了一些方法。然后,我们定义了一个名为 facade
的外观对象,它包含了这两个模块的所有方法,并提供了一个简单的接口,让客户端可以直接调用这些方法。最后,我们在客户端调用外观对象的方法,实际上是间接调用了底层模块的方法。
需要注意的是,外观模式并不是一种万能的设计模式,它并不能解决所有的问题。在某些情况下,使用外观模式可能会增加代码的复杂度和冗余度,因此需要谨慎使用。
21. 访问者模式
访问者模式(Visitor Pattern):是一种行为型设计模式,用于将操作与其所操作的对象分离开来。该模式的核心思想是将操作封装在一个访问者对象中,而不是分散在各个对象中。通过将操作与对象分离开来,访问者模式可以在不修改对象结构的情况下,添加新的操作。
在前端开发中,访问者模式通常用于处理DOM树上的操作。由于DOM树结构通常很深,而且节点类型不同,因此对DOM树进行一系列的操作,常常需要写很多代码。而访问者模式可以将这些操作抽象出来,封装到访问者对象中,从而简化了代码量。
在访问者模式中,有两种角色:访问者(Visitor)和被访问者(Element)。被访问者是一组对象,它们具有不同的接口,用于接受访问者的访问。访问者则是一组对象的操作,用于处理被访问者。访问者通常会遍历整个被访问者的结构,并对每个节点进行操作。
下面是一个简单的访问者模式示例:
// 定义被访问者
class Element {
accept(visitor) {
visitor.visit(this);
}
}
// 定义访问者
class Visitor {
visit(element) {
// 处理元素
}
}
// 定义具体的元素
class ConcreteElement extends Element {
// 具体的实现
}
// 定义具体的访问者
class ConcreteVisitor extends Visitor {
visit(element) {
// 处理具体的元素
}
}
// 使用访问者模式
const element = new ConcreteElement();
const visitor = new ConcreteVisitor();
element.accept(visitor);
在这个例子中,我们首先定义了一个被访问者 Element,它有一个 accept 方法,用于接受访问者的访问。然后定义了一个访问者 Visitor,它有一个 visit 方法,用于处理被访问者 Element。接下来,我们定义了具体的元素 ConcreteElement 和具体的访问者 ConcreteVisitor,并实现了它们的具体逻辑。最后,在主程序中,我们创建了一个 ConcreteElement 实例和一个 ConcreteVisitor 实例,将 ConcreteElement 实例作为参数传递给 ConcreteVisitor 的 visit 方法。
总之,访问者模式可以将操作和对象分离开来,从而实现代码的解耦和灵活性。在前端开发中,它常常被用来处理复杂的DOM树结构。
22. 委托模式
委托模式(Delegation pattern):将一个对象的某个方法委托给另一个对象来执行,它可以帮助我们将对象之间的关系更加灵活地组织起来,从而提高代码的可维护性和复用性。
在委托模式中,一个对象(称为委托对象)将一些特定的任务委托给另一个对象(称为代理对象)来执行。代理对象通常具有和委托对象相同的接口,因此可以完全替代委托对象,而且可以根据需要动态地改变委托对象,从而实现了对象之间的松耦合。
在实际应用中,委托模式常常和其他模式一起使用,比如组合模式、单例模式、观察者模式等。例如,我们可以使用委托模式来实现组合模式中的叶节点和枝节点的统一接口,从而实现对整个树形结构的递归遍历。
下面是一个使用委托模式的简单示例:
// 委托对象
const delegate = {
greet(name) {
return `Hello, ${name}!`;
}
};
// 代理对象
const proxy = {
delegate: delegate,
greet(name) {
return this.delegate.greet(name);
}
};
// 使用代理对象
console.log(proxy.greet("world")); // 输出:Hello, world!
在上面的例子中,我们定义了一个委托对象 delegate
,它有一个 greet
方法用于向指定的名称打招呼。然后,我们又定义了一个代理对象 proxy
,它将委托对象保存在自己的属性 delegate
中,并且实现了和委托对象相同的 greet
方法,但是它的实现其实是通过调用委托对象的 greet
方法来实现的。
最后,我们通过调用代理对象的 greet
方法来向世界打招呼,实际上代理对象内部会委托给委托对象来执行这个任务。
23. 计算属性模式
计算属性模式(Computed Property Pattern):在JavaScript中,可以使用Object.defineProperty()方法来实现计算属性模式,通过get和set方法来计算属性值。
计算属性模式用于将对象的某些属性值与其他属性值相关联。该模式常用于Vue.js等框架中。
计算属性模式的基本思想是,定义一个函数作为对象的属性,并在该函数中计算出相关联的属性值。当访问该属性时,实际上是调用该函数并返回计算结果。
例如,假设有一个对象包含长度和宽度属性,需要计算出它们的面积。可以定义一个计算属性area,该属性为一个函数,返回长度和宽度的乘积:
const rectangle = {
length: 10,
width: 5,
get area() {
return this.length * this.width;
}
};
console.log(rectangle.area); // 50
在上面的代码中,当访问rectangle
对象的area
属性时,实际上是调用了该对象的area
函数,并返回计算结果50。
计算属性模式的优点是,可以使对象属性之间的关系更加清晰和易于维护。例如,在上面的例子中,如果需要修改面积计算公式,只需要修改area
函数即可,而不需要修改访问该属性的代码。
需要注意的是,在计算属性模式中,计算属性的值并不是固定的,而是根据其他属性值的变化而变化的。因此,在对象的属性值发生变化时,需要确保计算属性的值也能及时更新。在Vue.js等框架中,可以通过watcher
等机制来实现自动更新。
24. 路由模式
路由模式(Router Pattern):将页面的不同状态映射到不同的URL路径上,使得用户可以直接通过URL来访问页面的不同状态。
路由模式通常用于实现单页面应用(SPA)的页面导航和状态管理。具体来说,路由模式通过解析URL路径来确定应该显示哪个页面,并使用历史记录API来管理页面状态。
一般来说,路由模式包含以下几个关键部分:
- 路由表:定义URL路径与页面组件的映射关系。
- 路由器:负责监听URL路径的变化,根据路由表匹配对应的页面组件,并将其渲染到页面上。
- 历史记录管理器:负责管理浏览器的历史记录,以便用户可以使用浏览器的前进和后退按钮导航应用程序的不同状态。
常见的前端路由框架有Vue Router、React Router和Angular Router等。这些框架都提供了一系列API和组件来帮助开发者快速构建SPA应用程序,并实现灵活、可扩展的路由功能。
下面是一个基于Vue Router的简单路由示例:
import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter)
const routes = [
{
path: '/',
component: Home
},
{
path: '/about',
component: About
},
{
path: '/contact',
component: Contact
}
]
const router = new VueRouter({
routes
})
new Vue({
router,
render: h => h(App)
}).$mount('#app')
首先,我们需要在Vue项目中安装并导入Vue Router
模块,然后,我们可以定义一个路由表,指定每个URL路径对应的页面组件,在这个示例中,我们定义了三个路由路径,分别对应Home
、About
和Contact
三个页面组件。
接着,我们可以创建一个VueRouter
实例,并将路由表作为参数传递进去,最后,我们需要将VueRouter
实例挂载到Vue
根实例中。
现在,我们就可以在应用程序中使用路由了。例如,我们可以在App.vue
组件中添加一个路由出口,并使用<router-link>
组件来定义导航链接:
<template>
<div id="app">
<router-link to="/">Home</router-link>
<router-link to="/about">About</router-link>
<router-link to="/contact">Contact</router-link>
<router-view></router-view>
</div>
</template>
在这个示例中,<router-view>
组件将渲染当前路由路径对应的页面组件。当用户点击导航链接时,Vue Router
将自动更新URL路径,并根据路由表匹配对应的页面组件进行渲染。
25. 解释器模式
解释器模式(Interpreter Pattern):是一种行为型设计模式,它可以用来解决一些特定问题,例如编译器、计算器等等。这种模式定义了一个语言的语法,并用一个解释器来解释语言中的表达式。
解释器模式可以用来处理例如数据格式化、表单验证等业务场景。在这些场景中,我们需要定义一些语法规则,然后使用解释器来解释这些规则。
解释器模式的基本结构包括四个角色:抽象表达式、终结符表达式、非终结符表达式和上下文。
- 抽象表达式定义了一个抽象的接口,用于解释表达式。
- 终结符表达式是最基本的表达式,它代表了语言中的一个单一的符号,例如一个变量或者一个数字。
- 非终结符表达式则是由多个终结符表达式组成的表达式,它代表了复杂的语言语法规则。
- 上下文用于存储解释器解释时的中间结果。
在使用解释器模式时,我们需要先定义好语言的语法规则,然后再根据这些规则创建相应的表达式对象,并将其组合成一个完整的表达式树。最后,我们可以使用解释器来解释这棵表达式树,并得到相应的结果。
以下是一个简单的示例,演示了如何使用解释器模式来处理一个简单的算术表达式。在这个示例中,我们定义了一个语法规则,用于表示加法和减法运算,并使用解释器模式来解释这个表达式。
// 定义抽象表达式
class Expression {
interpret() {}
}
// 定义终结符表达式
class NumberExpression extends Expression {
constructor(number) {
super();
this.number = number;
}
interpret() {
return this.number;
}
}
// 定义非终结符表达式
class AddExpression extends Expression {
constructor(left, right) {
super();
this.left = left;
this.right = right;
}
interpret() {
return this.left.interpret() + this.right.interpret();
}
}
class SubtractExpression extends Expression {
constructor(left, right) {
super();
this.left = left;
this.right = right;
}
interpret() {
return this.left.interpret() - this.right.interpret();
}
}
// 定义上下文
class Context {
constructor() {
this.expression = null;
}
setExpression(expression) {
this.expression = expression;
}
evaluate() {
return this.expression.interpret();
}
}
// 使用示例
const context = new Context();
const expression = new SubtractExpression(
new AddExpression(new NumberExpression(10), new NumberExpression(5)),
new NumberExpression(2)
);
context.setExpression(expression);
console.log(context.evaluate()); // 输出 13
在这个示例中,我们定义了四个表达式类:Expression
、NumberExpression
、AddExpression
和 SubtractExpression
,并分别实现了它们的 interpret()
方法。同时,我们还定义了一个上下文类 Context
,用于存储解释器解释时的中间结果。
在示例的最后,我们使用 SubtractExpression
、AddExpression
和 NumberExpression
等表达式对象来创建一个表达式树,并将其存储在上下文中。最后,我们使用 Context
对象的 evaluate()
方法来求出表达式的值,并输出结果。
26. 享元模式
享元模式(Flyweight Pattern):是一种用于优化对象创建和管理的设计模式。它旨在减少内存消耗和提高性能,通过共享具有相同状态的对象来实现这一目标。
具体来说,享元模式涉及两个主要的对象:享元工厂和具有共享状态的享元对象。享元工厂负责创建和管理共享对象,以确保每个对象只被创建一次。享元对象则包含需要共享的状态信息,并提供接口以访问该状态。
通过使用享元模式,可以显著减少内存消耗和提高性能,尤其是在处理大量相似对象时。常见的使用享元模式的场景包括:DOM元素的复用、缓存数据、减少ajax请求等。
需要注意的是,享元模式虽然可以优化内存和性能,但是也可能会牺牲一定的可读性和维护性。因此,应该在合适的场景下使用该模式。
以下是一个使用享元模式的简单示例,其中我们创建了一个享元工厂和一个具有共享状态的享元对象:
// 定义享元工厂
const FlyweightFactory = function () {
const flyweights = {};
const get = function (key) {
if (flyweights[key]) {
return flyweights[key];
}
const flyweight = {
// 共享的状态信息
key: key,
// 具体的操作方法
operation: function () {
console.log('Executing operation for key: ' + this.key);
}
};
flyweights[key] = flyweight;
return flyweight;
};
return {
get: get
};
};
// 使用享元工厂创建享元对象
const factory = new FlyweightFactory();
const flyweight1 = factory.get('key1');
const flyweight2 = factory.get('key2');
// 调用共享的操作方法
flyweight1.operation(); // 输出: "Executing operation for key: key1"
flyweight2.operation(); // 输出: "Executing operation for key: key2"
在上面的示例中,我们定义了一个名为 FlyweightFactory
的享元工厂,并实现了 get
方法来获取共享状态的享元对象。当请求一个新的享元对象时,我们首先检查它是否已经存在于工厂的内部缓存中,如果存在则返回它,否则创建一个新的对象并将其添加到缓存中。
我们然后使用 factory
实例来创建两个享元对象 flyweight1
和 flyweight2
,它们分别具有键值为 'key1'
和 'key2'
的共享状态信息。最后,我们调用每个对象的 operation
方法来执行共享的操作。
值得注意的是,在上面的示例中,我们创建了两个不同的享元对象,因为它们具有不同的键值。如果我们尝试再次获取具有相同键值的对象,将会返回已存在的对象,而不是创建一个新的。这就是享元模式的核心思想——通过共享具有相同状态的对象来减少内存消耗和提高性能。
27. 依赖注入模式
依赖注入模式(Dependency Injection Pattern):允许我们通过将对象的依赖关系从代码中分离出来,从而使代码更加模块化和可重用。
在传统的编程模式中,一个对象可能会直接创建或者获取它需要的其他对象,这样会造成对象之间的紧耦合关系,难以维护和扩展。而使用依赖注入模式,则可以将对象的依赖关系从对象内部移到外部,从而实现松耦合的设计,便于维护和扩展。
依赖注入模式可以通过构造函数、属性、方法等方式来实现。在前端开发中,通常使用框架(如Angular、Vue、React等)来实现依赖注入,这些框架提供了依赖注入容器,可以自动管理对象之间的依赖关系。
以下是一个使用依赖注入模式的示例代码:
class UserService {
constructor(apiService) {
this.apiService = apiService;
}
getUser(id) {
return this.apiService.get(/users/<span class="hljs-subst">${id}</span>
);
}
}
class ApiService {
constructor(httpService) {
this.httpService = httpService;
}
get(url) {
return this.httpService.get(url);
}
}
class HttpService {
get(url) {
// 发送HTTP请求
}
}
const httpService = new HttpService();
const apiService = new ApiService(httpService);
const userService = new UserService(apiService);
userService.getUser(1);
在上面的代码中,UserService
、ApiService
、HttpService
三个类之间都存在依赖关系。使用依赖注入模式,可以将这些依赖关系从内部移到外部,从而实现对象之间的解耦。在实例化UserService
对象时,将依赖的ApiService
对象作为参数传入构造函数;在实例化ApiService
对象时,将依赖的HttpService
对象作为参数传入构造函数。这样就实现了依赖注入。
每个设计模式都有其适用的场景和优缺点,需要根据具体情况来选择使用。其实,还有很多其他设计模式,MVC模式(Model-View-Controller)
、MVVM模式(Model-View-ViewModel)
、组件模式(Component Pattern)
等等,这里就不多介绍了,上文讲到的面试肯定够用了,但要真正融合进自己的项目中,还要多思考多理解多实践,认识和应用设计模式可以帮助我们编写更好的代码,提高代码的可读性、可维护性和可扩展性。