相信大家在平时的开发中或多或少听说的听说过各种设计模式,有非常常见的,比如:观察者模式,工厂模式,装饰器模式等等,熟练掌握设计模式不仅仅对我们技术提升有巨大的帮助,而且更有利于编程思维的提升,而且如果我们能够熟练的将各种设计模式应用到实际的开发中,对代码的易读和维护都能够起到积极的作用。
Tips:在学习设计模式的时候,尽量分开去学,即先学习设计,然后再去学模式,这样的话,对于之后的理解更加容易一些,什么设计呢,其实就是指设计原则,而模式这里指的就是就是我们要讲的设计模式。现在共有5大设计原则,不管是哪种设计模式,都是遵循设计原则的。下面来分别介绍这5大设计原则。
1.单一职责原则
单一职责原则原则就是每个程序只负责做好一件事情,如果功能过于复杂就拆分开,每个部分保持独立。这个其实也符合我们当下流行框架Vue和React的组件化开发,把一个复杂的页面拆分成一些零散的组件,并且每个组件保持独立,同时也可在不同的页面当中实现复用。
2.开放封闭原则
开发封闭原则大白话的意思就是对扩展开放,对修改封闭。放到实际开发中如何去理解呢,我们日常的网站和App开发每周都有发不同的版本来增加需求,那么增加需求的时候,尽量要做到扩展新代码,而非修改已有代码,如果我们修改已有代码无疑增加了风险,因为本来原来的代码是没有问题的,加了新的代码之后必然会增加不可预知的风险,当然有的个别需求必须修改已有代码,这个另说。同时这个原则也是我们软件设计的终极目标。
3.李氏置换原则
子类能够覆盖父类,父类能出现的地方,子类就可以出现,这个原则其实在JAVA等语言当中是较为常见的,多用于继承,而JavaScript作为弱类型语言,继承使用其实是很少的,这里简单提一下。
4.接口独立原则
接口独立原则的含义是保持接口的单一独立,避免出现胖接口,JavaScript中是没有接口(typescript例外),使用较少, 它是有点类似于单一职责原则,这里更关注接口。
5.依赖倒置原则
依赖倒置原则的含义是面向接口编程,依赖于抽象而不依赖于具体,使用方只关注接口而不关注具体类的实现,同样这里也是JavaScript中使用较少(没有接口&弱类型)
设计模式分类
工厂模式(工厂方法模式、抽象工厂模式、建造者模式) 单例模式
下面我们就来一一结合开发当中的实例,来分析各个设计模式
概念
工厂模式是由一个方法来确定是要创建哪个类的实例,在前端当中最为常见的工厂模式就是new操作的单独封装,当遇到new操作的时候,就要考虑是否该使用工厂模式。这里也可以结合生活中的例子去思考。当你去购买汉堡,直接点餐取餐,不会自己亲手做,商店要“封装”做汉堡的工作,做好直接给买者。也就是说通过提供原材料,最终得到是汉堡还是炸鸡,是由你自己决定的。
前端中实例
1. jQuery当中的$('')
jQuery当中的$('div'),这里的$选择器就是已经封装好的API,这里我们直接使用即可。下面简单实现一个JQuery的$操作符,帮助大家加深理解。
class jQuery { constructor(selector) { let slice = Array.prototype.slice let dom = slice.call(document.querySelectorAll(selector)) let len = dom ? dom.length : 0 for(let i = 0;i < len; i++) { this[i] = dom[i] } this.length = len this.selector = selector || '' } append(node) { } html(data) { } //等等API } window.$ = function (selector) { return new jQuery(selector) }
2. Vue异步组件
这个大家应该比较熟悉,而且官方文档讲的也非常详细,这里直接饮用官方文档的案例,在大型应用中,我们可能需要将应用分割成小一些的代码块,并且只在需要的时候才从服务器加载一个模块。为了简化,Vue 允许你以一个工厂函数的方式定义你的组件,这个工厂函数会异步解析你的组件定义。Vue 只有在这个组件需要被渲染的时候才会触发该工厂函数,且会把结果缓存起来供未来重渲染。例如:
Vue.component('async-example', function (resolve, reject) { setTimeout(function () { // 向 `resolve` 回调传递组件定义 resolve({ template: '<div>I am async!</div>' }) }, 1000) })
如你所见,这个工厂函数会收到一个 resolve 回调,这个回调函数会在你从服务器得到组件定义的时候被调用。你也可以调用 reject(reason) 来表示加载失败。这里的 setTimeout 是为了演示用的,如何获取组件取决于你自己。一个推荐的做法是将异步组件和 webpack 的 code-splitting 功能一起配合使用:
Vue.component('async-webpack-example', function (resolve) { // 这个特殊的 `require` 语法将会告诉 webpack // 自动将你的构建代码切割成多个包,这些包 // 会通过 Ajax 请求加载 require(['./my-async-component'], resolve) })
你也可以在工厂函数中返回一个 Promise,所以把 webpack 2 和 ES2015 语法加在一起,我们可以写成这样:
Vue.component( 'async-webpack-example', // 这个 `import` 函数会返回一个 `Promise` 对象。 () => import('./my-async-component') )
当使用局部注册的时候,你也可以直接提供一个返回 Promise 的函数:
new Vue({ // ... components: { 'my-component': () => import('./my-async-component') } })
概念
单例模式符合单一职责原则,用大白话描述单例模式就是系统中被唯一使用,例如:电子商务网站常见的购物车和登录都是单例模式的运用。
前端中的实例
1.jQuery中的$('')
仍旧是jQuery当中的$(' ')选择器,整个jQuery框架当中有一个这样的选择器。
2.Redux和Vuex
不管是Redux还是Vuex,里面的状态store都是唯一的,Redux中的store只能通过Reducer去修改,而Vuex中的store只能通过Mutation修改,其余修改方式都是错误的。
概念
适配器模式的含义是旧接口格式和使用者不兼容,中间加一个适配器接口。生活当中随处可见符合适配器模式的例子,如:插头转换器,电脑接口转换器。
前端中的实例
封装旧的接口
这里我来列举一个例子,这里那我们常用的发起ajax请求为例,你自己封装的ajax,使用方式如下:
ajax({ url:'/getList', type:'Post', dataType:'json', data:{ id:"123" } }) .done(function(){})
但是这个时候你接到的项目当中都是:$.ajax({...}),这个时候我们只需要加一层适配器即可,代码如下:
let $ = {
ajax:function (options) {
return ajax(options);
}
}
Vue中的computed
Vue的计算属性相信大家在项目的开发当中都是经常会用到的一个特性,比如一个字符串我们想要他的翻转后的结果,那么这里就可以使用计算属性,计算属性这个特性本身用的设计模式就是适配器模式,代码如下:
<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"
装饰器模式
概念
装饰器模式,装饰我们可以理解为就是给一个东西装饰另外一些东西使其更好看,对应到前端中就是为对象添加新功能,并且不改变其原有的结构和功能,这种模式创建了一个装饰类,用来包装原有的类,并在保持类方法签名完整性的前提下,提供了额外的功能。在我们日常生活中用到的手机壳就是经典的例子
1.ES7中的装饰器
ES7装饰器的具体用法可以去找阮一峰老师的相关文章,其实参照其字面意思,就是对类、方法、属性进行修饰,从而进行一些相关功能的定制。那么JavaScript的Decorator在原理和功能上简单明了,简单来说就是对对象进行包装,返回一个新的对象描述,这里可以类比高阶组件。这里我们列举一个简单的例子,想必大家都玩过王者荣耀,英雄的战斗力都是随着装备和等级的增加越来越厉害,那么这里我们就以王者荣耀当中的英雄为例,示例场景是这样的:
然后按照这个场景来写具体的代码:
创建Hero类
class Hero { constructor(define = 10,attack = 20, blood = 20) { this.init(define,attack,blood) } init(define,attack,blood) { this.define = define; this.attack = attack; this.blood = blood; } toString() { return `防御力: ${this.define},攻击力:${this.attack},血量:${this.blood}` } } let houyi = new Hero(); console.log(`当前状态 ===> ${houyi}`) //输出:当前状态 ===> 防御力:10,攻击力20,血量20
创建decorateCloth方法,为英雄增加布甲装备。
function decorateCloth(target,key,descriptor) { const method= descriptor.value; let moreDef = 100; let ret; descriptor.value = (...args) => { args[0] += moreDef; ret = method.apply(target,args); return ret; } return descriptor; } class Hero { constructor(define = 10,attack = 20, blood = 20) { this.init(define,attack,blood) } @decorateCloth init(define,attack,blood) { this.define = define; this.attack = attack; this.blood = blood; } toString() { return `防御力: ${this.define},攻击力:${this.attack},血量:${this.blood}` } } let houyi = new Hero(); console.log(`当前状态 ===> ${houyi}`) //输出:当前状态 ===> 防御力:110,攻击力20,血量20
可以看到输出结果防御力确实增加了,布甲确实起到了作用。
从上面的代码可以看出,如果有的时候我们并不需要关心函数的内部实现,仅仅是想调用它的话,装饰器能够带来比较好的可读性,使用起来也是非常的方便。
概念
代理模式是使用者无权访问目标对象,中间加代理,通过代理做授权和控制,我们经常会用到这个模式,不管实际的开发也好,还是网络部署当中,都能够看到它的身影。如:科学上网 谷歌搜索
前端中的实例
1.网页中的事件代理
其实网页的事件代理也是非常常考的面试题之一,其实就是把元素绑定到父元素上面,而不是对其下面的每一个子元素都进行相应的绑定,下面举一个具体的实例:
<div id="item"> <a href="#">a1</a> <a href="#">a2</a> <a href="#">a3</a> <a href="#">a4</a> <div> <button>点击增加一个a标签</button> <script> let div1 = document.getElementById('div1') div1.addEventListener('click',function(e) { let target = e.target if(e.nodeName === 'A') { alert(target.innerHTML) } }) </script>
2.jQuery中的$proxy
比如我们经常会遇到这样一种情况,如下代码所示
$('#div1').click(function() { $(this).addClass('red') }) $('#div1').click(function() { setTimeout(function () { // this 不符合期望 $(this).addClass('red'); },1000); })
解决的方法可能有的同学已经想到是先将this赋值给一个变量。
$('#div1').click(function() { setTimeout(function () { let _this = this; // this 不符合期望 $(_this).addClass('red'); },1000); })
是的这种方法是对的,但是这样就会增加一个变量,所以这里用$proxy解决更好,代码如下:
$('#div1').click(function() { setTimeout($proxy(function () { $(this).addClass('red'); }),1000); })
概念
观察者模式就是只要你作为订阅者订阅了某个事件,当事件触发的时候,发布者就会通知你。这里可以类比我们去咖啡厅点咖啡,当我们点了咖啡之后,就可以去做别的事情,当咖啡完成时,服务员就会叫你来取,你到时候取走咖啡即可。
前端中的实例
1.网页中的事件绑定
<button id="btn1">btn</button> <scirpt> $('#btn1').click(function() { console.log(1) }) </script>
这里我们订阅了btn1的click事件,当几点btn1所在的元素时,click function就会触发。
2.Node.js的自定义事件EventEmitter
const EventEmitter = require('events').EventEmitter const emitter1 = new EventEmitter(); emitter1.on('some',()=> { //监听some事件 console.log('some event is occured 1') }) emitter1.on('some',()=> { //监听some事件 console.log('some event is occured 2') }) emitter.emit('some')
概念
提到状态模式就不得不提到有限状态机,阮一峰总结过有限状态机的三个特征:
这里充分说明了状态模式的重要性。一个对象有状态变化,每次状态变化都会触发一个逻辑,不能总是用if else来控制,更要学会用状态来进行控制。
前端中的实例
1.ES6中的promise
promise是ES6新增的一个特性,它是异步编程的一种解决方案,比传统的解决方案更加方便,可以使用.then()操作避免了回调嵌套带来的回调地狱式的写法,那么下面就来简单实现一个promise。
let fsm = new statemachine({ init:'pending', transitions:[ { name:'resolve', from:'pending', to:'fullfilled' }, { name:'reject', from:'pending', to:'rejected' } ], methods:{ onResolve:function(state,data) { data.successList.forEach(fn => fn()) } onReject:function(state,data) { data.failList.forEach(fn => fn()) } } })
这就是一个简单的promise。一个promise只有三种状态,所以promise可以说是状态模式的经典案例。
总结
这篇文章我们主要介绍了5大设计原则和7个设计模式,这7个设计模式是前端使用频率较高的设计模式,当然还有很多其他的设计模式,由于篇幅原因这里无法全部介绍,