【前端面试总结】常用的设计模式及使用场景

175 阅读6分钟

1.策略模式

定义一系列策略或者算法,并把他们封装起来,使用的时候根据不同的需要选择不同的策略进行组合。主要解决if...else所带来的复杂性和难以维护性。

使用场景: 表单校验。可以将校验规则封装为一个个的策略,不同的校验取不同的策略即可。

2.发布-订阅模式(Publish/Subscribe模式)

一种消息范式,发布者通过广播通道将消息广播出去,订阅者根据自己的订阅得到相应的消息。

使用场景:

  • webpack 里面的Tapable.js
const { SyncHook } = require('tapable');
// 实例化钩子函数,这里可以定义形参
const hook = new SyncHook(['auther', 'age']);
// 注册事件1
hook.tap('监听器1', (name, age) => {
    console.log('监听器1', name, age);
});
// 注册事件2
hook.tap('监听器2', (name) => {
    console.log('监听器2', name);
});
// 注册事件3
hook.tap('监听器3', (name) => {
    console.log('监听器3', name);
});
// 触发事件,这里传入实参,会被每一个注册函数接受到
hook.call('lly', 24);
  • node.js里面的事件处理机制 EventEmitter
//event.js 文件
var events = require('events'); 
var emitter = new events.EventEmitter(); 
emitter.on('someEvent', function(arg1, arg2) { 
    console.log('listener1', arg1, arg2); 
}); 
emitter.on('someEvent', function(arg1, arg2) { 
    console.log('listener2', arg1, arg2); 
}); 
emitter.emit('someEvent', 'arg1 参数', 'arg2 参数'); 

  • 在DOM节点上添加事件
<div id="box"></div>
<script>
    function click() {
        console.log(3);
    }
    let box = document.getElementById('box')
    box.addEventListener('click', () => {
        console.log(1);
    })
    box.addEventListener('click', () => {
        console.log(2);
    })
    box.addEventListener('click', () => {
        click()
    })
    box.click()//模拟用户点击
</script>

3.观察者模式(Observer模式)

与发布订阅模式的异同:

相同: 《JavaScript设计模式与开发实践》一书中说分辨模式的关键是意图而不是结构。在意图方面上说,这两种模式的意图都是定义了对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知,并自动更新

不同: 而从结构方面来说,观察者模式是观察者和被观察者(目标对象)之间的通讯,两者是直接关联的,观察者模式通过将自己本身依附在目标对象上以便关注所感兴趣的内容;而发布-订阅模式是发布者和订阅者之间的通讯,但是两者不是直接关联的,而是通过一个事件通道或者说是以事件通道作为调度中心进行通信

源码:

class ObservedTarget {
    constructor() {
        this.observers = [] //用于存储所有的观察者
    }
    addObserver(...observer) {
        // console.log(...observer);
        this.observers.push(...observer) //添加观察者
    }
    notifyObserver(...args) {
        // 遍历观察者列表
        this.observers.forEach(item => {
            item.update(...args)
        })
    }
}
class Observer {
    constructor(name) {
        this.name = name
    }
    update(...args) {
        let content = [...args];
        console.log(`${this.name}接收到目标对象更新的状态是:${content}`);
    }
}
// 创建多个观察者
let observer1 = new Observer('observer1')
let observer2 = new Observer('observer2')
let observer3 = new Observer('observer3')
​
// 把观察者本身依附在目标对象上
let observerTarget = new ObservedTarget()
observerTarget.addObserver(observer1, observer2, observer3)//直接关联// 当目标对象更新内容时,通知所有的观察者
observerTarget.notifyObserver('这是我的新专辑!', '感谢粉丝对我的支持呀!')
​
//observer1接收到目标对象更新的状态是:这是我的新专辑!,感谢粉丝对我的支持呀!
//observer2接收到目标对象更新的状态是:这是我的新专辑!,感谢粉丝对我的支持呀!
//observer3接收到目标对象更新的状态是:这是我的新专辑!,感谢粉丝对我的支持呀!

使用场景:

  • Vue双向绑定原理

先创建一个响应式数据,即被观察者,响应式数据里面读取的时候收集副作用函数effect,即track(),这一步就是收集观察者,设置或者更新操作的时候触发副作用函数trigger(),即通知所有的观察者;

