浅谈JavaScript 代码简洁之道
测试代码质量的唯一方式:别人看你代码时说f*k的次数。
代码质量与其整洁度成正比。干净的代码,既在质量上较为可靠,也为后期维护、升级奠定了良好基础。
本文并不是代码风格指南,而是关于代码的可读性、复用性、扩展性探讨。
我们将从几个方面展开讨论:
- 变量
- 函数
- 对象和数据结构
- 类
- SOLID
- 测试
- 异步
- 错误处理
- 代码风格
- 注释
变量
用有意义且常用的单词命名变量
Bad:
constyyyymmdstr=moment().format('YYYY/MM/DD');
Good:
constcurrentDate=moment().format('YYYY/MM/DD');
保持统一
可能同一个项目对于获取用户信息,会有三个不一样的命名。应该保持统一,如果你不知道该如何取名,可以去codelf搜索,看别人是怎么取名的。
Bad:
getUserInfo(); getClientData(); getCustomerRecord();
Good:
getUser()
每个常量都该命名
可以用buddy.js或者ESLint检测代码中未命名的常量。
Bad:
//三个月之后你还能知道86400000是什么吗? setTimeout(blastOff,86400000);
Good:
constMILLISECOND_IN_A_DAY=86400000; setTimeout(blastOff,MILLISECOND_IN_A_DAY);
可描述
通过一个变量生成了一个新变量,也需要为这个新变量命名,也就是说每个变量当你看到他第一眼你就知道他是干什么的。
Bad:
constADDRESS='OneInfiniteLoop,Cupertino95014'; constCITY_ZIP_CODE_REGEX=/^[^,\]+[,\s]+(.+?)s*(d{5})?$/; saveCityZipCode(ADDRESS.match(CITY_ZIP_CODE_REGEX)[1], ADDRESS.match(CITY_ZIP_CODE_REGEX)[2]);
Good:
constADDRESS='OneInfiniteLoop,Cupertino95014'; constCITY_ZIP_CODE_REGEX=/^[^,\]+[,\s]+(.+?)s*(d{5})?$/; const[,city,zipCode]=ADDRESS.match(CITY_ZIP_CODE_REGEX)||[]; saveCityZipCode(city,zipCode);
直接了当
Bad:
constlocations=['Austin','NewYork','SanFrancisco']; locations.forEach((l)=>{ doStuff(); doSomeOtherStuff(); //... //... //... //需要看其他代码才能确定'l'是干什么的。 dispatch(l); });
Good:
constlocations=['Austin','NewYork','SanFrancisco']; locations.forEach((location)=>{ doStuff(); doSomeOtherStuff(); //... //... //... dispatch(location); });
避免无意义的前缀
如果创建了一个对象car,就没有必要把它的颜色命名为carColor。
Bad:
constcar={ carMake:'Honda', carModel:'Accord', carColor:'Blue' }; functionpaintCar(car){ car.carColor='Red'; }
Good:
constcar={ make:'Honda', model:'Accord', color:'Blue' }; functionpaintCar(car){ car.color='Red'; }
使用默认值
Bad:
functioncreateMicrobrewery(name){ constbreweryName=name||'HipsterBrewCo.'; //... }
Good:
functioncreateMicrobrewery(name='HipsterBrewCo.'){ //... }
函数
参数越少越好
如果参数超过两个,使用ES2015/ES6的解构语法,不用考虑参数的顺序。
Bad:
functioncreateMenu(title,body,buttonText,cancellable){ //... }
Good:
functioncreateMenu({title,body,buttonText,cancellable}){ //... } createMenu({ title:'Foo', body:'Bar', buttonText:'Baz', cancellable:true });
只做一件事情
这是一条在软件工程领域流传久远的规则。严格遵守这条规则会让你的代码可读性更好,也更容易重构。如果违反这个规则,那么代码会很难被测试或者重用。
Bad:
functionemailClients(clients){ clients.forEach((client)=>{ constclientRecord=database.lookup(client); if(clientRecord.isActive()){ email(client); } }); }
Good:
functionemailActiveClients(clients){ clients .filter(isActiveClient) .forEach(email); } functionisActiveClient(client){ constclientRecord=database.lookup(client); returnclientRecord.isActive(); }
顾名思义
看函数名就应该知道它是干啥的。
Bad:
functionaddToDate(date,month){ //... } constdate=newDate(); //很难知道是把什么加到日期中 addToDate(date,1);
Good:
functionaddMonthToDate(month,date){ //... } constdate=newDate(); addMonthToDate(1,date);
只需要一层抽象层
如果函数嵌套过多会导致很难复用以及测试。
Bad:
functionparseBetterJSAlternative(code){ constREGEXES=[ //... ]; conststatements=code.split(''); consttokens=[]; REGEXES.forEach((REGEX)=>{ statements.forEach((statement)=>{ //... }); }); constast=[]; tokens.forEach((token)=>{ //lex... }); ast.forEach((node)=>{ //parse... }); }
Good:
functionparseBetterJSAlternative(code){ consttokens=tokenize(code); constast=lexer(tokens); ast.forEach((node)=>{ //parse... }); } functiontokenize(code){ constREGEXES=[ //... ]; conststatements=code.split(''); consttokens=[]; REGEXES.forEach((REGEX)=>{ statements.forEach((statement)=>{ tokens.push(/*...*/); }); }); returntokens; } functionlexer(tokens){ constast=[]; tokens.forEach((token)=>{ ast.push(/*...*/); }); returnast; }
删除重复代码
很多时候虽然是同一个功能,但由于一两个不同点,让你不得不写两个几乎相同的函数。
要想优化重复代码需要有较强的抽象能力,错误的抽象还不如重复代码。所以在抽象过程中必须要遵循SOLID原则(SOLID是什么?稍后会详细介绍)。
Bad:
functionshowDeveloperList(developers){ developers.forEach((developer)=>{ constexpectedSalary=developer.calculateExpectedSalary(); constexperience=developer.getExperience(); constgithubLink=developer.getGithubLink(); constdata={ expectedSalary, experience, githubLink }; render(data); }); } functionshowManagerList(managers){ managers.forEach((manager)=>{ constexpectedSalary=manager.calculateExpectedSalary(); constexperience=manager.getExperience(); constportfolio=manager.getMBAProjects(); constdata={ expectedSalary, experience, portfolio }; render(data); }); }
Good:
functionshowEmployeeList(employees){ employees.forEach(employee=>{ constexpectedSalary=employee.calculateExpectedSalary(); constexperience=employee.getExperience(); constdata={ expectedSalary, experience, }; switch(employee.type){ case'develop': data.githubLink=employee.getGithubLink(); break case'manager': data.portfolio=employee.getMBAProjects(); break } render(data); }) }
对象设置默认属性
Bad:
constmenuConfig={ title:null, body:'Bar', buttonText:null, cancellable:true }; functioncreateMenu(config){ config.title=config.title||'Foo'; config.body=config.body||'Bar'; config.buttonText=config.buttonText||'Baz'; config.cancellable=config.cancellable!==undefined?config.cancellable:true; } createMenu(menuConfig);
Good:
constmenuConfig={ title:'Order', //'body'key缺失 buttonText:'Send', cancellable:true }; functioncreateMenu(config){ config=Object.assign({ title:'Foo', body:'Bar', buttonText:'Baz', cancellable:true },config); //config就变成了:{title:"Order",body:"Bar",buttonText:"Send",cancellable:true} //... } createMenu(menuConfig);
不要传flag参数
通过flag的true或false,来判断执行逻辑,违反了一个函数干一件事的原则。
Bad:
functioncreateFile(name,temp){ if(temp){ fs.create(`./temp/${name}`); }else{ fs.create(name); } }
Good:
functioncreateFile(name){ fs.create(name); } functioncreateFileTemplate(name){ createFile(`./temp/${name}`) }
避免副作用(第一部分)
函数接收一个值返回一个新值,除此之外的行为我们都称之为副作用,比如修改全局变量、对文件进行IO操作等。
当函数确实需要副作用时,比如对文件进行IO操作时,请不要用多个函数/类进行文件操作,有且仅用一个函数/类来处理。也就是说副作用需要在唯一的地方处理。
副作用的三大天坑:随意修改可变数据类型、随意分享没有数据结构的状态、没有在统一地方处理副作用。
Bad:
//全局变量被一个函数引用 //现在这个变量从字符串变成了数组,如果有其他的函数引用,会发生无法预见的错误。 varname='RyanMcDermott'; functionsplitIntoFirstAndLastName(){ name=name.split(''); } splitIntoFirstAndLastName(); console.log(name);//['Ryan','McDermott']; Good: varname='RyanMcDermott'; varnewName=splitIntoFirstAndLastName(name) functionsplitIntoFirstAndLastName(name){ returnname.split(''); } console.log(name);//'RyanMcDermott'; console.log(newName);//['Ryan','McDermott'];
避免副作用(第二部分)
在JavaScript中,基本类型通过赋值传递,对象和数组通过引用传递。以引用传递为例:
假如我们写一个购物车,通过addItemToCart()方法添加商品到购物车,修改购物车数组。此时调用purchase()方法购买,由于引用传递,获取的购物车数组正好是最新的数据。
看起来没问题对不对?
如果当用户点击购买时,网络出现故障,purchase()方法一直在重复调用,与此同时用户又添加了新的商品,这时网络又恢复了。那么purchase()方法获取到购物车数组就是错误的。
为了避免这种问题,我们需要在每次新增商品时,克隆购物车数组并返回新的数组。
Bad:
constaddItemToCart=(cart,item)=>{ cart.push({item,date:Date.now()}); };
Good:
constaddItemToCart=(cart,item)=>{ return[...cart,{item,date:Date.now()}] };
不要写全局方法
在JavaScript中,永远不要污染全局,会在生产环境中产生难以预料的bug。举个例子,比如你在Array.prototype上新增一个diff方法来判断两个数组的不同。而你同事也打算做类似的事情,不过他的diff方法是用来判断两个数组首位元素的不同。很明显你们方法会产生冲突,遇到这类问题我们可以用ES2015/ES6的语法来对Array进行扩展。
Bad:
Array.prototype.diff=functiondiff(comparisonArray){ consthash=newSet(comparisonArray); returnthis.filter(elem=>!hash.has(elem)); };
Good:
classSuperArrayextendsArray{ diff(comparisonArray){ consthash=newSet(comparisonArray); returnthis.filter(elem=>!hash.has(elem)); } }
比起命令式我更喜欢函数式编程
函数式变编程可以让代码的逻辑更清晰更优雅,方便测试。
Bad:
constprogrammerOutput=[ { name:'UncleBobby', linesOfCode:500 },{ name:'SuzieQ', linesOfCode:1500 },{ name:'JimmyGosling', linesOfCode:150 },{ name:'GracieHopper', linesOfCode:1000 } ]; lettotalOutput=0; for(leti=0;iGood:
constprogrammerOutput=[ { name:'UncleBobby', linesOfCode:500 },{ name:'SuzieQ', linesOfCode:1500 },{ name:'JimmyGosling', linesOfCode:150 },{ name:'GracieHopper', linesOfCode:1000 } ]; lettotalOutput=programmerOutput .map(output=>output.linesOfCode) .reduce((totalLines,lines)=>totalLines+lines,0)封装条件语句
Bad:
if(fsm.state==='fetching'&&isEmpty(listNode)){ //... }Good:
functionshouldShowSpinner(fsm,listNode){ returnfsm.state==='fetching'&&isEmpty(listNode); } if(shouldShowSpinner(fsmInstance,listNodeInstance)){ //... }尽量别用“非”条件句
Bad:
functionisDOMNodeNotPresent(node){ //... } if(!isDOMNodeNotPresent(node)){ //... }Good:
functionisDOMNodePresent(node){ //... } if(isDOMNodePresent(node)){ //... }避免使用条件语句
Q:不用条件语句写代码是不可能的。
A:绝大多数场景可以用多态替代。
Q:用多态可行,但为什么就不能用条件语句了呢?
A:为了让代码更简洁易读,如果你的函数中出现了条件判断,那么说明你的函数不止干了一件事情,违反了函数单一原则。
Bad:
classAirplane{ //... //获取巡航高度 getCruisingAltitude(){ switch(this.type){ case'777': returnthis.getMaxAltitude()-this.getPassengerCount(); case'AirForceOne': returnthis.getMaxAltitude(); case'Cessna': returnthis.getMaxAltitude()-this.getFuelExpenditure(); } } }Good:
classAirplane{ //... } //波音777 classBoeing777extendsAirplane{ //... getCruisingAltitude(){ returnthis.getMaxAltitude()-this.getPassengerCount(); } } //空军一号 classAirForceOneextendsAirplane{ //... getCruisingAltitude(){ returnthis.getMaxAltitude(); } } //赛纳斯飞机 classCessnaextendsAirplane{ //... getCruisingAltitude(){ returnthis.getMaxAltitude()-this.getFuelExpenditure(); } }避免类型检查(第一部分)
JavaScript是无类型的,意味着你可以传任意类型参数,这种自由度很容易让人困扰,不自觉的就会去检查类型。仔细想想是你真的需要检查类型还是你的API设计有问题?
Bad:
functiontravelToTexas(vehicle){ if(vehicleinstanceofBicycle){ vehicle.pedal(this.currentLocation,newLocation('texas')); }elseif(vehicleinstanceofCar){ vehicle.drive(this.currentLocation,newLocation('texas')); } }Good:
functiontravelToTexas(vehicle){ vehicle.move(this.currentLocation,newLocation('texas')); }避免类型检查(第二部分)
如果你需要做静态类型检查,比如字符串、整数等,推荐使用TypeScript,不然你的代码会变得又臭又长。
Bad:
functioncombine(val1,val2){ if(typeofval1==='number'&&typeofval2==='number'|| typeofval1==='string'&&typeofval2==='string'){ returnval1+val2; } thrownewError('MustbeoftypeStringorNumber'); }Good:
functioncombine(val1,val2){ returnval1+val2; }不要过度优化
现代浏览器已经在底层做了很多优化,过去的很多优化方案都是无效的,会浪费你的时间,想知道现代浏览器优化了哪些内容,请点这里。
Bad:
//在老的浏览器中,由于`list.length`没有做缓存,每次迭代都会去计算,造成不必要开销。 //现代浏览器已对此做了优化。 for(leti=0,len=list.length;iGood:
for(leti=0;i删除弃用代码
很多时候有些代码已经没有用了,但担心以后会用,舍不得删。
如果你忘了这件事,这些代码就永远存在那里了。
放心删吧,你可以在代码库历史版本中找他它。
Bad:
functionoldRequestModule(url){ //... } functionnewRequestModule(url){ //... } constreq=newRequestModule; inventoryTracker('apples',req,'www.inventory-awesome.io');Good:
functionnewRequestModule(url){ //... } constreq=newRequestModule; inventoryTracker('apples',req,'www.inventory-awesome.io');对象和数据结构
用get、set方法操作数据
这样做可以带来很多好处,比如在操作数据时打日志,方便跟踪错误;在set的时候很容易对数据进行校验…
Bad:
functionmakeBankAccount(){ //... return{ balance:0, //... }; } constaccount=makeBankAccount(); account.balance=100;Good:
functionmakeBankAccount(){ //私有变量 letbalance=0; functiongetBalance(){ returnbalance; } functionsetBalance(amount){ //...在更新balance前,对amount进行校验 balance=amount; } return{ //... getBalance, setBalance, }; } constaccount=makeBankAccount(); account.setBalance(100);使用私有变量
可以用闭包来创建私有变量
Bad:
constEmployee=function(name){ this.name=name; }; Employee.prototype.getName=functiongetName(){ returnthis.name; }; constemployee=newEmployee('JohnDoe'); console.log(`Employeename:${employee.getName()}`); //Employeename:JohnDoe deleteemployee.name; console.log(`Employeename:${employee.getName()}`); //Employeename:undefinedGood:
functionmakeEmployee(name){ return{ getName(){ returnname; }, }; } constemployee=makeEmployee('JohnDoe'); console.log(`Employeename:${employee.getName()}`); //Employeename:JohnDoe deleteemployee.name; console.log(`Employeename:${employee.getName()}`); //Employeename:JohnDoe类
使用class
在ES2015/ES6之前,没有类的语法,只能用构造函数的方式模拟类,可读性非常差。
Bad:
//动物 constAnimal=function(age){ if(!(thisinstanceofAnimal)){ thrownewError('InstantiateAnimalwith`new`'); } this.age=age; }; Animal.prototype.move=functionmove(){}; //哺乳动物 constMammal=function(age,furColor){ if(!(thisinstanceofMammal)){ thrownewError('InstantiateMammalwith`new`'); } Animal.call(this,age); this.furColor=furColor; }; Mammal.prototype=Object.create(Animal.prototype); Mammal.prototype.constructor=Mammal; Mammal.prototype.liveBirth=functionliveBirth(){}; //人类 constHuman=function(age,furColor,languageSpoken){ if(!(thisinstanceofHuman)){ thrownewError('InstantiateHumanwith`new`'); } Mammal.call(this,age,furColor); this.languageSpoken=languageSpoken; }; Human.prototype=Object.create(Mammal.prototype); Human.prototype.constructor=Human; Human.prototype.speak=functionspeak(){};Good:
//动物 classAnimal{ constructor(age){ this.age=age }; move(){}; } //哺乳动物 classMammalextendsAnimal{ constructor(age,furColor){ super(age); this.furColor=furColor; }; liveBirth(){}; } //人类 classHumanextendsMammal{ constructor(age,furColor,languageSpoken){ super(age,furColor); this.languageSpoken=languageSpoken; }; speak(){}; }链式调用
这种模式相当有用,可以在很多库中发现它的身影,比如jQuery、Lodash等。它让你的代码简洁优雅。实现起来也非常简单,在类的方法最后返回this可以了。
Bad:
classCar{ constructor(make,model,color){ this.make=make; this.model=model; this.color=color; } setMake(make){ this.make=make; } setModel(model){ this.model=model; } setColor(color){ this.color=color; } save(){ console.log(this.make,this.model,this.color); } } constcar=newCar('Ford','F-150','red'); car.setColor('pink'); car.save();Good:
classCar{ constructor(make,model,color){ this.make=make; this.model=model; this.color=color; } setMake(make){ this.make=make; returnthis; } setModel(model){ this.model=model; returnthis; } setColor(color){ this.color=color; returnthis; } save(){ console.log(this.make,this.model,this.color); returnthis; } } constcar=newCar('Ford','F-150','red') .setColor('pink'); .save();不要滥用继承
很多时候继承被滥用,导致可读性很差,要搞清楚两个类之间的关系,继承表达的一个属于关系,而不是包含关系,比如Human->Animalvs.User->UserDetails
Bad:
classEmployee{ constructor(name,email){ this.name=name; this.email=email; } //... } //TaxData(税收信息)并不是属于Employee(雇员),而是包含关系。 classEmployeeTaxDataextendsEmployee{ constructor(ssn,salary){ super(); this.ssn=ssn; this.salary=salary; } //... }Good:
classEmployeeTaxData{ constructor(ssn,salary){ this.ssn=ssn; this.salary=salary; } //... } classEmployee{ constructor(name,email){ this.name=name; this.email=email; } setTaxData(ssn,salary){ this.taxData=newEmployeeTaxData(ssn,salary); } //... }SOLID
SOLID是几个单词首字母组合而来,分别表示单一功能原则、开闭原则、里氏替换原则、接口隔离原则以及依赖反转原则。
单一功能原则
如果一个类干的事情太多太杂,会导致后期很难维护。我们应该厘清职责,各司其职减少相互之间依赖。
Bad:
classUserSettings{ constructor(user){ this.user=user; } changeSettings(settings){ if(this.verifyCredentials()){ //... } } verifyCredentials(){ //... } }Good:
classUserAuth{ constructor(user){ this.user=user; } verifyCredentials(){ //... } } classUserSetting{ constructor(user){ this.user=user; this.auth=newUserAuth(this.user); } changeSettings(settings){ if(this.auth.verifyCredentials()){ //... } } } }开闭原则
“开”指的就是类、模块、函数都应该具有可扩展性,“闭”指的是它们不应该被修改。也就是说你可以新增功能但不能去修改源码。
Bad:
classAjaxAdapterextendsAdapter{ constructor(){ super(); this.name='ajaxAdapter'; } } classNodeAdapterextendsAdapter{ constructor(){ super(); this.name='nodeAdapter'; } } classHttpRequester{ constructor(adapter){ this.adapter=adapter; } fetch(url){ if(this.adapter.name==='ajaxAdapter'){ returnmakeAjaxCall(url).then((response)=>{ //传递response并return }); }elseif(this.adapter.name==='httpNodeAdapter'){ returnmakeHttpCall(url).then((response)=>{ //传递response并return }); } } } functionmakeAjaxCall(url){ //处理request并returnpromise } functionmakeHttpCall(url){ //处理request并returnpromise }Good:
classAjaxAdapterextendsAdapter{ constructor(){ super(); this.name='ajaxAdapter'; } request(url){ //处理request并returnpromise } } classNodeAdapterextendsAdapter{ constructor(){ super(); this.name='nodeAdapter'; } request(url){ //处理request并returnpromise } } classHttpRequester{ constructor(adapter){ this.adapter=adapter; } fetch(url){ returnthis.adapter.request(url).then((response)=>{ //传递response并return }); } }里氏替换原则
名字很唬人,其实道理很简单,就是子类不要去重写父类的方法。
Bad:
//长方形 classRectangle{ constructor(){ this.width=0; this.height=0; } setColor(color){ //... } render(area){ //... } setWidth(width){ this.width=width; } setHeight(height){ this.height=height; } getArea(){ returnthis.width*this.height; } } //正方形 classSquareextendsRectangle{ setWidth(width){ this.width=width; this.height=width; } setHeight(height){ this.width=height; this.height=height; } } functionrenderLargeRectangles(rectangles){ rectangles.forEach((rectangle)=>{ rectangle.setWidth(4); rectangle.setHeight(5); constarea=rectangle.getArea(); rectangle.render(area); }); } constrectangles=[newRectangle(),newRectangle(),newSquare()]; renderLargeRectangles(rectangles);Good:
classShape{ setColor(color){ //... } render(area){ //... } } classRectangleextendsShape{ constructor(width,height){ super(); this.width=width; this.height=height; } getArea(){ returnthis.width*this.height; } } classSquareextendsShape{ constructor(length){ super(); this.length=length; } getArea(){ returnthis.length*this.length; } } functionrenderLargeShapes(shapes){ shapes.forEach((shape)=>{ constarea=shape.getArea(); shape.render(area); }); } constshapes=[newRectangle(4,5),newRectangle(4,5),newSquare(5)]; renderLargeShapes(shapes);接口隔离原则
JavaScript几乎没有接口的概念,所以这条原则很少被使用。官方定义是“客户端不应该依赖它不需要的接口”,也就是接口最小化,把接口解耦。
Bad:
classDOMTraverser{ constructor(settings){ this.settings=settings; this.setup(); } setup(){ this.rootNode=this.settings.rootNode; this.animationModule.setup(); } traverse(){ //... } } const$=newDOMTraverser({ rootNode:document.getElementsByTagName('body'), animationModule(){}//Mostofthetime,wewon'tneedtoanimatewhentraversing. //... });Good:
classDOMTraverser{ constructor(settings){ this.settings=settings; this.options=settings.options; this.setup(); } setup(){ this.rootNode=this.settings.rootNode; this.setupOptions(); } setupOptions(){ if(this.options.animationModule){ //... } } traverse(){ //... } } const$=newDOMTraverser({ rootNode:document.getElementsByTagName('body'), options:{ animationModule(){} } });依赖反转原则
说就两点:
- 高层次模块不能依赖低层次模块,它们依赖于抽象接口。
- 抽象接口不能依赖具体实现,具体实现依赖抽象接口。
总结下来就两个字,解耦。
Bad:
//库存查询 classInventoryRequester{ constructor(){ this.REQ_METHODS=['HTTP']; } requestItem(item){ //... } } //库存跟踪 classInventoryTracker{ constructor(items){ this.items=items; //这里依赖一个特殊的请求类,其实我们只是需要一个请求方法。 this.requester=newInventoryRequester(); } requestItems(){ this.items.forEach((item)=>{ this.requester.requestItem(item); }); } } constinventoryTracker=newInventoryTracker(['apples','bananas']); inventoryTracker.requestItems();Good:
//库存跟踪 classInventoryTracker{ constructor(items,requester){ this.items=items; this.requester=requester; } requestItems(){ this.items.forEach((item)=>{ this.requester.requestItem(item); }); } } //HTTP请求 classInventoryRequesterHTTP{ constructor(){ this.REQ_METHODS=['HTTP']; } requestItem(item){ //... } } //webSocket请求 classInventoryRequesterWS{ constructor(){ this.REQ_METHODS=['WS']; } requestItem(item){ //... } } //通过依赖注入的方式将请求模块解耦,这样我们就可以很轻易的替换成webSocket请求。 constinventoryTracker=newInventoryTracker(['apples','bananas'],newInventoryRequesterHTTP()); inventoryTracker.requestItems();测试
随着项目变得越来越庞大,时间线拉长,有的老代码可能半年都没碰过,如果此时上线,你有信心这部分代码能正常工作吗?测试的覆盖率和你的信心是成正比的。
PS:如果你发现你的代码很难被测试,那么你应该优化你的代码了。
单一化
Bad:
importassertfrom'assert'; describe('MakeMomentJSGreatAgain',()=>{ it('handlesdateboundaries',()=>{ letdate; date=newMakeMomentJSGreatAgain('1/1/2015'); date.addDays(30); assert.equal('1/31/2015',date); date=newMakeMomentJSGreatAgain('2/1/2016'); date.addDays(28); assert.equal('02/29/2016',date); date=newMakeMomentJSGreatAgain('2/1/2015'); date.addDays(28); assert.equal('03/01/2015',date); }); });Good:
importassertfrom'assert'; describe('MakeMomentJSGreatAgain',()=>{ it('handles30-daymonths',()=>{ constdate=newMakeMomentJSGreatAgain('1/1/2015'); date.addDays(30); assert.equal('1/31/2015',date); }); it('handlesleapyear',()=>{ constdate=newMakeMomentJSGreatAgain('2/1/2016'); date.addDays(28); assert.equal('02/29/2016',date); }); it('handlesnon-leapyear',()=>{ constdate=newMakeMomentJSGreatAgain('2/1/2015'); date.addDays(28); assert.equal('03/01/2015',date); }); });异步
不再使用回调
不会有人愿意去看嵌套回调的代码,用Promises替代回调吧。
Bad:
import{get}from'request'; import{writeFile}from'fs'; get('https://en.wikipedia.org/wiki/Robert_Cecil_Martin',(requestErr,response)=>{ if(requestErr){ console.error(requestErr); }else{ writeFile('article.html',response.body,(writeErr)=>{ if(writeErr){ console.error(writeErr); }else{ console.log('Filewritten'); } }); } });Good:
get('https://en.wikipedia.org/wiki/Robert_Cecil_Martin') .then((response)=>{ returnwriteFile('article.html',response); }) .then(()=>{ console.log('Filewritten'); }) .catch((err)=>{ console.error(err); });Async/Await比起Promises更简洁
Bad:
import{get}from'request-promise'; import{writeFile}from'fs-promise'; get('https://en.wikipedia.org/wiki/Robert_Cecil_Martin') .then((response)=>{ returnwriteFile('article.html',response); }) .then(()=>{ console.log('Filewritten'); }) .catch((err)=>{ console.error(err); });Good:
import{get}from'request-promise'; import{writeFile}from'fs-promise'; asyncfunctiongetCleanCodeArticle(){ try{ constresponse=awaitget('https://en.wikipedia.org/wiki/Robert_Cecil_Martin'); awaitwriteFile('article.html',response); console.log('Filewritten'); }catch(err){ console.error(err); } }错误处理
不要忽略抛异常
Bad:
try{ functionThatMightThrow(); }catch(error){ console.log(error); }Good:
try{ functionThatMightThrow(); }catch(error){ //这一种选择,比起console.log更直观 console.error(error); //也可以在界面上提醒用户 notifyUserOfError(error); //也可以把异常传回服务器 reportErrorToService(error); //其他的自定义方法 }不要忘了在Promises抛异常
Bad:
getdata() .then((data)=>{ functionThatMightThrow(data); }) .catch((error)=>{ console.log(error); });Good:
getdata() .then((data)=>{ functionThatMightThrow(data); }) .catch((error)=>{ //这一种选择,比起console.log更直观 console.error(error); //也可以在界面上提醒用户 notifyUserOfError(error); //也可以把异常传回服务器 reportErrorToService(error); //其他的自定义方法 });代码风格
代码风格是主观的,争论哪种好哪种不好是在浪费生命。市面上有很多自动处理代码风格的工具,选一个喜欢就行了,我们来讨论几个非自动处理的部分。
常量大写
Bad:
constDAYS_IN_WEEK=7; constdaysInMonth=30; constsongs=['BackInBlack','StairwaytoHeaven','HeyJude']; constArtists=['ACDC','LedZeppelin','TheBeatles']; functioneraseDatabase(){} functionrestore_database(){} classanimal{} classAlpaca{}Good:
constDAYS_IN_WEEK=7; constDAYS_IN_MONTH=30; constSONGS=['BackInBlack','StairwaytoHeaven','HeyJude']; constARTISTS=['ACDC','LedZeppelin','TheBeatles']; functioneraseDatabase(){} functionrestoreDatabase(){} classAnimal{} classAlpaca{}先声明后调用
就像我们看报纸文章一样,从上到下看,所以为了方便阅读把函数声明写在函数调用前面。
Bad:
classPerformanceReview{ constructor(employee){ this.employee=employee; } lookupPeers(){ returndb.lookup(this.employee,'peers'); } lookupManager(){ returndb.lookup(this.employee,'manager'); } getPeerReviews(){ constpeers=this.lookupPeers(); //... } perfReview(){ this.getPeerReviews(); this.getManagerReview(); this.getSelfReview(); } getManagerReview(){ constmanager=this.lookupManager(); } getSelfReview(){ //... } } constreview=newPerformanceReview(employee); review.perfReview();Good:
classPerformanceReview{ constructor(employee){ this.employee=employee; } perfReview(){ this.getPeerReviews(); this.getManagerReview(); this.getSelfReview(); } getPeerReviews(){ constpeers=this.lookupPeers(); //... } lookupPeers(){ returndb.lookup(this.employee,'peers'); } getManagerReview(){ constmanager=this.lookupManager(); } lookupManager(){ returndb.lookup(this.employee,'manager'); } getSelfReview(){ //... } } constreview=newPerformanceReview(employee); review.perfReview();注释
只有业务逻辑需要注释
代码注释不是越多越好。
Bad:
functionhashIt(data){ //这是初始值 lethash=0; //数组的长度 constlength=data.length; //循环数组 for(leti=0;iGood:
functionhashIt(data){ lethash=0; constlength=data.length; for(leti=0;i删掉注释的代码
git存在的意义就是保存你的旧代码,所以注释的代码赶紧删掉吧。
Bad:
doStuff(); //doOtherStuff(); //doSomeMoreStuff(); //doSoMuchStuff();Good:
doStuff();不要记日记
记住你有git!,gitlog可以帮你干这事。
Bad:
/** *2016-12-20:删除了xxx *2016-10-01:改进了xxx *2016-02-03:删除了第12行的类型检查 *2015-03-14:增加了一个合并的方法 */ functioncombine(a,b){ returna+b; }Good:
functioncombine(a,b){ returna+b; }注释不需要高亮
注释高亮,并不能起到提示的作用,反而会干扰你阅读代码。
Bad:
//////////////////////////////////////////////////////////////////////////////// //ScopeModelInstantiation //////////////////////////////////////////////////////////////////////////////// $scope.model={ menu:'foo', nav:'bar' }; //////////////////////////////////////////////////////////////////////////////// //Actionsetup //////////////////////////////////////////////////////////////////////////////// constactions=function(){ //... };Good:
$scope.model={ menu:'foo', nav:'bar' }; constactions=function(){ //... };翻译自ryanmcdermott的《clean-code-javascript》,本文对原文进行了一些修改。