JavaScript设计模式:https://juejin.cn/post/6844903503266054157
JavaScript设计模式:https://www.jianshu.com/p/90cebf0e529d
JavaScript设计模式整理:https://juejin.cn/post/6844903607452581896
JavaScript设计模式es6(23种):https://juejin.cn/post/6844904032826294286
以下内容来自:(大佬太强了!)
JavaScript设计模式整理:https://juejin.cn/post/6844903607452581896
JavaScript设计模式es6(23种):https://juejin.cn/post/6844904032826294286
设计模式原则
S – Single Responsibility Principle 单一职责原则
一个程序只做好一件事
如果功能过于复杂就拆分开,每个部分保持独立
O – OpenClosed Principle 开放/封闭原则
对扩展开放,对修改封闭
增加需求时,扩展新代码,而非修改已有代码
L – Liskov Substitution Principle 里氏替换原则
子类能覆盖父类
父类能出现的地方子类就能出现
I – Interface Segregation Principle 接口隔离原则
保持接口的单一独立
类似单一职责原则,这里更关注接口
D – Dependency Inversion Principle 依赖倒转原则
面向接口编程,依赖于抽象而不依赖于具体
使用方只关注接口而不关注具体类的实现
SO体现较多,举个栗子:(比如Promise)
单一职责原则:每个then中的逻辑只做好一件事
开放封闭原则(对扩展开放,对修改封闭):如果新增需求,扩展then
单例模式
单例模式两个条件
- 确保只有一个实例
- 可以全局访问
适用
适用于弹框的实现, 全局缓存
实现单例模式
const singleton = function(name) {
this.name = name
this.instance = null
}
singleton.prototype.getName = function() {
console.log(this.name)
}
singleton.getInstance = function(name) {
if (!this.instance) { // 关键语句
this.instance = new singleton(name)
}
return this.instance
}
// test
const a = singleton.getInstance('a') // 通过 getInstance 来获取实例
const b = singleton.getInstance('b')
console.log(a === b)
JavaScript 中的单例模式
因为 JavaScript 是无类的语言, 而且 JS 中的全局对象符合单例模式两个条件。很多时候我们把全局对象当成单例模式来使用,
var obj = {}
弹框层的实践
实现弹框的一种做法是先创建好弹框, 然后使之隐藏, 这样子的话会浪费部分不必要的 DOM 开销, 我们可以在需要弹框的时候再进行创建, 同时结合单例模式实现只有一个实例, 从而节省部分 DOM 开销。下列为登入框部分代码:
const createLoginLayer = function() {
const div = document.createElement('div')
div.innerHTML = '登入浮框'
div.style.display = 'none'
document.body.appendChild(div)
return div
}
使用单例模式将创建弹框代码解耦, 代码如下:
const getSingle = function(fn) {
let result
return function() {
return result || (result = fn.apply(this, arguments))
}
}
const createSingleLoginLayer = getSingle(createLoginLayer)
document.getElementById('loginBtn').onclick = function() {
createSingleLoginLayer()
}
策略模式
策略模式
定义
: 根据不同参数可以命中不同的策略
JavaScript 中的策略模式
观察如下获取年终奖的 demo, 根据不同的参数(level)获得不同策略方法(规则), 这是策略模式在 JS 比较经典的运用之一。
const strategy = {
'S': function(salary) {
return salary * 4
},
'A': function(salary) {
return salary * 3
},
'B': function(salary) {
return salary * 2
}
}
const calculateBonus = function(level, salary) {
return strategy[level](salary)
}
calculateBonus('A', 10000) // 30000
在函数是一等公民的 JS 中, 策略模式的使用常常隐藏在高阶函数中, 稍微变换下上述 demo 的形式如下, 可以发现我们平时已经在使用它了, 恭喜我们又掌握了一种设计模式。
const S = function(salary) {
return salary * 4
}
const A = function(salary) {
return salary * 3
}
const B = function(salary) {
return salary * 2
}
const calculateBonus = function(func, salary) {
return func(salary)
}
calculateBonus(A, 10000) // 30000
优点
- 能减少大量的 if 语句
- 复用性好
代理模式
代理模式
情景: 小明追女生 A
- 非代理模式: 小明 =花=> 女生 A
- 代理模式: 小明 =花=> 让女生 A 的好友 B 帮忙 =花=> 女生 A
代理模式的特点
- 代理对象和本体对象具有一致的接口, 对使用者友好
代理模式的种类有很多, 在 JS 中最常用的为虚拟代理和缓存代理。
虚拟代理实现图片预加载
下面这段代码运用代理模式来实现图片预加载, 可以看到通过代理模式巧妙地将创建图片与预加载逻辑分离, 并且在未来如果不需要预加载, 只要改成请求本体代替请求代理对象就行。
const myImage = (function() {
const imgNode = document.createElement('img')
document.body.appendChild(imgNode)
return {
setSrc: function(src) {
imgNode.src = src
}
}
})()
const proxyImage = (function() {
const img = new Image()
img.onload = function() { // http 图片加载完毕后才会执行
myImage.setSrc(this.src)
}
return {
setSrc: function(src) {
myImage.setSrc('loading.jpg') // 本地 loading 图片
img.src = src
}
}
})()
proxyImage.setSrc('http://loaded.jpg')
缓存代理实现乘积计算
const mult = function() {
let a = 1
for (let i = 0, l; l = arguments[i++];) {
a = a * l
}
return a
}
const proxyMult = (function() {
const cache = {}
return function() {
const tag = Array.prototype.join.call(arguments, ',')
if (cache[tag]) {
return cache[tag]
}
cache[tag] = mult.apply(this, arguments)
return cache[tag]
}
})()
proxyMult(1, 2, 3, 4) // 24
小 tip
在开发时候不要先去猜测是否需要使用代理模式, 如果发现直接使用某个对象不方便时, 再来优化不迟。
发布订阅模式
发布订阅模式
事件发布/订阅模式 (PubSub) 在异步编程中帮助我们完成更松的解耦, 甚至在 MVC、MVVC 的架构中以及设计模式中也少不了发布-订阅模式的参与。
优点: 在异步编程中实现更深的解耦
缺点: 如果过多的使用发布订阅模式, 会增加维护的难度
实现一个发布订阅模式
var Event = function() {
this.obj = {}
}
Event.prototype.on = function(eventType, fn) {
if (!this.obj[eventType]) {
this.obj[eventType] = []
}
this.obj[eventType].push(fn)
}
Event.prototype.emit = function() {
var eventType = Array.prototype.shift.call(arguments)
var arr = this.obj[eventType]
for (let i = 0; i < arr.length; i++) {
arr[i].apply(arr[i], arguments)
}
}
var ev = new Event()
ev.on('click', function(a) { // 订阅函数
console.log(a) // 1
})
ev.emit('click', 1) // 发布函数
订阅函数逻辑一定要优先于发布函数吗
考虑以下场景:
$.ajax('', () => {
// 异步订阅函数逻辑
})
// 在其他地方执行发布函数, 此时并不能保证执行发布函数的时候, 订阅函数已经执行
我们需要实现这样的逻辑:
var ev = new Event()
ev.emit('click', 1)
ev.on('click', function(a) {
console.log(a) // 1
})
目标明确后, 来着手实现它:
var Event = function() {
this.obj = {}
this.cacheList = []
}
Event.prototype.on = function(eventType, fn) {
if (!this.obj[eventType]) {
this.obj[eventType] = []
}
this.obj[eventType].push(fn)
for (let i = 0; i < this.cacheList.length; i++) {
this.cacheList[i]()
}
}
Event.prototype.emit = function() {
const arg = arguments
const that = this
function cache() {
var eventType = Array.prototype.shift.call(arg)
var arr = that.obj[eventType]
for (let i = 0; i < arr.length; i++) {
arr[i].apply(arr[i], arg)
}
}
this.cacheList.push(cache)
}
以上代码实现思路就是把原本在 emit 里触发的函数存到 cacheList, 再转交到 on 中触发。从而实现了发布函数先于订阅函数执行。
观察者模式
观察者模式
应用场景:
- 场景一: 当观察的数据对象发生变化时, 自动调用相应函数。比如 vue 的双向绑定;
- 场景二: 每当调用对象里的某个方法时, 就会调用相应’访问’逻辑。比如给测试框架赋能的 spy 函数;
场景一: 双向绑定
Object.defineProperty
使用 Object.defineProperty(obj, props, descriptor)
实现观察者模式, 其也是 vue 双向绑定 的核心, 示例如下(当改变 obj 中的 value 的时候, 自动调用相应相关函数):
var obj = {
data: { list: [] },
}
Object.defineProperty(obj, 'list', {
get() {
return this.data['list']
},
set(val) {
console.log('值被更改了')
this.data['list'] = val
}
})
Proxy
Proxy/Reflect 是 ES6 引入的新特性, 也可以使用其完成观察者模式, 示例如下(效果同上):
var obj = {
value: 0
}
var proxy = new Proxy(obj, {
set: function(target, key, value, receiver) { // {value: 0} "value" 1 Proxy {value: 0}
console.log('调用相应函数')
Reflect.set(target, key, value, receiver)
}
})
proxy.value = 1 // 调用相应函数
场景二
下面来实现 sinon 框架的 spy 函数:
const sinon = {
analyze: {},
spy: function(obj, fnName) {
const that = this
const oldFn = Object.getOwnPropertyDescriptor(obj, fnName).value
Object.defineProperty(obj, fnName, {
value: function() {
oldFn()
if (that.analyze[fnName]) {
that.analyze[fnName].count = ++that.analyze[fnName].count
} else {
that.analyze[fnName] = {}
that.analyze[fnName].count = 1
}
console.log(`${fnName} 被调用了 ${that.analyze[fnName].count} 次`)
}
})
}
}
const obj = {
someFn: function() {
console.log('my name is someFn')
}
}
sinon.spy(obj, 'someFn')
obj.someFn()
// my name is someFn
// someFn 被调用了 1 次
obj.someFn()
// my name is someFn
// someFn 被调用了 2 次
vue
在 3.0 版本上使用 Proxy
重构的原因
首先罗列 Object.defineProperty()
的缺点:
Object.defineProperty()
不会监测到数组引用不变的操作(比如push/pop
等);Object.defineProperty()
只能监测到对象的属性的改变, 即如果有深度嵌套的对象则需要再次给之绑定Object.defineProperty()
;
关于 Proxy
的优点
- 可以劫持数组的改变;
defineProperty
是对属性的劫持,Proxy
是对对象的劫持;