let currentEffect = []; // 通过这个全局变量将观察者和目标对象进行关联
// 目标对象
class Dep {
    constructor(val) {
        this._val = val;
        this.effects = new Set(); // 存储依赖,依赖只收集一次
    }
    get value() { //get操作
        // 读取
        this.tract() //每次读取都会触发依赖收集
        return this._val
​
    }
    set value(newVal) { //set操作
        // 修改
        this._val = newVal
        this.trigger() //值更新完毕后,通知更新
        return this._val
    }
    tract() { //收集依赖
        // 收集依赖时,需要先将收集的依赖存储起来,而且不重复收集依赖
        // 依赖是通过effectWatcher内部的回调函数配合effectWatcher实现的,所以需要关联到effectWatcher函数,可以先定义一个全局变量currentEffect
        if (currentEffect) {
            this.effects.add(currentEffect)
        }
    }
    trigger() { //通知更新
        // 遍历所有依赖并执行
        this.effects.forEach(effect => {
            effect()
        })
    }
}
// effect函数 观察者
function effectWatcher(effect) {
    currentEffect = effect;
    effect(); // 保证一上来就执行
    currentEffect = null
}

// 使用
const dep = new Dep('没有任何最新的动态~')
let content;
effectWatcher(() => {
    content = dep.value
    console.log(content);
})
// 当值发生改变
dep.value = '目标对象发布新专辑了!'
// 没有任何最新的动态~
// 目标对象发布新专辑了!

4.装饰器模式

允许向一个现有的对象添加新功能,同时不改变其结构。 使用场景:

  • React 高阶组件
import React from 'react';

const yellowHOC = WrapperComponent => {
    return class extends React.Component {
        render() {
            <div style={{ backgroundColor: 'yellow' }}>
                <WrapperComponent {...this.props} />
            </div>;
        }
    }
}
export default yellowHOC;
  • es装饰器 统一的错误提示、日志打印、访问接口时的loading等都可以使用装饰器模式。

5.适配器模式

为了解决不兼容的问题,把接口返回的数据结构统一修改成我们想要的数据结构,比如说接口返回有3种数据结构,但是我们的列表组件只接受一种数据结构的时候就可以用适配器模式。

6.代理模式

为对象提供一种代理,用来控制对这个对象的访问。 两个模块间的交互需要一定限制关系时候使用。 代理模式强调的是限制,装饰器强调的是添加,赋能。

使用场景:

  • 屏蔽XX邮箱
// 发邮件,不是qq邮箱的拦截
const emailList = ['qq.com', '163.com', 'gmail.com'];

// 代理
const ProxyEmail = function(email) {
  if (emailList.includes(email)) {
    // 屏蔽处理
  } else {
    // 转发,进行发邮件
    SendEmail.call(this, email);
  }
};

const SendEmail = function(email) {
  // 发送邮件
};

// 外部调用代理
ProxyEmail('cvte.com');
ProxyEmail('ojbk.com');

  • 预先设置图片的loading图
// 本体
var domImage = (function() {
  var imgEle = document.createElement('img');
  document.body.appendChild(imgEle);
  return {
    setSrc: function(src) {
      imgEle.src = src;
    }
  };
})();

// 代理
var proxyImage = (function() {
  var img = new Image();
  img.onload = function() {
    domImage.setSrc(this.src); // 图片加载完设置真实图片src
  };
  return {
    setSrc: function(src) {
      domImage.setSrc('./loading.gif'); // 预先设置图片src为loading图
      img.src = src;
    }
  };
})();

// 外部调用
proxyImage.setSrc('./product.png');

  • es6 Proxy代理器

7.责任链模式

避免请求发送者与接收者耦合在一起(比如:A里面有B,B里面有C,C里面有D);责任链模式可以使个节点解耦,并且灵活拆分重组。

责任链模式示例:

const Chain = function(fn) {
  this.fn = fn;
  
  this.setNext = function() {}

  this.run = function() {}
}

const applyDevice = function() {}
const chainApplyDevice = new Chain(applyDevice);

const selectAddress = function() {}
const chainSelectAddress = new Chain(selectAddress);

const selectChecker = function() {}
const chainSelectChecker = new Chain(selectChecker);

// 运用责任链模式实现上边功能
chainApplyDevice.setNext(chainSelectAddress).setNext(chainSelectChecker);
chainApplyDevice.run();

参考:
# 前端渣渣唠嗑一下前端中的设计模式(真实场景例子)
# 两个焦点:观察者模式和发布-订阅模式不一样?