Skip to content

设计模式

JavaScript中常用的设计模式

JavaScript 中常见设计模式整理

工厂模式

js
function createPerson(name, age, job) { 
 let o = new Object(); 
 o.name = name; 
 o.age = age; 
 o.job = job; 
 o.sayName = function() { 
 console.log(this.name); 
 }; 
 return o; 
} 
let person1 = createPerson("Nicholas", 29, "Software Engineer"); 
let person2 = createPerson("Greg", 27, "Doctor");

工厂模式虽然可以解决创建多个类似对象的问题,但没有解决对象标识问题题(即新创建的对象是什么类型)

构造函数模式

js
function Person(name, age, job){ 
 this.name = name; 
 this.age = age; 
 this.job = job; 
 this.sayName = function() { 
 console.log(this.name); 
 }; 
} 
let person1 = new Person("Nicholas", 29, "Software Engineer"); 
let person2 = new Person("Greg", 27, "Doctor"); 
person1.sayName(); // Nicholas 
person2.sayName(); // Greg

在这个例子中,Person()构造函数代替了 createPerson()工厂函数。实际上,Person()内部的代码跟 createPerson()基本是一样的,只是有如下区别。

  • 没有显式地创建对象。

  • 属性和方法直接赋值给了 this。

  • 没有 return

要创建 Person 的实例,应使用 new 操作符。以这种方式调用构造函数会执行如下操作。

(1) 在内存中创建一个新对象。

(2) 这个新对象内部的[[Prototype]]特性被赋值为构造函数的 prototype 属性。

(3) 构造函数内部的 this 被赋值为这个新对象(即 this 指向新对象)

(4) 执行构造函数内部的代码(给新对象添加属性)。

(5) 如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象。

  1. 构造函数也是函数, 与普通函数唯一的区别就是调用方式不同

  2. 构造函数的问题

​ 构造函数的主要问题在于,其定义的方法会在每个实例上都创建一遍. 即每次new都会创造自己的Function

但是, 如果把函数定义在构造函数外, 样虽然解决了相同逻辑的函数重复定义的问题,但全局作用域也因此被搞乱了,因为那个函数实际上只能在一个对象上调用。如果这个对象需要多个方法,那么就要在全局作用域中定义多个函数. 这个新问题可以通过原型模式来解决。

原型模式

每个函数都会创建一个 prototype 属性,这个属性是一个对象,包含应该由特定引用类型 (函数/对象) 的实例共享的属性和方法. 这个对象就是通过调用构造函数创建的对象的原型

js
// 所以, 就是把要共享的 函数\属性 挂载到公共的
function Person() {} 
Person.prototype.name = "Nicholas"; 
Person.prototype.age = 29; 
Person.prototype.job = "Software Engineer"; 
Person.prototype.sayName = function() { 
 console.log(this.name); 
}; 
let person1 = new Person(); 
person1.sayName(); // "Nicholas" 
let person2 = new Person(); 
person2.sayName(); // "Nicholas" 
console.log(person1.sayName == person2.sayName); // true

与构造函数模式不同,使用这种原型模式定义的属性和方法是由所有实例共享的。

适配器模式

设计模式-适配器模式

接口不兼容,即使用的属性不一样

比如a组件展示name, tilte,接口回来的是PigName, PigTime就要用适配器做一层转换;

又比如说不同的库暴露出来的方法不一致,这时候就要用适配器保证调用的接口一致

js
class GooleMap {
    show() {
        console.log('渲染谷歌地图')
    }
}

class BaiduMap {
    display() {
        console.log('渲染百度地图')
    }
}


// 定义适配器类, 对BaiduMap类进行封装
class BaiduMapAdapter {
    show() {
        var baiduMap = new BaiduMap()
        return baiduMap.display() 
    }
}

function render(map) {
    if (map.show instanceof Function) {
        map.show()
    }
}

render(new GooleMap())         // 渲染谷歌地图
render(new BaiduMapAdapter())  // 渲染百度地图

适配器模式符合开放封闭原则

关于开放封闭原则,其核心的思想是:

软件实体应该是可扩展,而不可修改的。也就是说,对扩展是开放的,而对修改是封闭的。

因此,开放封闭原则主要体现在两个方面:

对扩展开放,意味着有新的需求或变化时,可以对现有代码进行扩展,以适应新的情况。

对修改封闭,意味着类一旦设计完成,就可以独立完成其工作,而不要对类进行任何修改。

“需求总是变化”、“世界上没有一个软件是不变的”,这些言论是对软件需求最经典的表白。从中透射出一个关键的意思就是,对于软件设计者来说,必须在不需要对原有的系统进行修改的情况下,实现灵活的系统扩展。而如何能做到这一点呢?

代理模式

设计模式-代理模式

vue3 proxy

  1. 代理模式符合开放封闭原则
  2. 本体对象和代理对象拥有相同的方法,在用户看来并不知道请求的本体对象还是代理对象。

迭代器模式

设计模式-迭代器模式

es6迭代器

策略模式

JavaScript 设计模式之策略模式

定义一系列的算法,把它们一个个封装起来,并使它们可以替换

js
var fnA = function(val) {
    return val * 1
}

var fnB = function(val) {
    return val * 2
}

var fnC = function (val) {
    return val * 3
}


var calculate = function(fn, val) {
    return fn(val)
}

console.log(calculate(fnA, 100))// 100
console.log(calculate(fnB, 100))// 200
console.log(calculate(fnC, 100))// 300

组合模式

组合模式

  • 组合模式在对象间形成树形结构;
  • 组合模式中基本对象和组合对象被一致对待;
  • 无须关心对象有多少层, 调用时只需在根部进行调用;

img

js
const MacroCommand = function() {
  return {
    lists: [],
    add: function(task) {
      this.lists.push(task)
    },
    excute: function() { // ①: 组合对象调用这里的 excute,
      for (let i = 0; i < this.lists.length; i++) {
        this.lists[i].excute()
      }
    },
  }
}

const command1 = MacroCommand() // 基本对象

command1.add({
  excute: () => console.log('煮咖啡') // ②: 基本对象调用这里的 excute,
})

const command2 = MacroCommand() // 组合对象

command2.add({
  excute: () => console.log('打开电视')
})

command2.add({
  excute: () => console.log('打开音响')
})

const command3 = MacroCommand()

command3.add({
  excute: () => console.log('打开空调')
})

command3.add({
  excute: () => console.log('打开电脑')
})

const macroCommand = MacroCommand()
macroCommand.add(command1)
macroCommand.add(command2)
macroCommand.add(command3)

macroCommand.excute()

// 煮咖啡
// 打开电视
// 打开音响
// 打开空调
// 打开电脑

单例模式

设计模式-单例模式

  1. 单例模式的主要思想就是,实例如果已经创建,则直接返回
  2. 符合开放封闭原则

观察者模式(发布-订阅模式)

设计模式-观察者模式

js
class Event {
    constructor() {
        this.eventTypeObj = {}
    }
    on(eventType, fn) {
        if (!this.eventTypeObj[eventType]) {
            // 按照不同的订阅事件类型,存储不同的订阅回调
            this.eventTypeObj[eventType] = []
        }
        this.eventTypeObj[eventType].push(fn)
    }
    emit() {
        // 可以理解为arguments借用shift方法
        var eventType = Array.prototype.shift.call(arguments)
        var eventList = this.eventTypeObj[eventType]
        for (var i = 0; i < eventList.length; i++) {
            eventList[i].apply(eventList[i], arguments)
        }
    }
    remove(eventType, fn) {
        // 如果使用remove方法,fn为函数名称,不能是匿名函数
        var eventTypeList = this.eventTypeObj[eventType]
        if (!eventTypeList) {
            // 如果没有被人订阅改事件,直接返回
            return false
        }
        if (!fn) {
            // 如果没有传入取消订阅的回调函数,则改订阅类型的事件全部取消
            eventTypeList && (eventTypeList.length = 0)
        } else {
            for (var i = 0; i < eventTypeList.length; i++) {
                if (eventTypeList[i] === fn) {
                    eventTypeList.splice(i, 1)
                    // 删除之后,i--保证下轮循环不会漏掉没有被遍历到的函数名
                    i--;
                }
            }
        }
    }
}
var handleFn = function(data) {
    console.log(data)
}
var event = new Event()
event.on('click', handleFn)
event.emit('click', '1')   // 1
event.remove('click', handleFn)
event.emit('click', '2')  // 不打印
  1. 发布订阅模式可以使代码解耦,满足开放封闭原则
  2. 当过多的使用发布订阅模式,如果订阅消息始终都没有触发,则订阅者一直保存在内存中。

