模式和重构之间有着内在的联系。从某种角度来看,设计模式的目的是为许多重构活动提供目标。1.提炼函数在JavaScript开发中,我们大部分时间都在和函数打交道,所以我们希望这些函数有好的名字,函数体包含的逻辑清晰明了。如果一个函数太长,你必须添加几个注释以使其更易于阅读,那么就需要重构这些函数。如果一个函数中有一段代码是可以隔离的,那么我们最好将这段代码放到另一个独立的函数中。这是一个很常见的优化工作,这样做的好处主要有以下几点。避免非常大的功能。独立的功能有利于代码重用。分离的功能更容易覆盖。如果独立函数有一个好名字,它本身就会作为注释。比如在一个负责获取用户信息的函数中,我们还需要打印与用户信息相关的日志,那么打印日志的语句可以封装在一个独立的函数中:vargetUserInfo=function(){ajax('http://xxx.com/userInfo',function(data){console.log('userId:'+data.userId);console.log('userName:'+data.userName);console.log('昵称:'+data.nickName);});};改为:vargetUserInfo=function(){ajax('http://xxx.com/userInfo',function(data){printDetails(data);});};varprintDetails=function(data){console.log('userId:'+data.userId);console.log('userName:'+data.userName);console.log('nickName:'+data.nickName);};2。合并重复的条件片段如果一个函数体中有一些条件分支语句,并且这些条件分支语句内部散落着一些重复的代码,那么就需要进行合并去重。如果我们有一个分页函数paging,这个函数接收一个参数currPage,currPage表示要跳转的页码。在跳转之前,为了防止currPage传递过小或者过大的数字,我们需要手动修正它的值,具体见如下伪代码:varpaging=function(currPage){if(currPage<=0){currPage=0;jump(currPage);//跳转}elseif(currPage>=totalPage){currPage=totalPage;jump(currPage);//跳转}else{jump(currPage);//跳转}};看,负责跳转的代码jump(currPage)出现在每个条件分支中,所以这段代码可以完全分开:varpaging=function(currPage){if(currPage<=0){currPage=0;}elseif(currPage>=totalPage){currPage=totalPage;}jump(currPage);//分离跳转功能};3.Distillconditionalbranchstatementsintofunctions在编程中,复杂的条件分支语句是导致程序难以阅读和理解的重要原因,并且很容易导致庞大的函数。假设需要写一个getPrice函数来计算一个产品的价格。产品的计算只有一个规则:如果当前是夏季,则所有产品将以20%的折扣出售。代码如下:vargetPrice=function(price){vardate=newDate();if(date.getMonth()>=6&&date.getMonth()<=9){//summerreturnprice*0.8;}returnprice;};观察这句代码:if(date.getMonth()>=6&&date.getMonth()<=9){//...}这段代码的意思很简单,就是判断当前是否是夏季(七月)?十月)。这段代码虽然很短,但是代码表达的意图和代码本身还是有一定距离的。阅读代码的人必须花更多的精力来理解它所传达的意图。其实这段代码可以提炼成一个单独的函数,这样可以更准确的表达代码的意思,函数名本身可以起到注释的作用。代码如下:varisSummer=function(){vardate=newDate();returndate.getMonth()>=6&&date.getMonth()<=9;};vargetPrice=function(price){if(isSummer()){//夏季返还价格*0.8;}返还价格;};4.合理使用循环在函数体中,如果某些代码实际上负责一些重复的工作,那么合理使用循环不仅可以完成相同的功能,还可以减少代码量。下面是一段用于创建XHR对象的代码。为了简化例子,我们只考虑版本9以下的IE浏览器。代码如下:varcreateXHR=function(){varxhr;try{xhr=newActiveXObject('MSXML2.XMLHttp.6.0');}catch(e){try{xhr=newActiveXObject('MSXML2.XMLHttp.3.0');}catch(e){xhr=newActiveXObject('MSXML2.XMLHttp');}}returnxhr;};varxhr=createXHR();下面我们灵活地使用循环来达到和上面代码一样的效果:(vari=0,version;version=versions[i++];){try{returnnewActiveXObject(version);}catch(e){}}};varxhr=createXHR();5.让函数提前退出,而不是嵌套条件分支很多程序员都有“每个函数只能有一次入口和一次出口”的想法。现代编程语言将函数限制为只有一个条目。但是关于“一个函数只有一个出口”,往往会有一些不同的说法。下面的伪代码是典型的遵循“函数只有一个出口”的代码:onlyif(obj.isFolder){//如果是文件夹ret=deleteFolder(obj);}elseif(obj.isFile){//如果是文件ret=deleteFile(obj);}}returnret;};嵌套的条件分支语句绝对是代码维护者的噩梦。对于阅读代码的人来说,嵌套的if和else语句比扁平的if和else语句更难阅读和理解。有时,一个外部if分支的左括号和右括号相距500米。用《重构》的话来说,嵌套的条件分支往往是相信“每个函数只能有一个出口”的程序员写的。但实际上,如果您对该功能的其余部分不感兴趣,您应该立即退出。引导读者看到一些无用的else片段,只会阻碍他们对程序的理解。所以我们可以选择一些条件分支,让这个函数在进入这些条件分支后立即退出。为此,有一个常用的技巧,就是当遇到嵌套的if分支时,我们可以将外面的if表达式反转。重构后的del函数如下:vardel=function(obj){if(obj.isReadOnly){//反转if表达式return;}if(obj.isFolder){returndeleteFolder(obj);}if(obj.isFile){returndeleteFile(obj);}};6.传递对象参数而不是长参数列表有时一个函数可能会接收多个参数,参数越多,函数就越难理解和使用。使用该函数的人首先要了解所有参数的含义,使用时一定要小心,不要把某个参数传错了,或者把两个参数的位置弄反了。如果我们要在第三个参数和第四个参数中增加一个新的参数,会涉及到很多代码修改,代码如下:varsetUserInfo=function(id,name,address,sex,mobile,qq){console.log('id='+id);console.log('name='+name);console.log('address='+address);console.log('sex='+sex);console.log('mobile='+mobile);console.log('qq='+qq);};setUserInfo(1314,'sven','shenzhen','male','137********',377876679)这时候我们可以把所有的参数都放到一个对象中,然后把这个对象传给setUserInfo函数,setUserInfo函数需要的数据就可以自己从对象中获取到。现在不需要关心参数的个数和顺序,只需要保持参数对应的key值不变即可:varsetUserInfo=function(obj){console.log('id='+obj.id);console.log('name='+obj.name);console.log('address='+obj.address);console.log('sex='+obj.sex);console.log('mobile='+obj.mobile);console.log('qq='+obj.qq);};setUserInfo({id:1314,name:'sven',address:'shenzhen',sex:'male',mobile:'137********',qq:377876679});7.尽量减少参数的数量。如果在调用一个函数的时候需要传入多个参数,那么这个函数就让人望而生畏了。我们必须弄清楚这些参数的含义,并小心地将它们按顺序传递给函数。而如果一个函数可以在不传递任何参数的情况下使用,那么这种函数就很受欢迎。在实际开发中,给函数传递参数是不可避免的,但是我们应该尽量减少函数接收的参数个数。这是一个非常简单的例子。有一个绘图函数draw,现在只能画正方形,接收3个参数,分别是图形的宽、高、正方形:vardraw=function(width,height,square){};但实际上正方形的面积是可以通过宽高来计算的,所以我们可以把draw函数中的参数square去掉:vardraw=function(width,height){varsquare=width*height;};假设以后draw函数开始支持画圆了,我们需要把参数width和height换成radius半径,但是图形的面积square千万不要客户端传入,而应该在draw内部计算通过向传入的参数添加某些规则来实现功能。此时,我们可以使用策略模式,将draw函数做成一个支持绘制多种图形的函数。8、少用三元运算符一些程序员喜欢大量使用三元运算符来代替传统的if和else。原因是三元运算符性能高,代码少。然而,这两个理由其实都很难站得住脚。即使我们假设三元运算符的效率真的比if和else高,差异也完全可以忽略不计。在实际开发中,即使一段代码循环一百万次,使用三元运算符的时间开销与使用if和else处于同一水平。同样,与代码可读性和可维护性的损失相比,三元运算符节省的代码量也可以忽略不计。让JS文件加载更快的方法有很多,比如压缩、缓存、使用CDN和子域名等等,光看三元运算符节省的字符数,无异于300斤的人责怪头皮屑因为他超重。如果条件分支逻辑简单明了,这并不妨碍我们使用三元运算符:varglobal=typeofwindow!=="undefined"?window:this;但是如果条件分支逻辑非常复杂,如下面的代码所示,那我们最好选择分步写if和else。if和else语句有很多优点。一是它们比较容易阅读,二是修改起来比三元运算符周围的代码更方便:if(!aup||!bup){returna===doc?-1:b===doc?1:aup?-1:bup?1:sortInput?(indexOf.call(sortInput,a)-indexOf.call(sortInput,b)):0;}9.经常合理使用链式调用使用jQuery的程序员对链式方法调用已经相当习惯了。在JavaScript中,可以很容易地实现方法链式调用,即方法调用完成后返回对象本身,如下代码所示:varUser=function(){this.id=null;this.name=null;};User.prototype.setId=function(id){this.id=id;returnthis;};User.prototype.setName=function(name){this.name=name;返回这个;};console.log(newUser().setId(1314).setName('sven'));或者:varUser={id:null,name:null,setId:function(id){this.id=id;returnthis;},setName:function(name){this.name=name;returnthis;}};控制台。日志(User.setId(1314).setName('sven'));使用链式调用的方法并不会造成太大的阅读困难,确实可以节省一些字符和中间变量,但是节省的字符数量也是微不足道的。链式调用的缺点是调试时非常不方便。如果我们知道一条链有错误,我们首先要拆解这条链添加一些调试日志或者添加断点,从而定位到错误。它出现的地方。如果链的结构比较稳定,以后不容易修改,那么用链调用也无可厚非。但如果链条容易变动,调试维护困难,建议使用普通调用的形式:varuser=newUser();user.setId(1314);user.setName('sven');10.分解大在HTML5版《街头霸王》的第一段代码中,负责创建游戏角色的Spirit类非常庞大。它不仅负责创建角色精灵,还包括角色的攻击和防御等动作方法。代码如下:varSpirit=function(name){this.name=name;};Spirit.prototype.attack=function(type){//attackif(type==='waveBoxing'){console.log(this.name+':使用waveboxing');}elseif(type==='whirlKick'){console.log(this.name+':使用WhirlKick');}};varspirit=newSpir??it('RYU');spirit.attack('waveBoxing');//Output:RYU:usewavepunchspirit.attack('whirlKick');//Output:RYU:usewhirlwindleg后来发现Spirit.prototype.attack方法的实现量太大了,其实就是完全有必要,因为存在一个单独的类。面向对象的设计鼓励在合理数量的较小对象之间分布行为:varAttack=function(spirit){this.spirit=spirit;};Attack.prototype.start=function(type){returnthis.list[type]。call(this);};Attack.prototype.list={waveBoxing:function(){console.log(this.spirit.name+':使用waveboxing');},whirlKick:function(){console.log(this.spirit.name+':使用旋风腿');}};现在Spirit类已经精简了很多,不再包括各种攻击方式,而是将攻击动作委托给Attack类的对象来执行,这段代码也是策略模式的使用之一:varSpirit=function(name){this.name=name;this.attackObj=newAttack(this);};Spirit.prototype.attack=function(type){//攻击这个.attackObj.start(type);};varspirit=newSpir??it('RYU');spirit.attack('waveBoxing');//Output:RYU:usewaveboxingspirit.attack('whirlKick');//Output:RYU:UseWhirlwind11.使用return退出多重循环假设有double函数体中的loop语句,我们需要在内循环中进行判断,当达到某个临界条件时退出外循环。大多数时候我们会引入一个控制标志变量:varfunc=function(){varflag=false;for(vari=0;i<10;i++){for(varj=0;j<10;j++){if(i*j>30){flag=true;break;}}if(flag===true){break;}}};第二种方式是设置循环标志:varfunc=function(){outerloop:for(vari=0;i<10;i++){innerloop:for(varj=0;j<10;j++){if(i*j>30){breakouterloop;}}}};这两种做法无疑让人眼花缭乱,更简单的做法是在需要终止循环时直接退出整个方法:varfunc=function(){for(vari=0;i<10;i++){for(varj=0;j<10;j++){if(i*j>30){return;}}}};当然,使用return直接退出方法会出现问题。如果有一些代码会在循环后执行怎么办?如果我们提前退出整个方法,这些代码将没有机会被执行:varfunc=function(){for(vari=0;i<10;i++){for(varj=0;j<10;j++){if(i*j>30){return;}}}console.log(i);//这段代码没有机会执行};为了解决这个问题,我们可以把循环后面的代码放在return后面,如果代码比较多,应该把它们提炼成一个函数:varprint=function(i){console.log(i);};varfunc=function(){for(vari=0;i<10;i++){for(varj=0;j<10;j++){if(i*j>30){returnprint(i);}}}};func();本文转载自微信公众号“大招天下”,可通过以下二维码关注。转载本文请联系大千世界公众号。
