设计模式(design pattern)本质上是把经常用到的代码套路,归纳总结后系统的表达出来。
学习设计模式好处有以下几点:
网络上流行的”23种设计模式”是静态语言在生产过程中的经验总结,由于语言的特性,其中有些设计模式在 Javascript代码的编写过程中,有的可能应用场景很少,有的语言本身的特性就已经实现。因此,这基于 Javascript 这门语言的特性和应用场景,针对性的进行学习。
工厂模式(Factory)是用来创建对象的一种最常用的设计模式。我们不暴露创建对象的具体逻辑,而是将逻辑封装在一个函数中,那么这个函数就可以被视为一个工厂。
function createUser(role) {
function User(options) {
this.name = options.name
this.viewPage = options.viewPage
}
switch(role){
case 'superAdmin':
return new User({name:'超级管理员',viewPage:['首页','通讯录','发现页','应用数据','权限管理']});
break;
case 'admin':
return new User({name:'管理员',viewPage:['首页','通讯录','发现页','应用数据']});
break;
case 'user':
return new User({name:'普通用户',viewPage:['首页','通讯录','发现页']});
break;
default:
throw new Error('参数错误')
}
}
createUser('admin')
上面的代码就是一种最常见的工厂模式。
如果要把多个构造函数生成实例的逻辑封装到某个工厂中,也可以将构造函数挂载到工厂函数的原型链上,或者工厂函数的静态方法中:
function createUser(role) {
return new this[role]
}
createUser.prototype.superAdmin = function () {
this.name = '超级管理员'
this.viewPage = ['首页','通讯录','发现页','应用数据','权限管理']
}
createUser.prototype.admin = function () {
this.name = '管理员'
this.viewPage = ['首页','通讯录','发现页','应用数据']
}
createUser.prototype.user = function () {
this.name = '普通用户'
this.viewPage = ['首页','通讯录','发现页']
}
new createUser('admin')
以下几种情景下,开发者应该考虑使用工厂模式:
示例:
Vue源码
单例模式(Singleton)思想在于保证一个特定类最多只能有一个实例,意味着当你第二次使用同一个类创建信对象时,应得到和第一次创建对象完全相同。
在 JS 中实现单例模式的通常思路是:将已经生成的对象通过闭包进行维护,下次再次生成新对象时,就直接返回老对象。
let SingleUser1 = (function () {
let instance = null
return function User() {
if(instance){
return instance
}
return instance = this
}
})()
将构造函数User和判断实例是否存在的逻辑解耦:
let SingleUser2 = (function () {
let instance = null
function User(name) {
this.name = name
}
return function (name) {
if(instance){
return instance
}
return instance = new User(name)
}
})()
对将一个构造函数单例化的逻辑可以进一步封装:
let singleton = function (fn) {
let instance = null
return function (args) {
if(instance){
return instance
}
return instance = new fn(args)
}
}
凡是使用唯一对象的场景,都适用于单例模式,例如登录框,弹窗,遮罩。另外ES6和CommonJS模块化语法中导出的对象也是单例
export default new Vuex.Store({/**/})
示例:
日常工作中,我们经常需要实现一个遮罩层,来防止用户中断页面操作。所谓的遮罩层,就是一个大小跟窗口一致的半透明div层。我们要求页面最多只能存在一个遮罩层,此时就非常适合使用单例模式:
//例子:生成遮罩
let createMask = singleton( function(){
let mask = document.createElement('div')
mask.style.background = 'red'
mask.style.width = '100%'
mask.style.height = '100%'
mask.style.position = 'fixed'
document.body.appendChild( mask );
return mask
})
适配器模式(Adapter)是将一个类(对象)的接口(方法或属性)转化成用户希望的另外一个接口(方法或属性),适配器模式使得原本由于接口不兼容而不能一起工作的那些类(对象)可以正常工作。正如适配器模式的定义,开发中凡是需要对接口的提供者和消费者进行兼容时,适配器模式就可以派上用场,最常见的就是需要对旧代码的渐进式地改造,或者是对某些已有的老接口进行兼容。
示例:
对旧的ajax方法进行迁移改造,由于历史原因,无法一次性移除所有的旧代码,因此需要使用适配模式对原代码进行兼容:
$ = {
ajax(options) {
let {method, url} = options
let axiosOptions = {method, url}
let dataProp = method === 'get' ? 'params' : 'data'
axiosOptions[dataProp] = options.data
return axios(axiosOptions).then((res) => {
options.success && options.success(res.data,res.status,res.request)
}).catch((err) => {
options.error && options.error(err)
})
}
}
装饰器模式(Decorator)是指允许向一个现有的对象添加新的功能,同时又不改变其结构。使得多个不同类或者对象之间共享或者扩展一些方法或者行为的时候,更加优雅。
装饰器模式在生活中也可以很容易找到相关的例子:例如手机壳,他并没有改变我们手机原有的功能,比如打电话,听音乐什么的。但却为手机提供了新的功能:防磨防摔等。
以下是装饰器模式在js中实现的简单实现:
function Phone() {
}
Phone.prototype.makeCall = function () {
console.log('拨通电话');
}
function decorate(target) {
target.prototype.code = function () {
console.log('写代码');
}
return target
}
Phone = decorate(Phone)
const phone = new Phone()
上例,通过decorate
函数来装饰构造函数Phone
,使得Phone
的实例既可以打电话,也可以写代码。如此,我们将功能单独抽离出来,依次得到复用,例如再次使用decorate函数去装饰构造函数Pad
等等
示例:
许多语言中包含装饰器语法来让编码者更方便地实现装饰器模式,例如Python
等。ES7
的语言标准中提出了装饰器语法,但一直处于Stage-2状态,没有正式通过。目前Node环境与所有浏览器都尚未支持装饰器语法,如果想使用装饰器语法,可以通过babel来转译:
安装babel:
npm install -D @babel/core babel-cli babel-preset-es2015 --registry https://registry.npm.taobao.org
安装babel插件来识别装饰器语法:
npm install -D babel-plugin-transform-decorators-legacy --registry https://registry.npm.taobao.org
新建Babel配置文件.babelrc
:
{
"presets":["es2015"],
"plugins":["transform-decorators-legacy"]
}
使用装饰器语法改造上面的例子:
function code (target) {
target.prototype.code = function () {
console.log('写代码');
}
}
@code
class Phone {
makeCall () {
console.log('打电话');
}
}
const phone = new Phone();
运行 npm script: babel index.js -o bulid.js
装饰器不光可以装饰类,还可以装饰方法
class Math {
@log
add(a, b) {
return a + b;
}
}
function log(target, name, descriptor) {
// 此时target是 Math.prototype , name是方法名,即'add'
// descriptor对象原来的值如下
// {
// value: specifiedFunction,
// enumerable: false,
// configurable: true,
// writable: true
// };
var oldValue = descriptor.value;
descriptor.value = function() {
console.log(`Calling ${name} with`, arguments);
// 注意:如果不调用或者不返回oldValue,则最终不会执行原方法
return oldValue.apply(this, arguments);
};
return descriptor;
}
const math = new Math();
//现在调用add方法,则会触发log功能
math.add(2, 4);
npm包core-decorators
将常用的装饰器工具进行了封装
安装:
npm install core-decorators -D --registry https://registry.npm.taobao.org
使用:
import { readonly,autobind, deprecate} from 'core-decorators';
class Phone {
@autobind
makeCall () {
console.log(this);
console.log('打电话');
}
}
const phone = new Phone();
window.phone = phone
以上代码使用了ES6模块化语法,若想在浏览器环境中运行,请使用webpack编译后引入
npm install webpack webpack-cli -D --registry https://registry.npm.taobao.org
npm script:
babel index3.js -o build.js && webpack build.js
合理利用装饰器可以极大的提高开发效率,对一些非逻辑相关的代码进行封装提炼能够帮助我们快速完成重复性的工作,节省时间。例如React中的高阶组件以及使用TypeScript开发Vue等等。
同样的,滥用装饰器也会使代码本身逻辑变得扑朔迷离,如果确定一段代码不会在其他地方用到,或者一个函数的核心逻辑就是这些代码,那么就没有必要将它取出来作为一个装饰器来存在。
代理模式(Proxy)为对象提供另一个代理对象以控制对这个对象的访问。
使用代理的原因是我们不想对原对象进行直接操作,而是通过一个“中间人”来传达操作。生活中有许多代理的例子,比如访问某个网站,不想直接访问,通过中间的一台服务器来转发请求,这台服务器就是代理服务器。又比如明星,普通人无法直接联系他们,而是通过经纪人进行联系。
使用ES6的Proxy语法实现对代理模式的简单实现:
let star = {
name:'zs',
age:21,
height:170,
bottomPrice:100000,
announcements:[],
}
let proxy = new Proxy(star,{
get:function (target,key) {
if(key === 'height'){
return target.height + 10
}else if(key === 'announcements'){
return new Proxy(target.announcements,{
set:function (target,key,value) {
if(key !== 'length' && target.length === 3){
console.log('不好意思,今年通告满了')
return true
}
target[key] = value
return true
}
})
}else{
return target[key]
}
},
set:function (target, key, value,) {
if(key === 'price'){
if(value > target.bottomPrice * 1.5){
console.log('成交');
target.price = value
}else if(value > target.bottomPrice){
console.log('咱们再商量商量');
}else {
throw new Error('下次说吧')
}
}
}
})
proxy.announcements.push('爸爸去哪儿')
proxy.announcements.push('中国好声音')
proxy.announcements.push('奇葩说')
proxy.announcements.push('快乐大本营')
proxy.price = 160000
proxy.price = 120000
proxy.price = 9000
代理模式能将代理对象与被调用对象分离,降低了系统的耦合度。代理模式在客户端和目标对象之间起到一个中介作用,这样可以起到保护目标对象的作用。代理对象也可以对目标对象调用之前进行其他操作。
示例:dom事件代理 。Vue源码。
注意区分适配器模式(Adapter),装饰器模式(Decorator),代理模式(Proxy):
适配器模式提供不同的新接口,通常用作接口转换的兼容处理
代理模式提供一模一样的新接口,对行为进行拦截
装饰器模式,直接访问原接口,直接对原接口进行功能上的增强
外观模式(Facade),是指为一组复杂的子系统接口提供一个更高级的统一接口,通过这个接口使得对子系统接口的访问更容易。
以下是外观模式在JS中的简单实现:
function addEvent(dom, type, fn) {
if (dom.addEventListener) { // 支持DOM2级事件处理方法的浏览器
dom.addEventListener(type, fn, false)
} else if (dom.attachEvent) { // 不支持DOM2级但支持attachEvent
dom.attachEvent('on' + type, fn)
} else {
dom['on' + type] = fn // 都不支持的浏览器
}
}
const myInput = document.getElementById('myinput')
addEvent(myInput, 'click', function() {console.log('绑定 click 事件')})
外观模式核心在于对其他接口的封装,是一种开发中非常常见的设计模式,框架或者库中的工具函数遵循的模式就是外观模式。
注意区分工厂模式和外观模式:
工厂模式核心是对创建对象的逻辑进行封装。
外观模式核心是对不同的接口进行封装。