命令模式

vuex算吗

设计模式-命令模式

html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>cmd-demo</title>
</head>
<body>
<div>
  <button id="btn1">按钮1</button>
  <button id="btn2">按钮2</button>
<!--  <button id="btn3">按钮3</button>-->
</div>
<script>
  var btn1 = document.getElementById('btn1')
  var btn2 = document.getElementById('btn2')
  // var btn3 = document.getElementById('btn3')

  // 定义一个命令发布者(执行者)的类
  class Executor {
    setCommand(btn, command) {
      btn.onclick = function() {
        command.execute()
      }
    }
  }

  // 定义一个命令接收者
  class Menu {
    refresh() {
      console.log('刷新菜单')
    }

    addSubMenu() {
      console.log('增加子菜单')
    }
  }

  // 定义一个刷新菜单的命令对象的类
  class RefreshMenu {
    constructor(receiver) {
      // 命令对象与接收者关联
      this.receiver = receiver
    }

    // 暴露出统一的接口给命令发布者Executor
    execute() {
      this.receiver.refresh()
    }
  }

  // 定义一个增加子菜单的命令对象的类
  class AddSubMenu {
    constructor(receiver) {
      // 命令对象与接收者关联
      this.receiver = receiver
    }
    // 暴露出统一的接口给命令发布者Executor
    execute() {
      this.receiver.addSubMenu()
    }
  }

  var menu = new Menu()
  var executor = new Executor()

  var refreshMenu = new RefreshMenu(menu)
  // 给按钮1添加刷新功能
  executor.setCommand(btn1, refreshMenu)

  var addSubMenu = new AddSubMenu(menu)
  // 给按钮2添加增加子菜单功能
  executor.setCommand(btn2, addSubMenu)

  // 如果想给按钮3增加删除菜单的功能,就继续增加删除菜单的命令对象和接收者的具体删除方法,而不必修改命令对象
</script>
</body>
</html>
  • 命令接收者 receiver:实现命令的类
  • 命令发布者(执行者)Executor:绑定元素并触发执行执行函数的类
  • 功能类,暴露exectue方法,接收receiver引用

状态模式

设计模式-状态模式

举一个关于开关控制电灯的例子,电灯只有一个开关

  • 第一次按下打开弱光
  • 第二次按下打开强光
  • 第三次按下关闭。
html
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <title>state-demo</title>
</head>

<body>
    <button id="btn">开关</button>
    <script>
        // 定义一个关闭状态的类   
        class OffLightState {
            constructor(light) {
                this.light = light
            }
            // 每个类都需要这个方法,在不同状态下按都需要触发这个方法
            pressBtn() {
                this.light.setState(this.light.weekLightState)
                console.log('开启弱光')
            }
        }

        // 定义一个弱光状态的类   
        class WeekLightState {
            constructor(light) {
                this.light = light
            }
            pressBtn() {
                this.light.setState(this.light.strongLightState)
                console.log('开启强光')
            }
        }

        // 定义一个强光状态的类
        class StrongLightState {
            constructor(light) {
                this.light = light
            }
            pressBtn() {
                this.light.setState(this.light.offLightState)
                console.log('关闭电灯')
            }
        }


        class Light {
            constructor() {
                this.offLightState = new OffLightState(this)
                this.weekLightState = new WeekLightState(this)
                this.strongLightState = new StrongLightState(this)
                this.currentState = null
            }
            setState(newState) {
                this.currentState = newState
            }
            init() {
                this.currentState = this.offLightState
            }
        }

        let light = new Light()
        light.init()
        var btn = document.getElementById('btn')
        btn.onclick = function() {
            light.currentState.pressBtn()
        }
    </script>
</body>

</html>

给三个状态都初始化一个类,各自持有pressBtn方法,点击就触发公共的setState,传入灯的状态,触发状态改变

职责链模式

职责链模式

外观(facade)模式

精读《设计模式 - Facade 外观模式》

我的理解是把一组功能抽象成一个函数,有统一的出入口,

Released under the MIT License.