常见JavaScript设计模式—原来这么简单个人文章
设计模式(Designpattern)是一套被反复使用、经过分类、代码设计经验的总结,简单来说设计模式就是为了解决软件设计领域不同场景下相应问题的解决方案.
SOLID实际上指的是五个基本原则,但在前端领域涉及到最多的是仍然是前面两条:
主要分为三个类型:
设计模式的核心是区分逻辑中的可变部分和不变部分,并使它们进行分离,从而达到使变化的部分易扩展、不变的部分稳定.
核心就是创建一个对象,这里的可变部分是参数,不变部分是共有属性.
实现方式一:
在JavaScript中没有这样的直接定义,不过根据上面的描述其实我们可以把它映射到typescript中的interface接口,理解到这其实让我联想到了vue.js中的自定义渲染器,预留的自定义渲染器的各个方法目的就是实现跨平台的渲染方式
其实,vuex中就使用到了单例模式,代码本身比较简单,当install方法被多次调用时,就会得到一个错误信息,并不会多次向Vue中混入vuex中自定义的内容:
这里举个封装localStorage方法的例子,并提供给外部对应的创建方法,如下:
letstorageInstance=null;classStorage{getItem(key){letvalue=localStorage.getItem(key);try{returnJSON.parse(value);}catch(error){returnvalue;}}setItem(key,value){try{localStorage.setItem(JSON.stringify(value));}catch(error){//dosomethingconsole.error(error);}}}//单例模式exportdefaultfunctioncreateStorage(){if(!storageInstance){storageInstance=newStorage();}returnstorageInstance;}原型模式在JavaScript中原型模式是很常见的,JavaScript中实现的继承或者叫委托也许更合适,因为它不等同于如Java等语言中的继承,毕竟JavaScript的继承是基于原型(prototype)来实现.
classPerson{say(){console.log(`hello,mynameis${this.name}!`);}eat(foodName){console.log(`eating${foodName}`);}}classStudentextendsPerson{constructor(name){super();this.name=name;}}constzs=newStudent('zs');constls=newStudent('ls');console.log(zs.say===ls.say);//Java中是不相等的,JavaScript中是相等的console.log(zs.eat===ls.eat);//Java中是不相等的,JavaScript中是相等的vue2中的原型模式文件位置:\src\core\instance\lifecycle.js
核心是在不改变原对象/方法的基础上,通过对其进行包装拓展,使原有对象/方法可以满足更复杂的需求.
装饰器模式本质上就是函数的传参和调用,通过函数为已有对象/方法进行扩展,而不用修改原对象/方法,满足开放封闭原则.
通过配置babel通过将test.js转为为bable_test.js用来查看装饰器的本质:
babel.config.json
{"presets":[["@babel/preset-env",{"targets":{"node":"current"}}]],"plugins":[["@babel/plugin-proposal-decorators",{"legacy":true}],["@babel/plugin-proposal-class-properties",{"loose":true}]]}test.js
//定义装饰器functiondecoratorTest(target){console.log(target);}//使用装饰器,装饰Person类@decoratorTestclassPerson{say(){}eat(){}}执行babeltest.js--out-filebabel_test.js命令是生成babel_test.js
"usestrict";var_class;functiondecoratorTest(target){console.log(target);}letPerson=decoratorTest(_class=classPerson{say(){}eat(){}})||_class;React中的装饰器模式——HOC高阶组件高阶组件是参数为组件,返回值为新组件的函数,在React中HOC通常用于复用组件公共逻辑.
//TodoList组件classTodoListextendsReact.Component{}//HOC函数functionWrapContainer(Comp){return(
);}//HOC装饰TodoList组件,为TodoList组件包裹红色边框constnewTodoList=WrapContainer(TodoList);适配器模式适配器模式本质就是让原本不兼容的功能能够生效,避免大规模修改代码,对外提供统一使用.
通过观察Axios的目录结构,很容就发现其使用了适配器模式:
代理模式顾名思义就是不能直接访问目标对象,需要通过代理器来实现访问,通常是为了提升性能、保证安全等.
事件代理是很常见的性能优化手段之一,react的事件机制也采用了事件代理的方式(篇幅有限可自行了解),这里演示简单的JavaScript事件代理:
thisnumberis1
thisnumberis2
thisnumberis3
thisnumberis4
thisnumberis5
Vue中的代理ProxyVue.js3中通过Proxy实现了对数据的代理,任何读取、设置的操作都会被代理对象的handlers拦截到,从而实现Vue中的track和trigger策略模式实际上就是定义一系列的算法,将单个功能封装起来,并且对扩展开放.
假如我们需要为某个游乐场的门票价格做差异化询价,主要人员类型分为儿童、成年人、老年人三种,其对应的门票折扣为8折、9折、8.5折
if-else代码一把梭
缺点:无论哪种人员类型的折扣变动,都需要修改finalPrice函数,不符合对对修改封闭
functionfinalPrice(type,price){if(type==="child"){//dootherthingreturnprice*0.8;}if(type==="adult"){//dootherthingreturnprice*0.9;}if(type==="aged"){//dootherthingreturnprice*0.85;}}单一功能封装缺点:若人员类型增加妇女类型,仍然需要修改finalPrice函数,且不符合对扩展开放
functionchildPrice(price){//dootherthingreturnprice*0.8;}functionadultPrice(price){//dootherthingreturnprice*0.9;}functionagedPrice(price){//dootherthingreturnprice*0.85;}functionfinalPrice(type,price){if(type==="child"){returnchildPrice(price);}if(type==="adult"){returnadultPrice(price);}if(type==="aged"){returnagedPrice(price);}}创建映射关系通过映射关系,很好的将finalPrice和具体的计算逻辑进行分离,在需要扩展类型时,只需要修改priceTypeMap对象而不用修改对外暴露的finalPrice函数.
constpriceTypeMap={child:function(price){//dootherthingreturnprice*0.8;},adult:function(price){//dootherthingreturnprice*0.9;},aged:function(price){//dootherthingreturnprice*0.85;},};functionfinalPrice(type,price){returnpriceTypeMap[type](price);}状态模式状态模式允许一个对象在其内部状态发生改变时,能够改变原本的行为.
假如现在我们需要设计一个售票机器,主要出售巴士、火车、飞机票等,价格分别为50、150、1000,并且能够根据剩余票数决定是否能够继续购买.
有了上面的策略模式的思想,立马就可以设计出如下的代码:
缺点:没有根据剩余票数决定是否可以继续售卖,主要原因就在于抽离的ticketTypeMap和TicketMachine之间的状态没有关联
constticketTypeMap={bus(){//dootherthingreturn50;},train(){//dootherthingreturn150;},plane(){//dootherthingreturn1000;},};classTicketMachine{constructor(){//剩余票数this.remain={bus:100,train:150,plane:200,};}selling(type){returnticketTypeMap[type]();}}关联对象状态—函数传参通过函数传参的方式将对象传递给目标函数,让目标函数通过该对象访问和修改对象内部的状态.
constticketTypeMap={bus(remain){if(remain.bus<=0)returnError("抱歉,巴士票已售完");remain.bus--;return50;},train(remain){if(remain.train<=0)returnError("抱歉,火车票已售完");remain.train--;return150;},plane(remain){if(remain.plane<=0)returnError("抱歉,飞机票已售完");remain.plane--;return1000;},};classTicketMachine{constructor(){//剩余票数this.remain={bus:100,train:150,plane:200,};}selling(type){returnticketTypeMap[type](this.remain);}}关联对象状态—整合方法实际上ticketTypeMap映射的方法和TicketMachine有较强的关联性,不应该单独存在,因此,可以将这个映射对象整合进TicketMachine当中
classTicketMachine{constructor(){//剩余票数this.remain={bus:100,train:150,plane:200,};}ticketTypeMap={that:this,bus(){const{remain}=this.that;if(remain.bus<=0)returnError("抱歉,巴士票已售完");remain.bus--;return50;},train(){const{remain}=this.that;if(remain.train<=0)returnError("抱歉,火车票已售完");remain.train--;return150;},plane(){const{remain}=this.that;if(remain.plane<=0)returnError("抱歉,飞机票已售完");remain.plane--;return1000;},};selling(type){returnthis.ticketTypeMap[type]();}}观察者模式观察者模式定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个目标对象,当这个目标对象的状态发生变化时,会通知所有观察者对象,使它们能够自动更新.
vue中的响应式原理就使用了观察者模式,我们简单回顾一下其工作流程:
以上过程中,显然Observe和Watcher就是被观察者和观察者,因为Observe中实现了对Watcher的收集和监听到数据状态发生变化时通知Watcher更新的处理,可以认为Dep只是Observe中使用到的一个存储和派发Watcher的工具.
发布订阅模式有三个核心:发布者、事件中心、订阅者,且发布订阅模式中的发布者和订阅者不能直接进行通信,必须要经过事件中心来统一调度.
实际上,发布订阅模式和观察者模式在概念上非常相似,做的事情也都一致,主要区别在于:
vue中的全局事件总线(EventBus)和node中的EventEmitter,甚至是浏览器中的事件注册(addEventListener)和执行,它们都属于发布订阅模式.
下面实现一个简单的发布订阅模式:
classEventEmitter{constructor(){this.handlers={};}on(name,handle){if(!this.handlers[name]){this.handlers[name]=[];}this.handlers[name].push(handle);}emit(name,...args){if(this.handlers[name]){this.handlers[name].forEach((handle)=>{handle(...args);});}}off(name,handle){if(this.handlers[name]){this.handlers[name]=this.handlers[name].filter((h)=>{if(handle)returnh!==handle;returnfalse;});}}once(name,handle){constonceHandle=(...args)=>{handle(...args);this.off(name,onceHandle);};this.on(name,onceHandle);}}迭代器模式迭代器模式是指提供一种方法顺序访问一个聚合对象中的各个元素,而又不需要暴露该对象的内部表示,核心目的就是遍历.
看起来很难有一种方法能够兼容以上几种数据结构的遍历方式,即不需要考虑数据结构本身就能实现遍历的目的,但我们可以基于ES6的Symbol.iterator实现自定义迭代器.
Symbol.iterator为每一个对象定义了默认的迭代器,拥有该迭代器后就可以被for...of循环使用.