设计模式介绍
设计模式是开发的过程中,遇到一些问题时的解决方案,这些方案是通过大量试验与踩坑总结出来的最佳实践,主要指的是一些代码思想与方法论,而不是指现场的代码。本文主要总结前端开发中时常遇到的一些设计模式。
设计模式的五大原则
- 单一职责 一个程序只做一件事,如果功能过于复杂就拆分开,每个部分保持独立
- 开发封闭 对扩展开放,对修改封闭 增加需求时,扩展新代码,而非修改旧代码
- 李氏置换 子类能覆盖父类(继承)
- 接口独立 类似于单一职责,关注于接口 保存接口的单一独立
- 依赖倒置 只关注接口而不关注具体类的实现
简单的举个栗子🌰
function loadImg (src) {
let p = new Promise(function (resolve, reject) {
let img = document.createElement('img')
img.onload = function () {
resolve(img)
}
img.onerror = function () {
reject('加载失败')
}
img.src = src;
})
return p
}
let src = 'https://sf1-ttcdn-tos.pstatp.com/img/user-avatar/311a30cc2e1d1dc418b7aec40de4c6fb~300x300.image'
let result = loadImg(src)
result.then((img) => {
// part1
console.log(img.width)
return img
// 增加需求,增加then
}).then((img) => {
// part2
console.log(img.height)
}).then(() => {
// ...
}).catch((err) => {
console.log(err)
})
以上通过一个简单的Promise示范了设计模式中的单一职责原则和开发封闭原则,一个then中只做一件事情;如果有多个需求,增加多个then,每个then里保持独立。
如何学习设计模式?
问题抛出
身边很多前端小伙伴都存在一个疑惑:设计模式需要学习吗?需要去死记硬背吗?怎么样才算是学会了呢?是不是在日常的开发中每写一个方法都要去使用设计模式呢?
学习方法
其实,设计模式强调的是思想多于编码,我们通过阅读各种框架的源码,会发现里面大量使用设计模式进行开发,是由于开发人员已经养成一种习惯,自然而然的就会使用到代码中,所以形成思想是至关重要的。对此,就如何学习设计模式这个问题,我提出自己的一些看法
- 明白每个设计的道理和用意
- 通过每个设计模式的经典示例应用体会它的真正使用场景
- 自己编码时多思考,可以尽量模仿
- 好记性不如烂笔头
- 祈祷奇迹不如无尽的练习
前端常见的设计模式
工厂模式
简介
工厂模式主要是为创建实例提供了接口,将new操作进行单独封装
举个例子:
小明去华莱士买汉堡,直接点餐、取餐,不用自己亲手做
而华莱士要封装做汉堡的操作,做好直接卖给小明
示例
class Hamburger {
constructor(name) {
this.name = name
}
init () {
console.log('汉堡初始化')
}
}
// 华莱士相当于一个工厂,封装了返回汉堡的示例的操作
class Hls {
create (name) {
return new Hamburger(name)
}
}
// 新建一个华莱士
let hls = new Hls()
// 通过create方法返回一个示例
let hamburger1 = hls.create('香辣鸡腿堡');
hamburger1.init()
场景
jQuery
在jQuery中,我们去获取一个元素,是通过方法实现的形式就是使用了工厂模式
代码演示
class jQuery {
constructor(selector) {
// ...
}
append() {}
addClass() {}
// ...
}
window.$ = function (selector) {
return new jQuery(selector)
}
React中的createElement
通过返回VNode的示例来创建节点
代码演示
class VNode {}
React.createElement = function() {
return new VNode()
}
单例模式
简介
系统中被唯一使用的
一个类只有一个示例
举个例子:
系统中的登录框,电商平台的购物车
示例
class SingleObject {
constructor() {
this.isLogin = false
}
login () {
this.isLogin = true
}
}
SingleObject.getInstance = (function () {
let instance
// 利用闭包把外部函数的变量保存在内存中
return function () {
// 如果不存在实例,新建一个实例
if (!instance) {
instance = new SingleObject()
}
// 如果存在,则直接返回
return instance
}
})()
const a = SingleObject.getInstance()
const b = SingleObject.getInstance()
// a进行登录操作
a.login();
// 单例模式,无论创建多少个实例,都是一模一样的
console.log(a === b) // true
// 由于a已登录,b和a一样,b也已登录
console.log(b.isLogin) // true
场景
Vuex的实现
通过传入Vue的实例对象,如果已经传入过,提示已传入实例,否则再将vuex初始化的逻辑写进vue的钩子函数里
代码演示
vue-router的实现
跟vuex的实现方法大同小异,此处不过多赘述,感兴趣的同学可以查阅vue-router创建时的源码
适配器模式
简介
本来的不适合使用的方法,转成适合的
举个例子:
笔记本电脑通常没有过多插槽,这时需要使用一个转接口进行插槽的扩展
示例
class Iphone {
getName() {
return '我是iphone插头'
}
}
class Target {
constructor() {
this.t = new Iphone()
}
getName() {
return `${this.t.getName()},已转接成andorid插头`
}
}
const target = new Target()
console.log(target.getName()) // 我是iphone插头,已转接成andorid插头
场景
vue中的computed
vue中有时data中的数据不满足于某个需求的使用,通常可以通过计算属性computed重新组装一个新的数据
代码演示
<div id="example">
<p>Original message: "{{ message }}"</p>
<p>Computed reversed message: "{{ reversedMessage }}"</p>
</div>
var vm = new Vue({
el: '#example',
data: {
message: 'Hello'
},
computed: {
// 计算属性的 getter
reversedMessage: function () {
// `this` 指向 vm 实例
return this.message.split('').reverse().join('')
}
}
})
结果:
Original message: "Hello"
Computed reversed message: "olleH"
装饰器模式
(与上方适配器模式不同,上方是完全不能用,这个是增加功能)
简介
为对象添加新功能
不改变原有的结构和功能
将现有对象和装饰器进行分离,两者独立存在
举个例子:
手机壳(拓展手机防摔功能)
示例
class Circle {
draw () {
console.log('画圆')
}
}
class Decorator {
// 传入circle实例
constructor(circle) {
this.circle = circle
}
setBorder () {
console.log('设置边框')
}
draw () {
this.circle.draw()
// 画圆之后设置边框
this.setBorder()
}
}
let c = new Circle()
let decorator = new Decorator(c)
decorator.draw()
场景
ES7装饰器
装饰器可以用来装饰整个类
附上阮一峰老师关于装饰器的文档:es6.ruanyifeng.com/#docs/decor…
代码演示
// @testable是一个装饰器,修改了`MyTestableClass`这个类的行为
@testable
class MyTestableClass {
// ...
}
function testable(target) {
// 为它加上了静态属性`isTestable`
target.isTestable = true;
}
MyTestableClass.isTestable // true
vue-class-component
以class的模式写vue组件
代码演示
代理模式
简介
使用者无权访问目标对象
中间加代理,通过代理做授权控制
举个例子:
公司内网家里访问需要vpn
明星的经纪人
示例
这里通过简单演示vue3响应式的原理进行演示
附上阮一峰老师关于Proxy的文档:es6.ruanyifeng.com/#docs/proxy
const data = {
name: 'a',
age: 18,
likes: []
}
// 为data创建一个代理
const proxyData = new Proxy(data, {
get(target, key, receiver) {
const result = Reflect.get(target, key, receiver)
console.log('get')
return result
},
set(target, key, value, receiver) {
const result = Reflect.set(target, key, value, receiver)
console.log('set')
return result
}
})
// 通过改变proxyData而不是data,进行代理
proxyData.name = '2'
proxyData.likes.push('eat')
// 打印data
console.log(data) // 'set' 'get' { age: 18, likes: ["eat"], name: "2" }
场景
事件代理(委托)
下面奉上一道经典面试题,如何点击li弹出对应的内容
代码演示
<ul id="ul">
<li>111</li>
<li>222</li>
<li>333</li>
<li>444</li>
</ul>
window.onload = function(){
var oUl = document.getElementById("ul");
// 通过事件冒泡,直接为父组件绑定事件
oUl.onclick = function(ev){
var ev = ev || window.event;
var target = ev.target || ev.srcElement;
if (target.nodeName.toLowerCase() == 'li'){
alert(target.innerHTML);
}
}
}
观察者模式
简介
发布订阅
一对多
举个例子:
去店里点奶茶,点好了被告知取餐
示例
// 主题类
class Subject {
constructor() {
// 设置一个state和观察者数组
this.state = 0
this.observers = []
}
getState() {
return this.state
}
setState(state) {
this.state = state
// 赋值的时候调用通知的方法
this.notify()
}
notify() {
// 通知各个观察者更新
this.observers.forEach(observer => {
observer.update()
})
}
// 传入观察者实例,绑定观察者
attach(observer) {
this.observers.push(observer)
}
}
// 观察者类
class Observer {
constructor(name, subject) {
this.name = name
// 传入主题实例
this.subject = subject
// 在此观察者实例上传入主题
this.subject.attach(this)
}
update() {
console.log(`${this.name} update, state:${this.subject.getState()}`)
}
}
const s = new Subject()
// 一对多 绑定多个观察者
const o1 = new Observer('o1', s)
const o2 = new Observer('o2', s)
s.setState(1234) // "o1 update, state:1234" "o2 update, state:1234"
场景
vue2响应式defineProperty
通过遍历data,给每个属性绑定观察者
代码演示
function update() {
console.log('更新啦')
}
const newPrototype = Array.prototype
const arrProto = Object.create(newPrototype)
// 由于Object.defineProperty不能监听数组,此处重写数组的方法
// 此处只演示数组的push方法,其他方法的实现与此一致
arrProto.push = function() {
update()
newPrototype.push.call(this, ...arguments)
}
function watcherFn(obj) {
// 如果是数组,重写obj的原型指向arrProto
if (Array.isArray(obj)) {
obj.__proto__ = arrProto
} else {
for (let k of Object.keys(obj)) {
register(obj, k, obj[k])
}
}
}
function register(obj, key, value) {
if (typeof value === 'object') {
watcherFn(value)
} else {
Object.defineProperty(obj, key, {
get() {
return value
},
set(val) {
if (val !== value) {
update()
value = val
}
}
})
}
}
const obj = {
name: 'h',
info: {
address: 'bj'
},
likes: ['music']
}
watcherFn(obj)
obj.name = 'b'
obj.info.address = 'gz'
obj.likes.push('sing')
console.log(obj) // 更新啦 × 3
node自定义事件 EventEmitter
代码演示
总结
除了以上这六种设计模式外,还有许多其他的设计模式,由于本人是前端开发,在日常工作中遇到的场景大多是这几种设计模式,当然还有状态模式、策略模式等等。
其实学好设计模式,不但能拓展我们编码的思维,还可以在日常的开发中,编写出简洁的、目的明确的、层次分明的代码,更重要的是,我们可以在面试中不时的透露出对设计模式的熟悉与运用,增加面试官对你的好感,最重要的肯定就是为了日后的升职加薪啦,还是那句话:祈祷奇迹不如无尽的练习。希望同学们都可以多模仿,多看源码,减少臭气味的代码,编写出更高质量的代码!