浅谈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;i
Good:
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;i
Good:
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:undefined
Good:
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;i
Good:
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》,本文对原文进行了一些修改。