# 设计模式精选面试题

# 介绍一下单一职责原则和开放封闭原则

  • 单一职责原则:一个类只负责一个功能领域中的相应职责,或者可以定义为:就一个类而言,应该只有一个引起它变化的原因。

  • 开放封闭原则:核心的思想是软件实体(类、模块、函数等)是可扩展的、但不可修改的。也就是说,对扩展是开放的,而对修改是封闭的。

# 观察者模式和发布订阅模式的区别

观察者模式和发布订阅模式最大的区别就是发布订阅模式有个事件调度中心。

往更深层次讲:观察者和被观察者,是松耦合的关系,发布者和订阅者,则完全不存在耦合

// 发布/订阅模式简单应用
class Notify {
  constructor () {
	this.subscribers = []
  }
  add (handler) {
	this.subscribers.push(handler)
  }
  emit () {
	this.subscribers.forEach(subscriber => subscriber())
  }
  let notify = new Notify()
  notify.add(() => {
	console.log('emit here')
  })
  notify.emit() // emit here
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 单例模式

单例模式即一个类只能构造出唯一实例,单例模式的意义在于共享、唯一,Redux/Vuex 中的 storeJQ$ 或者业务场景中的购物车、登录框都是单例模式的应用

class SingletonLogin {
  constructor(name,password){
    this.name = name
    this.password = password
  }
  static getInstance(name,password){
    // 判断对象是否已经被创建,若创建则返回旧对象
    if(!this.instance)this.instance = new SingletonLogin(name,password)
    return this.instance
  }
}
 
let obj1 = SingletonLogin.getInstance('CXK','123')
let obj2 = SingletonLogin.getInstance('CXK','321')
 
console.log(obj1===obj2)    // true
console.log(obj1)           // {name:CXK,password:123}
console.log(obj2)           // 输出的依然是{name:CXK,password:123}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 工厂模式

工厂模式即对创建对象逻辑的封装,或者可以简单理解为对 new 的封装,这种封装就像创建对象的工厂,故名工厂模式。工厂模式常见于大型项目,比如 JQ$ 对象,我们创建选择器对象时之所以没有 new selector 就是因为 $() 已经是一个工厂方法,其他例子例如React.createElement()Vue.component()都是工厂模式的实现。工厂模式有多种:简单工厂模式、工厂方法模式、抽象工厂模式,这里只以简单工厂模式为例:

class User {
  constructor(name, auth) {
    this.name = name
    this.auth = auth
  }
}

class UserFactory {
  static createUser(name, auth) {
    // 工厂内部封装了创建对象的逻辑:
    // 权限为admin时,auth=1, 权限为user时, auth为2
    // 使用者在外部创建对象时,不需要知道各个权限对应哪个字段, 不需要知道赋权的逻辑,只需要知道创建了一个管理员和用户
    if(auth === 'admin')  new User(name, 1)
    if(auth === 'user')  new User(name, 2)
  }
}

const admin = UserFactory.createUser('cxk', 'admin');
const user = UserFactory.createUser('cxk', 'user');
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 观察者模式

观察者模式算是前端最常用的设计模式了,观察者模式概念很简单:观察者监听被观察者的变化,被观察者发生改变时,通知所有的观察者。观察者模式被广泛用于监听事件的实现。

//观察者
class Observer {    
  constructor (fn) {      
    this.update = fn    
  }
}
//被观察者
class Subject {    
    constructor() {        
        this.observers = []          // 观察者队列    
    }    
    addObserver(observer) {          
        this.observers.push(observer)// 往观察者队列添加观察者    
    }    
    notify() {                       // 通知所有观察者,实际上是把观察者的update()都执行了一遍       
        this.observers.forEach(observer => {        
            observer.update()            // 依次取出观察者,并执行观察者的update方法        
        })    
    }
}

var subject = new Subject()       // 被观察者
const update = () => {console.log('被观察者发出通知')}  //收到广播时要执行的方法
var ob1 = new Observer(update)    // 观察者1
var ob2 = new Observer(update)    // 观察者2
subject.addObserver(ob1)          // 观察者1订阅subject的通知
subject.addObserver(ob2)          // 观察者2订阅subject的通知
subject.notify()                  // 发出广播,执行所有观察者的update方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

有些文章也把观察者模式称为发布订阅模式,其实二者是有所区别的,发布订阅相较于观察者模式多一个调度中心。

# 装饰器模式

装饰器模式,可以理解为对类的一个包装,动态地拓展类的功能,ES7 的装饰器语法以及 React 中的高阶组件 (HoC) 都是这一模式的实现。react-reduxconnect() 也运用了装饰器模式,这里以 ES7 的装饰器为例:

function info(target) {
  target.prototype.name = '张三'
  target.prototype.age = 10
}

// @info
class Man {}

let man = new Man()
man.name // 张三
1
2
3
4
5
6
7
8
9
10

# 适配器模式

适配器模式,将一个接口转换成客户希望的另一个接口,使接口不兼容的那些类可以一起工作。我们在生活中就常常有使用适配器的场景,例如出境旅游插头插座不匹配,这时我们就需要使用转换插头,也就是适配器来帮我们解决问题。

class Adaptee {
  test() {
      return '旧接口'
  }
}
 
class Target {
  constructor() {
      this.adaptee = new Adaptee()
  }
  test() {
      let info = this.adaptee.test()
      return `适配${info}`
  }
}
 
let target = new Target()
console.log(target.test())
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 代理模式

代理模式,为一个对象找一个替代对象,以便对原对象进行访问。即在访问者与目标对象之间加一层代理,通过代理做授权和控制。最常见的例子是经纪人代理明星业务,假设你作为一个投资者,想联系明星打广告,那么你就需要先经过代理经纪人,经纪人对你的资质进行考察,并通知你明星排期,替明星本人过滤不必要的信息。事件代理、JQuery$.proxy,ES6proxy 都是这一模式的实现,下面以 ES6proxy 为例:

const idol = {
  name: '蔡x抻',
  phone: 10086,
  price: 1000000  //报价
}

const agent = new Proxy(idol, {
  get: function(target) {
    //拦截明星电话的请求,只提供经纪人电话
    return '经纪人电话:10010'
  },
  set: function(target, key, value) {
    if(key === 'price' ) {
      //经纪人过滤资质
      if(value < target.price) throw new Error('报价过低')
      target.price = value
    }
  }
})


agent.phone        // 经纪人电话:10010
agent.price = 100  // Uncaught Error: 报价过低
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

参考链接:

前端基础拾遗90问 (opens new window)