前言
以下内容主要是笔者的笔记内容,有些设计模式在前端里特别常见的就省略了。
JavaScript 设计模式核心原理与应用实践
- 创建型
- 构造器模式
- 工厂模式
- 抽象工厂模式
- 原型模式
- 结构型
- 装饰器模式
- 适配器模式
- 代理模式
- 行为型
- 观察者模式
- 策略模式
- 状态模式
- 迭代器模式
- 主要用到的设计模式基本都围绕
单一功能
和开放封闭
这两个原则来展开 - 将变与不变分离,确保变化的部分灵活、不变的部分稳定,这样的代码,就是我们所谓的
健壮
的代码,它可以经得起变化的考验
工厂模式
简单工厂模式
先介绍一下构造器模式:
js
function User(name , age, career) {
this.name = name
this.age = age
this.career = career
}
楼上个这 User,就是一个构造器。
简单的调用:
javascript
const user = new User(name, age, career)
从此再也不用手写字面量。
工厂模式 其实就是将创建对象的过程单独封装。
这个承载了共性的 User 类和个性化的逻辑判断写入同一个函数:
javascript
function User(name , age, career, work) {
this.name = name
this.age = age
this.career = career
this.work = work
}
function Factory(name, age, career) {
let work
switch(career) {
case 'coder':
work = ['写代码','写系分', '修Bug']
break
case 'product manager':
work = ['订会议室', '写PRD', '催更']
break
case 'boss':
work = ['喝茶', '看报', '见客户']
case 'xxx':
// 其它工种的职责分配
...
return new User(name, age, career, work)
}
这样一来,我们要做事情是不是简单太多了?不用自己时刻想着我拿到的这组数据是什么工种、我应该怎么给它分配构造函数,更不用手写无数个构造函数------Factory已经帮我们做完了一切,而我们只需要像以前一样无脑传参就可以了!
抽象工厂模式
抽象类:
js
class MobilePhoneFactory {
// 提供操作系统的接口
createOS(){
throw new Error("抽象工厂方法不允许直接调用,你需要将我重写!");
}
// 提供硬件的接口
createHardWare(){
throw new Error("抽象工厂方法不允许直接调用,你需要将我重写!");
}
}
具体工厂:
js
// 具体工厂继承自抽象工厂
class FakeStarFactory extends MobilePhoneFactory {
createOS() {
// 提供安卓系统实例
return new AndroidOS()
}
createHardWare() {
// 提供高通硬件实例
return new QualcommHardWare()
}
}
抽象操作系统类:
js
// 定义操作系统这类产品的抽象产品类
class OS {
controlHardWare() {
throw new Error('抽象产品方法不允许直接调用,你需要将我重写!');
}
}
// 定义具体操作系统的具体产品类
class AndroidOS extends OS {
controlHardWare() {
console.log('我会用安卓的方式去操作硬件')
}
}
class AppleOS extends OS {
controlHardWare() {
console.log('我会用🍎的方式去操作硬件')
}
}
...
硬件类产品同理:
javascript
// 定义手机硬件这类产品的抽象产品类
class HardWare {
// 手机硬件的共性方法,这里提取了"根据命令运转"这个共性
operateByOrder() {
throw new Error('抽象产品方法不允许直接调用,你需要将我重写!');
}
}
// 定义具体硬件的具体产品类
class QualcommHardWare extends HardWare {
operateByOrder() {
console.log('我会用高通的方式去运转')
}
}
class MiWare extends HardWare {
operateByOrder() {
console.log('我会用小米的方式去运转')
}
}
...
好了,如此一来,当我们需要生产一台FakeStar手机时,我们只需要这样做:
javascript
// 这是我的手机
const myPhone = new FakeStarFactory()
// 让它拥有操作系统
const myOS = myPhone.createOS()
// 让它拥有硬件
const myHardWare = myPhone.createHardWare()
// 启动操作系统(输出'我会用安卓的方式去操作硬件')
myOS.controlHardWare()
// 唤醒硬件(输出'我会用高通的方式去运转')
myHardWare.operateByOrder()
关键的时刻来了------假如有一天,FakeStar过气了,我们需要产出一款新机投入市场,这时候怎么办?我们是不是不需要对抽象工厂MobilePhoneFactory做任何修改,只需要拓展它的种类:
javascript
class newStarFactory extends MobilePhoneFactory {
createOS() {
// 操作系统实现代码
}
createHardWare() {
// 硬件实现代码
}
}
这么个操作,对原有的系统不会造成任何潜在影响 所谓的对拓展开放,对修改封闭
就这么圆满实现了。前面我们之所以要实现抽象产品类,也是同样的道理。
单例模式
闭包实现:
js
SingleDog.getInstance = () => {
let instance = null;
return function() {
if(!instance){
instance = new SingleDog();
}
return instance;
}
}
一个 Vue 应用只能对应一个 Store,在Vuex中的使用:
- 保证在同一个
Vue
应用里只能有一个Vuex
实例
js
let Vue // 这个Vue的作用和楼上的instance作用一样
export function install (_Vue) {
// 判断传入的Vue实例对象是否已经被install过Vuex插件(是否有了唯一的 store)
if (Vue && _Vue === Vue) {
if (process.env.NODE_ENV !== 'production') {
console.error(
'[vuex] already installed. Vue.use(Vuex) should be called only once.'
)
}
return
}
// 若没有,则为这个Vue实例对象install一个唯一的Vuex
Vue = _Vue
// 将Vuex的初始化逻辑写进Vue的钩子函数里
applyMixin(Vue)
}
- 保证了在同一个
Vue
应用中只存在一个Vuex
实例。
js
function vuexInit () {
const options = this.$options
// 将 store 实例挂载到 Vue 实例上
if (options.store) {
this.$store = typeof options.store === 'function'
? options.store()
: options.store
} else if (options.parent && options.parent.$store) {
this.$store = options.parent.$store
}
}
这段逻辑意味着,$store
实例在 Vue 组件树中是被层层继承 下来的------当子组件自身不具备 $store
时,会查找父组件的 $store
并继承。这样,整个 Vue 组件树中的所有组件都会访问到同一个 Store
实例------那就是根组件的Store
实例。
尽管 Vuex 并不是严格意义上的单例模式,但它却很大程度上从单例模式的思想中受益,也为我们在实践中应用单例模式提供了全新的思路。
实现一个 Storage
js
// 先实现一个基础的StorageBase类,把getItem和setItem方法放在它的原型链上
function StorageBase () {}
StorageBase.prototype.getItem = function (key){
return localStorage.getItem(key)
}
StorageBase.prototype.setItem = function (key, value) {
return localStorage.setItem(key, value)
}
// 以闭包的形式创建一个引用自由变量的构造函数
const Storage = (function(){
let instance = null
return function(){
// 判断自由变量是否为null
if(!instance) {
// 如果为null则new出唯一实例
instance = new StorageBase()
}
return instance
}
})()
// 这里其实不用 new Storage 的形式调用,直接 Storage() 也会有一样的效果
const storage1 = new Storage()
const storage2 = new Storage()
storage1.setItem('name', '李雷')
// 李雷
storage1.getItem('name')
// 也是李雷
storage2.getItem('name')
// 返回true
storage1 === storage2
实现一个全局的模态框
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>单例模式弹框</title>
</head>
<style>
#modal {
height: 200px;
width: 200px;
line-height: 200px;
position: fixed;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
border: 1px solid black;
text-align: center;
}
</style>
<body>
<button id='open'>打开弹框</button>
<button id='close'>关闭弹框</button>
</body>
<script>
// 核心逻辑,这里采用了闭包思路来实现单例模式
const Modal = (function() {
let modal = null
return function() {
if(!modal) {
modal = document.createElement('div')
modal.innerHTML = '我是一个全局唯一的Modal'
modal.id = 'modal'
modal.style.display = 'none'
document.body.appendChild(modal)
}
return modal
}
})()
// 点击打开按钮展示模态框
document.getElementById('open').addEventListener('click', function() {
// 未点击则不创建modal实例,避免不必要的内存占用;此处不用 new Modal 的形式调用也可以,和 Storage 同理
const modal = new Modal()
modal.style.display = 'block'
})
// 点击关闭按钮隐藏模态框
document.getElementById('close').addEventListener('click', function() {
const modal = new Modal()
if(modal) {
modal.style.display = 'none'
}
})
</script>
</html>
原型模式
- 谈原型模式,其实是谈原型范式,原型编程范式的体现就是基于原型链的继承。
装饰器模式
在 ES7 中,我们可以像写 python 一样通过一个@语法糖轻松地给一个类装上装饰器:
javascript
// 装饰器函数,它的第一个参数是目标类
function classDecorator(target) {
target.hasDecorator = true
return target
}
// 将装饰器"安装"到Button类上
@classDecorator
class Button {
// Button类的相关逻辑
}
// 验证装饰器是否生效
console.log('Button 是否被装饰了:', Button.hasDecorator)
我们可以联想记忆函数的作用,就能理解装饰器了:
js
// 创建一个函数 memoize,接受一个函数作为参数
function memoize(func) {
// 创建一个空对象 cache,用于存储计算结果
const cache = {};
// 返回一个新的函数,此处使用了 rest parameter 操作符 ...args,
// 它可以让我们将传入的参数转换成一个数组
return function(...args) {
// 使用 JSON.stringify 将参数列表 args 转换成字符串,作为缓存对象 cache 的 key 值
const key = JSON.stringify(args);
// 如果缓存对象中存在该 key 值,则直接返回缓存值
if (cache[key]) {
console.log('从缓存中获取结果');
return cache[key];
} else {
// 否则,执行原始函数,将结果存入缓存对象中,并返回结果
console.log('进行计算');
const result = func.apply(this, args);
cache[key] = result;
return result;
}
}
}
function fib(n) {
if (n < 2) return n;
return fib(n - 1) + fib(n - 2);
}
console.time('fib(40)');
console.log(fib(40));
console.timeEnd('fib(40)');
const memoizedFib = memoize(fib);// 高阶函数
console.time('memoizedFib(40)');
console.log(memoizedFib(40));
console.timeEnd('memoizedFib(40)');
适配器模式
- 兼容代码就是一把梭
- 生产实践中:
axios
中的适配器
js
function getDefaultAdapter() {
var adapter;
// 判断当前是否是node环境
if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') {
// 如果是node环境,调用node专属的http适配器
adapter = require('./adapters/http');
} else if (typeof XMLHttpRequest !== 'undefined') {
// 如果是浏览器环境,调用基于xhr的适配器
adapter = require('./adapters/xhr');
}
return adapter;
}
代理模式
- 事件代理
用代理模式实现多个子元素的事件监听,代码会简单很多:
javascript
// 获取父元素
const father = document.getElementById('father')
// 给父元素安装一次监听函数
father.addEventListener('click', function(e) {
// 识别是否是目标子元素
if(e.target.tagName === 'A') {
// 以下是监听函数的函数体
e.preventDefault()
alert(`我是${e.target.innerText}`)
}
} )
在这种做法下,我们的点击操作并不会直接触及目标子元素,而是由父元素对事件进行处理和分发、间接地将其作用于子元素,因此这种操作从模式上划分属于代理模式。
- 保护代理
- proxy 代理模式
策略模式
实际上常见在弹窗组件里,策略模式适用于需要根据不同情况采取不同行为的场景,它能够减少大量的条件语句,并提高代码的可读性和可维护性。同时,通过将算法封装成独立的策略函数,可以方便地扩展和添加新的算法。
js
// 策略对象
const strategies = {
add: function(a, b) {
return a + b;
},
subtract: function(a, b) {
return a - b;
},
multiply: function(a, b) {
return a * b;
},
divide: function(a, b) {
return a / b;
}
};
// 上下文对象
function calculate(strategy, a, b) {
if (typeof strategies[strategy] === 'function') {
return strategies[strategy](a, b);
} else {
throw new Error('Invalid strategy');
}
}
// 使用示例
console.log(calculate('add', 5, 2)); // 输出: 7
console.log(calculate('subtract', 5, 2)); // 输出: 3
console.log(calculate('multiply', 5, 2)); // 输出: 10
console.log(calculate('divide', 5, 2)); // 输出: 2.5
状态模式
状态模式是一种行为设计模式,它允许对象在内部状态发生变化时改变其行为。
js
// 定义不同的状态对象
const states = {
idle: {
handleInput: function() {
console.log('当前状态为:空闲');
console.log('处理输入事件');
// 处理空闲状态的逻辑
},
update: function() {
console.log('当前状态为:空闲');
console.log('更新状态');
// 更新空闲状态的逻辑
}
},
running: {
handleInput: function() {
console.log('当前状态为:运行');
console.log('处理输入事件');
// 处理运行状态的逻辑
},
update: function() {
console.log('当前状态为:运行');
console.log('更新状态');
// 更新运行状态的逻辑
}
}
};
// 上下文对象
function Game() {
let currentState = states.idle;
this.changeState = function(state) {
currentState = state;
};
this.handleInput = function() {
currentState.handleInput();
};
this.update = function() {
currentState.update();
};
}
// 使用示例
const game = new Game();
game.handleInput(); // 输出: 当前状态为:空闲 / 处理输入事件
game.update(); // 输出: 当前状态为:空闲 / 更新状态
game.changeState(states.running);
game.handleInput(); // 输出: 当前状态为:运行 / 处理输入事件
game.update(); // 输出: 当前状态为:运行 / 更新状态
状态模式可以很好地将对象的行为和内部状态解耦,使得对象能够根据内部状态的变化自动改变行为。这样可以避免大量的条件语句和分支判断,提高代码的可读性和可维护性。同时,通过将状态封装成独立的对象,可以方便地扩展和添加新的状态。
两者区别和联系:
-
状态模式和策略模式都是行为设计模式,它们允许对象根据不同情况采取不同的行为。
-
状态模式通过改变对象的内部状态来改变行为,每个状态对应一个对象。对象的行为取决于当前状态,适用于需要根据不同状态采取不同行为的场景。
-
策略模式将算法或行为封装成独立的对象,对象的行为取决于传入的策略对象。适用于需要在运行时根据不同条件选择不同算法的场景。
-
状态模式的上下文对象维护状态变量,根据变量调用对应状态对象的方法。策略模式的上下文对象接受策略对象作为参数,调用对应算法或行为函数。
-
总的来说,状态模式和策略模式在目的、实现方式和上下文对象角色上有所不同,根据具体需求选择适合的模式。