一、使用过哪些设计模式?
1、单例模式
一个类只要一个实例,并提供全局访问点来获取实例
2、工厂模式
根据工厂方式创建对象,而不是通过new操作符,这样隐藏了具体实现,并根据需要创建所需类型的对象
3、观察者模式
定义了一种一对多的依赖关系发,当一个对象的状态发生变化时,它的所有依赖着(观察者)都会受到消息并更新
4、装饰器模式
动态的将责任附加到对象上,通过将对象包装在装饰器对象中,可以在运行时为对象添加新的行为
5、适配器模式
将一个类的接口转换为客户端所期望的一个接口,使得原本由于接口不匹配而无法在一起工作的类可以协同工作
二、设计模式分类
- 创建型模式:工厂模式、单例模式、原型模式
- 结构型模式:适配器模式、装饰器模式、代理模式、桥接模式
- 行为式模式:观察者模式、策略模式、责任链模式、模板方法模式等
三、观察者模式和发布订阅模式
观察者模式:一个对象(观察者)订阅另一个对象(主题),当主题被激活的时候,触发观察者里面的事件
发布订阅模式:订阅者将想要订阅的事件注册到调度中心,当发布者发布事件到调度中心(事件被触发),再由调度中心统一调度订阅中心注册到调度中心的处理代码
区别:
角色数量不同:
- 观察者模式:只有两个:观察者和被观察者
- 发布订阅:发布者、订阅者、调度中心
使用场景不同:
- 观察者适用于单个应用内部使用
- 发布订阅模式适用于跨应用场景
手写发布订阅模式
class EventEmitter {
// 初始化一个空对象,用于存储事件名称及其对应的回调函数数组
constructor() {
this.events = {};
}
// 注册事件
on(eventName, callback) {
if (typeof callback !== "function") {
throw new Error("callback must be a function");
}
// 如果事件不存在,创建一个新数组,存在则直接使用现有数组并进行添加
this.events[eventName] = this.events[eventName] || [];
this.events[eventName].push(callback);
}
// 发布事件
// args为调用emit时传入的所有参数
emit(eventName, ...args) {
// 检查事件是否存在,不存在直接返回
if (this.events[eventName]) {
// 避免迭代过程中数组修改
[...this.events[eventName]].forEach(callback => {
callback.apply(this, args);
})
}
}
// 取消订阅
off(eventName, callback) {
// 事件不存在,直接返回
if (!this.events[eventName]) return;
this.events[eventName] = this.events[eventName].filter(
// 过滤回调数组
cb => cb !== callback
);
}
// 清除事件
clear(eventName) {
if (this.events[eventName]) {
delete this.events[eventName];
}
}
clearAll() {
this.events = {};
}
}
上述发布改为异步
class EventEmitter {
constructor() {
this.events = {};
}
// 注册事件
on(eventName, callback) {
if (typeof callback !== "function") {
throw new Error("callback must be a function");
}
this.events[eventName] = this.events[eventName] || [];
this.events[eventName].push(callback);
}
// 发布事件(支持同步/异步)
emit(eventName, isAsync = false, ...args) {
if (!this.events[eventName]) return;
for (const callback of [...this.events[eventName]]) {
if (isAsync) {
Promise.resolve().then(() => {
try {
callback.apply(this, args);
} catch (error) {
console.error(`Async error in event "${eventName}":`, error);
}
});
} else {
try {
callback.apply(this, args);
} catch (error) {
console.error(`Error in event "${eventName}":`, error);
}
}
}
}
// 只执行一次
once(eventName, callback) {
const wrapper = (...args) => {
callback.apply(this, args);
this.off(eventName, wrapper);
};
this.on(eventName, wrapper);
}
// 取消订阅
off(eventName, callback) {
if (!this.events[eventName]) return;
this.events[eventName] = this.events[eventName].filter(cb => cb !== callback);
}
// 清除某个事件
clear(eventName) {
delete this.events[eventName];
}
// 清除所有事件
clearAll() {
this.events = {};
}
}
module.exports = EventEmitter;
四、什么是MVVM?和NVC、MVP有什么区别
这三者都是常见的软件架构设计模式,主要通过分离关注点的方式来组织代码的结构,优化开发效率,
在之前的项目开发中,使用单页应用时,往往一个路由页面对应一个脚本文件,所有的页面逻辑都放在一个脚本文件中。页面的渲染、数据的获取、对用户事件的响应所有应用逻辑就混合一起,因此在项目复杂时候,整个文件就变得冗长,混乱,不利于项目的开发和维护
MVC通过分离model、view和Controller的方式组织代码结构,其中view负责页面的显示逻辑,model负责存储页面的业务数据以及对数据的操作,并且View和Model使用了观察者模式,当Model层发生变化时就会通知有关view层更新应用。Controller主要负责用户与应用的响应操作,当产生交互时,controller中的事件触发器就开始工作,通过调用model层实现对其修改,然后再去通知View层。
MVP和MVC的不同在于Presenter和Controller。在MVC中使用观察者模式,来实现当Model层发生变化时通知view层进行更新。这样View和model层耦合在一起了,就会导致代码混乱。MVP的模式则是通过Presenter来实现对View和Model层的解耦。在MVC在,Controller只知道Model的接口,因此没有白发控制View的更新,在MVP中,View的接口暴漏给了Presenter,因此可以在里面将Model的变化和View的变化绑定一起,实现同步更新。
MVVM中的VM是指ViewModel。与MVP思想一样,只是通过双向绑定的方式将View和Model的同步更新自动化了。其实就是将Presenter的工作给自动化了。
React 并不是严格意义上的 MVVM 框架,但它实现了类似 MVVM 的核心思想:组件本身相当于 ViewModel,负责管理状态和处理逻辑;JSX 是 View 的声明式描述,随着组件的 state
或 props
变化自动更新;而数据模型可以是组件内部状态,也可以通过 Redux 等库统一管理,相当于 Model。React 通过这种单向数据流的机制,实现了 View 与数据之间的绑定和同步,具备响应式 UI 和良好的组件解耦能力,整体架构非常贴近 MVVM 模式。区别于VUE,通过数据劫持和发布订阅模式实现这个功能。
五、单例模式
- 单例就是保证一个类只有一个实例,并且提供一个访问该全局访问点
1、哪里使用了单例模式
单例模式保证只有一个全局计数器对象存在,避免多个实例导致状态不一致
网站的计数器:
class Counter { constructor() { if (Counter.instance) return Counter.instance; this.count = 0; Counter.instance = this; // 唯一实例 } increment() { this.count += 1; return this.count; } getCount() { return this.count; } } const counter = new Counter(); // 不调用 Object.freeze(),保持可变 module.exports = counter;
尝试使用
Object.freeze()
来防止单例被篡改,但这也导致属性如count
变成只读。为保持数据状态可变性,我选择不冻结整个对象,而是通过模块导出控制实例唯一性或者Object.freeze(Counter.prototype);
应用程序的日志应用
多线程的线程池
windows的任务管理器/回收站
2、优缺点
优点:
- 因为只有一个实例,所有对单例类的所有实例化都是一个实例,防止其它对象对自己的的实例化,确保所有对象访问一个实例
- 在系统内存中只有一个实例对象,节约系统资源,尤其在需要频繁创建和销毁的对象时
- 允许可变数目的实例
- 避免对共享资源的多重占用
缺点:
- 不适应于变化的对象
- 单例类的职责过重,一定程度上违背了“单一职责”
- 单例模式没有抽象层,就导致了扩展的困难
- 其他负面;如数据库连接池对象设计为单例类,可能导致共享连接池对象的程序过多而出现的连接池溢出,且实例对象长时间不被利用,会被任务垃圾进行回收
六、工厂模式
创建对象的最佳方式,创建的对象时不会对客户端暴漏创建逻辑,并且通过使用一个共同接口来指向新创建对象,实现创建者和调用者分离,工厂方式有简单工厂,工厂方法,抽象工厂模式
好处:
- 用工厂方法代替new操作
- 降低程序的耦合性,为后期维护提高便利
七、设计模式的原则
- 单一职责:一个方法负责一件事
- 接口隔离原则:使用多个隔离的接口,降低类间的耦合度
- 依赖倒转原则:面向接口编程
- 迪米特法则:一个对象应当对其它对象有尽可能少的了解,类间解耦
- 开放封闭原则:扩展软件实体解决需求变化
- 里氏代换原则:使用的基类可以在任何地方使用继承的子类,完美替换基类