程序猿的本职工做就是写代码,写出高质量的代码应该是咱们的追求和对本身的要求,由于:javascript
- 高质量的代码每每意味着更少的BUG,更好的模块化,是咱们扩展性,复用性的基础
- 高质量的代码也意味着更好的书写,更好的命名,有利于咱们的维护
怎样来定义代码质量的"好",业界有不少标准,本文认为好的代码应该有如下特色:前端
- 代码整洁,好比缩进之类的,如今有不少工具能够自动解决这个问题,好比eslint。
- 结构规整,没有漫长的结构,函数拆分合理,不会来一个几千行的函数,也不会有几十个
if...else
。这要求写代码的人有一些优化的经验,本文会介绍几种模式来优化这些状况。- 阅读起来好理解,不会出现一堆
a,b,c
这种命名,而是应该尽可能语义化,变量名和函数名都尽可能有意义,最好是代码即注释,让别人看你的代码就知道你在干吗。
本文介绍的设计模式主要有策略/状态模式
,外观模式
,迭代器模式
,备忘录模式
。java
假如咱们须要作一个计算器,须要支持加减乘除,为了判断用户具体须要进行哪一个操做,咱们须要4个if...else
来进行判断,若是支持更多操做,那if...else
会更长,不利于阅读,看着也不优雅。因此咱们能够用策略模式优化以下:ios
function calculator(type, a, b) { const strategy = { add: function(a, b) { return a + b; }, minus: function(a, b) { return a - b; }, division: function(a, b) { return a / b; }, times: function(a, b) { return a * b; } } return strategy[type](a, b); } // 使用时 calculator('add', 1, 1);
上述代码咱们用一个对象取代了多个if...else
,咱们须要的操做都对应这个对象里面的一个属性,这个属性名字对应咱们传入的type
,咱们直接用这个属性名字就能够获取对应的操做。git
状态模式和策略模式很像,也是有一个对象存储一些策略,可是还有一个变量来存储当前的状态,咱们根据当前状态来获取具体的操做:github
function stateFactor(state) { const stateObj = { status: '', state: { state1: function(){}, state2: function(){}, }, run: function() { return this.state[this.status]; } } stateObj.status = state; return stateObj; } // 使用时 stateFactor('state1').run();
if...else
实际上是根据不一样的条件来改变代码的行为,而策略模式和状态模式均可以根据传入的策略或者状态的不一样来改变行为,全部咱们能够用这两种模式来替代if...else
。axios
这个例子的需求是咱们的页面须要根据不一样的角色来渲染不一样的内容,若是咱们用if...else
写就是这样:设计模式
// 有三个模块须要显示,不一样角色看到的模块应该不一样 function showPart1() {} function showPart2() {} function showPart3() {} // 获取当前用户的角色,而后决定显示哪些部分 axios.get('xxx').then((role) => { if(role === 'boss'){ showPart1(); showPart2(); showPart3(); } else if(role === 'manager') { showPart1(); showPart2(); } else if(role === 'staff') { showPart3(); } });
上述代码中咱们经过API请求得到了当前用户的角色,而后一堆if...else
去判断应该显示哪些模块,若是角色不少,这里的if...else
就可能很长,咱们能够尝试用状态模式优化下:数组
// 先把各类角色都包装到一个ShowController类里面 function ShowController() { this.role = ''; this.roleMap = { boss: function() { showPart1(); showPart2(); showPart3(); }, manager: function() { showPart1(); showPart2(); }, staff: function() { showPart3(); } } } // ShowController上添加一个实例方法show,用来根据角色展现不一样的内容 ShowController.prototype.show = function() { axios.get('xxx').then((role) => { this.role = role; this.roleMap[this.role](); }); } // 使用时 new ShowController().show();
上述代码咱们经过一个状态模式改写了访问权限模块,去掉了if...else
,并且不一样角色的展现都封装到了roleMap
里面,后面要增长或者减小都会方便不少。缓存
这个例子的需求是咱们如今有一个小球,咱们须要控制他移动,他移动的方向能够是上下左右,还能够是左上,右下之类的复合运动。若是咱们也用if...else
来写,这头都会写大:
// 先来四个方向的基本运动 function moveUp() {} function moveDown() {} function moveLeft() {} function moveRight() {} // 具体移动的方法,能够接收一个或两个参数,一个就是基本操做,两个参数就是左上,右下这类操做 function move(...args) { if(args.length === 1) { if(args[0] === 'up') { moveUp(); } else if(args[0] === 'down') { moveDown(); } else if(args[0] === 'left') { moveLeft(); } else if(args[0] === 'right') { moveRight(); } } else { if(args[0] === 'left' && args[1] === 'up') { moveLeft(); moveUp(); } else if(args[0] === 'right' && args[1] === 'down') { moveRight(); moveDown(); } // 后面还有不少if... } }
能够看到这里if...else
看得咱们头都大了,仍是用策略模式来优化下吧:
// 建一个移动控制类 function MoveController() { this.status = []; this.moveHanders = { // 写上每一个指令对应的方法 up: moveUp, dowm: moveDown, left: moveLeft, right: moveRight } } // MoveController添加一个实例方法来触发运动 MoveController.prototype.run = function(...args) { this.status = args; this.status.forEach((move) => { this.moveHanders[move](); }); } // 使用时 new MoveController().run('left', 'up')
上述代码咱们也是将全部的策略都封装到了moveHanders
里面,而后经过实例方法run
传入的方法来执行具体的策略。
当咱们设计一个模块时,里面的方法能够会设计得比较细,可是暴露给外面使用的时候,不必定非得直接暴露这些细小的接口,外部使用者须要的多是组合部分接口来实现某个功能,咱们暴露的时候其实就能够将这个组织好。这就像餐厅里面的菜单,有不少菜,用户能够一个一个菜去点,也能够直接点一个套餐,外观模式提供的就相似于这样一个组织好的套餐:
function model1() {} function model2() {} // 能够提供一个更高阶的接口,组合好了model1和model2给外部使用 function use() { model2(model1()); }
外观模式提及来其实很是常见,不少模块内部都很复杂,可是对外的接口可能都是一两个,咱们无需知道复杂的内部细节,只须要调用统一的高级接口就行,好比下面的选项卡模块:
// 一个选项卡类,他内部可能有多个子模块 function Tab() {} Tab.prototype.renderHTML = function() {} // 渲染页面的子模块 Tab.prototype.bindEvent = function() {} // 绑定事件的子模块 Tab.prototype.loadCss = function() {} // 加载样式的子模块 // 对外不须要暴露上面那些具体的子模块,只须要一个高级接口就行 Tab.prototype.init = function(config) { this.loadCss(); this.renderHTML(); this.bindEvent(); }
上述代码这种封装模式很是常见,其实也是用到了外观模式,他固然也能够暴露具体的renderHTML
,bindEvent
,loadCss
这些子模块,可是外部使用者可能并不关心这些细节,只须要给一个统一的高级接口就行,就至关于改变了外观暴露出来,因此叫外观模式
。
这个例子也很常见,就是把一些相似的功能封装成一个方法,而不是每一个地方去写一遍。在之前仍是IE主导天下的时候,咱们须要作不少兼容的工做,仅仅是一个绑定事件就有addEventListener
,attachEvent
,onclick
等,为了不每次都进行这些检测,咱们能够将他们封装成一个方法:
function addEvent(dom, type, fn) { if(dom.addEventListener) { return dom.addEventListener(type, fn, false); } else if(dom.attachEvent) { return dom.attachEvent("on" + type, fn); } else { dom["on" + type] = fn; } }
而后将addEvent
暴露出去给外面使用,其实咱们在实际编码时常常这样封装方法,只是咱们本身可能没意识到这个是外观模式。
迭代器模式模式在JS里面很常见了,数组自带的forEach
就是迭代器模式的一个应用,咱们也能够实现一个相似的功能:
function Iterator(items) { this.items = items; } Iterator.prototype.dealEach = function(fn) { for(let i = 0; i < this.items.length; i++) { fn(this.items[i], i); } }
上述代码咱们新建了一个迭代器类,构造函数接收一个数组,实例方法dealEach
能够接收一个回调,对实例上的items
每一项都执行这个回调。
其实JS数组不少原生方法都用了迭代器模式,好比find
,find
接收一个测试函数,返回符合这个测试函数的第一个数据。这个例子要作的是扩展这个功能,返回全部符合这个测试函数的数据项,并且也能够接收两个参数,第一个参数是属性名,第二个参数是值,一样返回全部该属性与值匹配的项:
// 外层用一个工厂模式封装下,调用时不用写new function iteratorFactory(data) { function Iterator(data) { this.data = data; } Iterator.prototype.findAll = function(handler, value) { const result = []; let handlerFn; // 处理参数,若是第一个参数是函数,直接拿来用 // 若是不是函数,就是属性名,给一个对比的默认函数 if(typeof handler === 'function') { handlerFn = handler; } else { handlerFn = function(item) { if(item[handler] === value) { return true; } return false; } } // 循环数据里面的每一项,将符合结果的塞入结果数组 for(let i = 0; i < this.data.length; i++) { const item = this.data[i]; const res = handlerFn(item); if(res) { result.push(item) } } return result; } return new Iterator(data); } // 写个数据测试下 const data = [{num: 1}, {num: 2}, {num: 3}]; iteratorFactory(data).findAll('num', 2); // [{num: 2}] iteratorFactory(data).findAll(item => item.num >= 2); // [{num: 2}, {num: 3}]
上述代码封装了一个相似数组find
的迭代器,扩展了他的功能,这种迭代器很是适合用来处理API返回的大量结构类似的数据。
备忘录模式相似于JS常用的缓存函数,内部记录一个状态,也就是缓存,当咱们再次访问的时候能够直接拿缓存数据:
function memo() { const cache = {}; return function(arg) { if(cache[arg]) { return cache[arg]; } else { // 没缓存的时候先执行方法,获得结果res // 而后将res写入缓存 cache[arg] = res; return res; } }
这个例子在实际项目中也比较常见,用户每次点进一个新文章都须要从API请求数据,若是他下次再点进同一篇文章,咱们可能但愿直接用上次请求的数据,而再也不次请求,这时候就能够用到咱们的备忘录模式了,直接拿上面的结构来用就好了:
function pageCache(pageId) { const cache = {}; return function(pageId) { // 为了保持返回类型一致,咱们都返回一个Promise if(cache[pageId]) { return Promise.solve(cache[pageId]); } else { return axios.get(pageId).then((data) => { cache[pageId] = data; return data; }) } } }
上述代码用了备忘录模式来解决这个问题,可是代码比较简单,实际项目中可能需求会更加复杂一些,可是这个思路仍是能够参考的。
这个例子的需求是,咱们须要作一个能够移动的DIV,用户把这个DIV随意移动,可是他有时候可能误操做或者反悔了,想把这个DIV移动回去,也就是将状态回退到上一次,有了回退状态的需求,固然还有配对的前进状态的需求。这种相似的需求咱们就能够用备忘录模式实现:
function moveDiv() { this.states = []; // 一个数组记录全部状态 this.currentState = 0; // 一个变量记录当前状态位置 } // 移动方法,每次移动记录状态 moveDiv.prototype.move = function(type, num) { changeDiv(type, num); // 伪代码,移动DIV的具体操做,这里并未实现 // 记录本次操做到states里面去 this.states.push({type,num}); this.currentState = this.states.length - 1; // 改变当前状态指针 } // 前进方法,取出状态执行 moveDiv.prototype.forward = function() { // 若是当前不是最后一个状态 if(this.currentState < this.states.length - 1) { // 取出前进的状态 this.currentState++; const state = this.states[this.currentState]; // 执行该状态位置 changeDiv(state.type, state.num); } } // 后退方法是相似的 moveDiv.prototype.back = function() { // 若是当前不是第一个状态 if(this.currentState > 0) { // 取出后退的状态 this.currentState--; const state = this.states[this.currentState]; // 执行该状态位置 changeDiv(state.type, state.num); } }
上述代码经过一个数组将用户全部操做过的状态都记录下来了,用户能够随时在状态间进行前进和后退。
本文讲的这几种设计模式策略/状态模式
,外观模式
,迭代器模式
,备忘录模式
都很好理解,并且在实际工做中也很是常见,熟练使用他们能够有效减小冗余代码,提升咱们的代码质量。
策略模式
经过将咱们的if
条件改写为一条条的策略减小了if...else
的数量,看起来更清爽,扩展起来也更方便。状态模式
跟策略模式
很像,只是还多了一个状态,能够根据这个状态来选取具体的策略。外观模式
可能咱们已经在无心间使用了,就是将模块一些内部逻辑封装在一个更高级的接口内部,或者将一些相似操做封装在一个方法内部,从而让外部调用更加方便。迭代器模式
在JS数组上有不少实现,咱们也能够模仿他们作一下数据处理的工做,特别适合处理从API拿来的大量结构类似的数据。备忘录模式
就是加一个缓存对象,用来记录以前获取过的数据或者操做的状态,后面能够用来加快访问速度或者进行状态回滚。本文是讲设计模式的最后一篇文章,前面三篇是:
文章的最后,感谢你花费宝贵的时间阅读本文,若是本文给了你一点点帮助或者启发,请不要吝啬你的赞和GitHub小星星,你的支持是做者持续创做的动力。
本文素材来自于网易高级前端开发工程师微专业唐磊老师的设计模式课程。
做者博文GitHub项目地址: https://github.com/dennis-jiang/Front-End-Knowledges