浅谈JavaScript 代码简洁之道
测试代码质量的唯一方式:别人看你代码时说 f * k 的次数。
代码质量与其整洁度成正比。干净的代码,既在质量上较为可靠,也为后期维护、升级奠定了良好基础。
本文并不是代码风格指南,而是关于代码的可读性、复用性、扩展性探讨。
我们将从几个方面展开讨论:
- 变量
- 函数
- 对象和数据结构
- 类
- solid
- 测试
- 异步
- 错误处理
- 代码风格
- 注释
变量
用有意义且常用的单词命名变量
bad:
const yyyymmdstr = moment().format('yyyy/mm/dd');
good:
const currentdate = moment().format('yyyy/mm/dd');
保持统一
可能同一个项目对于获取用户信息,会有三个不一样的命名。应该保持统一,如果你不知道该如何取名,可以去 搜索,看别人是怎么取名的。
bad:
getuserinfo(); getclientdata(); getcustomerrecord();
good:
getuser()
每个常量都该命名
可以用或者eslint 检测代码中未命名的常量。
bad:
// 三个月之后你还能知道 86400000 是什么吗? settimeout(blastoff, 86400000);
good:
const millisecond_in_a_day = 86400000; settimeout(blastoff, millisecond_in_a_day);
可描述
通过一个变量生成了一个新变量,也需要为这个新变量命名,也就是说每个变量当你看到他第一眼你就知道他是干什么的。
bad:
const address = 'one infinite loop, cupertino 95014'; const city_zip_code_regex = /^[^,\]+[,\s]+(.+?)s*(d{5})?$/; savecityzipcode(address.match(city_zip_code_regex)[1], address.match(city_zip_code_regex)[2]);
good:
const address = 'one infinite loop, cupertino 95014'; const city_zip_code_regex = /^[^,\]+[,\s]+(.+?)s*(d{5})?$/; const [, city, zipcode] = address.match(city_zip_code_regex) || []; savecityzipcode(city, zipcode);
直接了当
bad:
const locations = ['austin', 'new york', 'san francisco']; locations.foreach((l) => { dostuff(); dosomeotherstuff(); // ... // ... // ... // 需要看其他代码才能确定 'l' 是干什么的。 dispatch(l); });
good:
const locations = ['austin', 'new york', 'san francisco']; locations.foreach((location) => { dostuff(); dosomeotherstuff(); // ... // ... // ... dispatch(location); });
避免无意义的前缀
如果创建了一个对象 car,就没有必要把它的颜色命名为 carcolor。
bad:
const car = { carmake: 'honda', carmodel: 'accord', carcolor: 'blue' }; function paintcar(car) { car.carcolor = 'red'; }
good:
const car = { make: 'honda', model: 'accord', color: 'blue' }; function paintcar(car) { car.color = 'red'; }
使用默认值
bad:
function createmicrobrewery(name) { const breweryname = name || 'hipster brew co.'; // ... }
good:
function createmicrobrewery(name = 'hipster brew co.') { // ... }
函数
参数越少越好
如果参数超过两个,使用 es2015/es6 的解构语法,不用考虑参数的顺序。
bad:
function createmenu(title, body, buttontext, cancellable) { // ... }
good:
function createmenu({ title, body, buttontext, cancellable }) { // ... } createmenu({ title: 'foo', body: 'bar', buttontext: 'baz', cancellable: true });
只做一件事情
这是一条在软件工程领域流传久远的规则。严格遵守这条规则会让你的代码可读性更好,也更容易重构。如果违反这个规则,那么代码会很难被测试或者重用。
bad:
function emailclients(clients) { clients.foreach((client) => { const clientrecord = database.lookup(client); if (clientrecord.isactive()) { email(client); } }); }
good:
function emailactiveclients(clients) { clients .filter(isactiveclient) .foreach(email); } function isactiveclient(client) { const clientrecord = database.lookup(client); return clientrecord.isactive(); }
顾名思义
看函数名就应该知道它是干啥的。
bad:
function addtodate(date, month) { // ... } const date = new date(); // 很难知道是把什么加到日期中 addtodate(date, 1);
good:
function addmonthtodate(month, date) { // ... } const date = new date(); addmonthtodate(1, date);
只需要一层抽象层
如果函数嵌套过多会导致很难复用以及测试。
bad:
function parsebetterjsalternative(code) { const regexes = [ // ... ]; const statements = code.split(' '); const tokens = []; regexes.foreach((regex) => { statements.foreach((statement) => { // ... }); }); const ast = []; tokens.foreach((token) => { // lex... }); ast.foreach((node) => { // parse... }); }
good:
function parsebetterjsalternative(code) { const tokens = tokenize(code); const ast = lexer(tokens); ast.foreach((node) => { // parse... }); } function tokenize(code) { const regexes = [ // ... ]; const statements = code.split(' '); const tokens = []; regexes.foreach((regex) => { statements.foreach((statement) => { tokens.push( /* ... */ ); }); }); return tokens; } function lexer(tokens) { const ast = []; tokens.foreach((token) => { ast.push( /* ... */ ); }); return ast; }
删除重复代码
很多时候虽然是同一个功能,但由于一两个不同点,让你不得不写两个几乎相同的函数。
要想优化重复代码需要有较强的抽象能力,错误的抽象还不如重复代码。所以在抽象过程中必须要遵循 solid 原则(solid 是什么?稍后会详细介绍)。
bad:
function showdeveloperlist(developers) { developers.foreach((developer) => { const expectedsalary = developer.calculateexpectedsalary(); const experience = developer.getexperience(); const githublink = developer.getgithublink(); const data = { expectedsalary, experience, githublink }; render(data); }); } function showmanagerlist(managers) { managers.foreach((manager) => { const expectedsalary = manager.calculateexpectedsalary(); const experience = manager.getexperience(); const portfolio = manager.getmbaprojects(); const data = { expectedsalary, experience, portfolio }; render(data); }); }
good:
function showemployeelist(employees) { employees.foreach(employee => { const expectedsalary = employee.calculateexpectedsalary(); const experience = employee.getexperience(); const data = { expectedsalary, experience, }; switch(employee.type) { case 'develop': data.githublink = employee.getgithublink(); break case 'manager': data.portfolio = employee.getmbaprojects(); break } render(data); }) }
对象设置默认属性
bad:
const menuconfig = { title: null, body: 'bar', buttontext: null, cancellable: true }; function createmenu(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:
const menuconfig = { title: 'order', // 'body' key 缺失 buttontext: 'send', cancellable: true }; function createmenu(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:
function createfile(name, temp) { if (temp) { fs.create(`./temp/${name}`); } else { fs.create(name); } }
good:
function createfile(name) { fs.create(name); } function createfiletemplate(name) { createfile(`./temp/${name}`) }
避免副作用(第一部分)
函数接收一个值返回一个新值,除此之外的行为我们都称之为副作用,比如修改全局变量、对文件进行 io 操作等。
当函数确实需要副作用时,比如对文件进行 io 操作时,请不要用多个函数/类进行文件操作,有且仅用一个函数/类来处理。也就是说副作用需要在唯一的地方处理。
副作用的三大天坑:随意修改可变数据类型、随意分享没有数据结构的状态、没有在统一地方处理副作用。
bad:
// 全局变量被一个函数引用 // 现在这个变量从字符串变成了数组,如果有其他的函数引用,会发生无法预见的错误。 var name = 'ryan mcdermott'; function splitintofirstandlastname() { name = name.split(' '); } splitintofirstandlastname(); console.log(name); // ['ryan', 'mcdermott']; good: var name = 'ryan mcdermott'; var newname = splitintofirstandlastname(name) function splitintofirstandlastname(name) { return name.split(' '); } console.log(name); // 'ryan mcdermott'; console.log(newname); // ['ryan', 'mcdermott'];
避免副作用(第二部分)
在 javascript 中,基本类型通过赋值传递,对象和数组通过引用传递。以引用传递为例:
假如我们写一个购物车,通过 additemtocart() 方法添加商品到购物车,修改 购物车数组。此时调用 purchase() 方法购买,由于引用传递,获取的 购物车数组 正好是最新的数据。
看起来没问题对不对?
如果当用户点击购买时,网络出现故障, purchase() 方法一直在重复调用,与此同时用户又添加了新的商品,这时网络又恢复了。那么 purchase() 方法获取到 购物车数组 就是错误的。
为了避免这种问题,我们需要在每次新增商品时,克隆 购物车数组 并返回新的数组。
bad:
const additemtocart = (cart, item) => { cart.push({ item, date: date.now() }); };
good:
const additemtocart = (cart, item) => { return [...cart, {item, date: date.now()}] };
不要写全局方法
在 javascript 中,永远不要污染全局,会在生产环境中产生难以预料的 bug。举个例子,比如你在 array.prototype 上新增一个 diff 方法来判断两个数组的不同。而你同事也打算做类似的事情,不过他的 diff 方法是用来判断两个数组首位元素的不同。很明显你们方法会产生冲突,遇到这类问题我们可以用 es2015/es6 的语法来对 array 进行扩展。
bad:
array.prototype.diff = function diff(comparisonarray) { const hash = new set(comparisonarray); return this.filter(elem => !hash.has(elem)); };
good:
class superarray extends array { diff(comparisonarray) { const hash = new set(comparisonarray); return this.filter(elem => !hash.has(elem)); } }
比起命令式我更喜欢函数式编程
函数式变编程可以让代码的逻辑更清晰更优雅,方便测试。
bad:
const programmeroutput = [ { name: 'uncle bobby', linesofcode: 500 }, { name: 'suzie q', linesofcode: 1500 }, { name: 'jimmy gosling', linesofcode: 150 }, { name: 'gracie hopper', linesofcode: 1000 } ]; let totaloutput = 0; for (let i = 0; i < programmeroutput.length; i++) { totaloutput += programmeroutput[i].linesofcode; }
good:
const programmeroutput = [ { name: 'uncle bobby', linesofcode: 500 }, { name: 'suzie q', linesofcode: 1500 }, { name: 'jimmy gosling', linesofcode: 150 }, { name: 'gracie hopper', linesofcode: 1000 } ]; let totaloutput = programmeroutput .map(output => output.linesofcode) .reduce((totallines, lines) => totallines + lines, 0)
封装条件语句
bad:
if (fsm.state === 'fetching' && isempty(listnode)) { // ... }
good:
function shouldshowspinner(fsm, listnode) { return fsm.state === 'fetching' && isempty(listnode); } if (shouldshowspinner(fsminstance, listnodeinstance)) { // ... }
尽量别用“非”条件句
bad:
function isdomnodenotpresent(node) { // ... } if (!isdomnodenotpresent(node)) { // ... }
good:
function isdomnodepresent(node) { // ... } if (isdomnodepresent(node)) { // ... }
避免使用条件语句
q:不用条件语句写代码是不可能的。
a:绝大多数场景可以用多态替代。
q:用多态可行,但为什么就不能用条件语句了呢?
a:为了让代码更简洁易读,如果你的函数中出现了条件判断,那么说明你的函数不止干了一件事情,违反了函数单一原则。
bad:
class airplane { // ... // 获取巡航高度 getcruisingaltitude() { switch (this.type) { case '777': return this.getmaxaltitude() - this.getpassengercount(); case 'air force one': return this.getmaxaltitude(); case 'cessna': return this.getmaxaltitude() - this.getfuelexpenditure(); } } }
good:
class airplane { // ... } // 波音777 class boeing777 extends airplane { // ... getcruisingaltitude() { return this.getmaxaltitude() - this.getpassengercount(); } } // 空军一号 class airforceone extends airplane { // ... getcruisingaltitude() { return this.getmaxaltitude(); } } // 赛纳斯飞机 class cessna extends airplane { // ... getcruisingaltitude() { return this.getmaxaltitude() - this.getfuelexpenditure(); } }
避免类型检查(第一部分)
javascript 是无类型的,意味着你可以传任意类型参数,这种*度很容易让人困扰,不自觉的就会去检查类型。仔细想想是你真的需要检查类型还是你的 api 设计有问题?
bad:
function traveltotexas(vehicle) { if (vehicle instanceof bicycle) { vehicle.pedal(this.currentlocation, new location('texas')); } else if (vehicle instanceof car) { vehicle.drive(this.currentlocation, new location('texas')); } }
good:
function traveltotexas(vehicle) { vehicle.move(this.currentlocation, new location('texas')); }
避免类型检查(第二部分)
如果你需要做静态类型检查,比如字符串、整数等,推荐使用 typescript,不然你的代码会变得又臭又长。
bad:
function combine(val1, val2) { if (typeof val1 === 'number' && typeof val2 === 'number' || typeof val1 === 'string' && typeof val2 === 'string') { return val1 + val2; } throw new error('must be of type string or number'); }
good:
function combine(val1, val2) { return val1 + val2; }
不要过度优化
现代浏览器已经在底层做了很多优化,过去的很多优化方案都是无效的,会浪费你的时间,想知道现代浏览器优化了哪些内容,请点这里。
bad:
// 在老的浏览器中,由于 `list.length` 没有做缓存,每次迭代都会去计算,造成不必要开销。 // 现代浏览器已对此做了优化。 for (let i = 0, len = list.length; i < len; i++) { // ... }
good:
for (let i = 0; i < list.length; i++) { // ... }
删除弃用代码
很多时候有些代码已经没有用了,但担心以后会用,舍不得删。
如果你忘了这件事,这些代码就永远存在那里了。
放心删吧,你可以在代码库历史版本中找他它。
bad:
function oldrequestmodule(url) { // ... } function newrequestmodule(url) { // ... } const req = newrequestmodule; inventorytracker('apples', req, 'www.inventory-awesome.io');
good:
function newrequestmodule(url) { // ... } const req = newrequestmodule; inventorytracker('apples', req, 'www.inventory-awesome.io');
对象和数据结构
用 get、set 方法操作数据
这样做可以带来很多好处,比如在操作数据时打日志,方便跟踪错误;在 set 的时候很容易对数据进行校验…
bad:
function makebankaccount() { // ... return { balance: 0, // ... }; } const account = makebankaccount(); account.balance = 100;
good:
function makebankaccount() { // 私有变量 let balance = 0; function getbalance() { return balance; } function setbalance(amount) { // ... 在更新 balance 前,对 amount 进行校验 balance = amount; } return { // ... getbalance, setbalance, }; } const account = makebankaccount(); account.setbalance(100);
使用私有变量
可以用闭包来创建私有变量
bad:
const employee = function(name) { this.name = name; }; employee.prototype.getname = function getname() { return this.name; }; const employee = new employee('john doe'); console.log(`employee name: ${employee.getname()}`); // employee name: john doe delete employee.name; console.log(`employee name: ${employee.getname()}`); // employee name: undefined
good:
function makeemployee(name) { return { getname() { return name; }, }; } const employee = makeemployee('john doe'); console.log(`employee name: ${employee.getname()}`); // employee name: john doe delete employee.name; console.log(`employee name: ${employee.getname()}`); // employee name: john doe
类
使用 class
在 es2015/es6 之前,没有类的语法,只能用构造函数的方式模拟类,可读性非常差。
bad:
// 动物 const animal = function(age) { if (!(this instanceof animal)) { throw new error('instantiate animal with `new`'); } this.age = age; }; animal.prototype.move = function move() {}; // 哺乳动物 const mammal = function(age, furcolor) { if (!(this instanceof mammal)) { throw new error('instantiate mammal with `new`'); } animal.call(this, age); this.furcolor = furcolor; }; mammal.prototype = object.create(animal.prototype); mammal.prototype.constructor = mammal; mammal.prototype.livebirth = function livebirth() {}; // 人类 const human = function(age, furcolor, languagespoken) { if (!(this instanceof human)) { throw new error('instantiate human with `new`'); } mammal.call(this, age, furcolor); this.languagespoken = languagespoken; }; human.prototype = object.create(mammal.prototype); human.prototype.constructor = human; human.prototype.speak = function speak() {};
good:
// 动物 class animal { constructor(age) { this.age = age }; move() {}; } // 哺乳动物 class mammal extends animal{ constructor(age, furcolor) { super(age); this.furcolor = furcolor; }; livebirth() {}; } // 人类 class human extends mammal{ constructor(age, furcolor, languagespoken) { super(age, furcolor); this.languagespoken = languagespoken; }; speak() {}; }
链式调用
这种模式相当有用,可以在很多库中发现它的身影,比如 jquery、lodash 等。它让你的代码简洁优雅。实现起来也非常简单,在类的方法最后返回 this 可以了。
bad:
class car { 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); } } const car = new car('ford','f-150','red'); car.setcolor('pink'); car.save();
good:
class car { constructor(make, model, color) { this.make = make; this.model = model; this.color = color; } setmake(make) { this.make = make; return this; } setmodel(model) { this.model = model; return this; } setcolor(color) { this.color = color; return this; } save() { console.log(this.make, this.model, this.color); return this; } } const car = new car('ford','f-150','red') .setcolor('pink'); .save();
不要滥用继承
很多时候继承被滥用,导致可读性很差,要搞清楚两个类之间的关系,继承表达的一个属于关系,而不是包含关系,比如 human->animal vs. user->userdetails
bad:
class employee { constructor(name, email) { this.name = name; this.email = email; } // ... } // taxdata(税收信息)并不是属于 employee(雇员),而是包含关系。 class employeetaxdata extends employee { constructor(ssn, salary) { super(); this.ssn = ssn; this.salary = salary; } // ... }
good:
class employeetaxdata { constructor(ssn, salary) { this.ssn = ssn; this.salary = salary; } // ... } class employee { constructor(name, email) { this.name = name; this.email = email; } settaxdata(ssn, salary) { this.taxdata = new employeetaxdata(ssn, salary); } // ... }
solid
solid 是几个单词首字母组合而来,分别表示 单一功能原则
、开闭原则
、里氏替换原则
、接口隔离原则
以及依赖反转原则
。
单一功能原则
如果一个类干的事情太多太杂,会导致后期很难维护。我们应该厘清职责,各司其职减少相互之间依赖。
bad:
class usersettings { constructor(user) { this.user = user; } changesettings(settings) { if (this.verifycredentials()) { // ... } } verifycredentials() { // ... } }
good:
class userauth { constructor(user) { this.user = user; } verifycredentials() { // ... } } class usersetting { constructor(user) { this.user = user; this.auth = new userauth(this.user); } changesettings(settings) { if (this.auth.verifycredentials()) { // ... } } } }
开闭原则
“开”指的就是类、模块、函数都应该具有可扩展性,“闭”指的是它们不应该被修改。也就是说你可以新增功能但不能去修改源码。
bad:
class ajaxadapter extends adapter { constructor() { super(); this.name = 'ajaxadapter'; } } class nodeadapter extends adapter { constructor() { super(); this.name = 'nodeadapter'; } } class httprequester { constructor(adapter) { this.adapter = adapter; } fetch(url) { if (this.adapter.name === 'ajaxadapter') { return makeajaxcall(url).then((response) => { // 传递 response 并 return }); } else if (this.adapter.name === 'httpnodeadapter') { return makehttpcall(url).then((response) => { // 传递 response 并 return }); } } } function makeajaxcall(url) { // 处理 request 并 return promise } function makehttpcall(url) { // 处理 request 并 return promise }
good:
class ajaxadapter extends adapter { constructor() { super(); this.name = 'ajaxadapter'; } request(url) { // 处理 request 并 return promise } } class nodeadapter extends adapter { constructor() { super(); this.name = 'nodeadapter'; } request(url) { // 处理 request 并 return promise } } class httprequester { constructor(adapter) { this.adapter = adapter; } fetch(url) { return this.adapter.request(url).then((response) => { // 传递 response 并 return }); } }
里氏替换原则
名字很唬人,其实道理很简单,就是子类不要去重写父类的方法。
bad:
// 长方形 class rectangle { constructor() { this.width = 0; this.height = 0; } setcolor(color) { // ... } render(area) { // ... } setwidth(width) { this.width = width; } setheight(height) { this.height = height; } getarea() { return this.width * this.height; } } // 正方形 class square extends rectangle { setwidth(width) { this.width = width; this.height = width; } setheight(height) { this.width = height; this.height = height; } } function renderlargerectangles(rectangles) { rectangles.foreach((rectangle) => { rectangle.setwidth(4); rectangle.setheight(5); const area = rectangle.getarea(); rectangle.render(area); }); } const rectangles = [new rectangle(), new rectangle(), new square()]; renderlargerectangles(rectangles);
good:
class shape { setcolor(color) { // ... } render(area) { // ... } } class rectangle extends shape { constructor(width, height) { super(); this.width = width; this.height = height; } getarea() { return this.width * this.height; } } class square extends shape { constructor(length) { super(); this.length = length; } getarea() { return this.length * this.length; } } function renderlargeshapes(shapes) { shapes.foreach((shape) => { const area = shape.getarea(); shape.render(area); }); } const shapes = [new rectangle(4, 5), new rectangle(4, 5), new square(5)]; renderlargeshapes(shapes);
接口隔离原则
javascript 几乎没有接口的概念,所以这条原则很少被使用。官方定义是“客户端不应该依赖它不需要的接口”,也就是接口最小化,把接口解耦。
bad:
class domtraverser { constructor(settings) { this.settings = settings; this.setup(); } setup() { this.rootnode = this.settings.rootnode; this.animationmodule.setup(); } traverse() { // ... } } const $ = new domtraverser({ rootnode: document.getelementsbytagname('body'), animationmodule() {} // most of the time, we won't need to animate when traversing. // ... });
good:
class domtraverser { 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 $ = new domtraverser({ rootnode: document.getelementsbytagname('body'), options: { animationmodule() {} } });
依赖反转原则
说就两点:
- 高层次模块不能依赖低层次模块,它们依赖于抽象接口。
- 抽象接口不能依赖具体实现,具体实现依赖抽象接口。
总结下来就两个字,解耦。
bad:
// 库存查询 class inventoryrequester { constructor() { this.req_methods = ['http']; } requestitem(item) { // ... } } // 库存跟踪 class inventorytracker { constructor(items) { this.items = items; // 这里依赖一个特殊的请求类,其实我们只是需要一个请求方法。 this.requester = new inventoryrequester(); } requestitems() { this.items.foreach((item) => { this.requester.requestitem(item); }); } } const inventorytracker = new inventorytracker(['apples', 'bananas']); inventorytracker.requestitems();
good:
// 库存跟踪 class inventorytracker { constructor(items, requester) { this.items = items; this.requester = requester; } requestitems() { this.items.foreach((item) => { this.requester.requestitem(item); }); } } // http 请求 class inventoryrequesterhttp { constructor() { this.req_methods = ['http']; } requestitem(item) { // ... } } // websocket 请求 class inventoryrequesterws { constructor() { this.req_methods = ['ws']; } requestitem(item) { // ... } } // 通过依赖注入的方式将请求模块解耦,这样我们就可以很轻易的替换成 websocket 请求。 const inventorytracker = new inventorytracker(['apples', 'bananas'], new inventoryrequesterhttp()); inventorytracker.requestitems();
测试
随着项目变得越来越庞大,时间线拉长,有的老代码可能半年都没碰过,如果此时上线,你有信心这部分代码能正常工作吗?测试的覆盖率和你的信心是成正比的。
ps: 如果你发现你的代码很难被测试,那么你应该优化你的代码了。
单一化
bad:
import assert from 'assert'; describe('makemomentjsgreatagain', () => { it('handles date boundaries', () => { let date; date = new makemomentjsgreatagain('1/1/2015'); date.adddays(30); assert.equal('1/31/2015', date); date = new makemomentjsgreatagain('2/1/2016'); date.adddays(28); assert.equal('02/29/2016', date); date = new makemomentjsgreatagain('2/1/2015'); date.adddays(28); assert.equal('03/01/2015', date); }); });
good:
import assert from 'assert'; describe('makemomentjsgreatagain', () => { it('handles 30-day months', () => { const date = new makemomentjsgreatagain('1/1/2015'); date.adddays(30); assert.equal('1/31/2015', date); }); it('handles leap year', () => { const date = new makemomentjsgreatagain('2/1/2016'); date.adddays(28); assert.equal('02/29/2016', date); }); it('handles non-leap year', () => { const date = new makemomentjsgreatagain('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('file written'); } }); } });
good:
get('https://en.wikipedia.org/wiki/robert_cecil_martin') .then((response) => { return writefile('article.html', response); }) .then(() => { console.log('file written'); }) .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) => { return writefile('article.html', response); }) .then(() => { console.log('file written'); }) .catch((err) => { console.error(err); });
good:
import { get } from 'request-promise'; import { writefile } from 'fs-promise'; async function getcleancodearticle() { try { const response = await get('https://en.wikipedia.org/wiki/robert_cecil_martin'); await writefile('article.html', response); console.log('file written'); } 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:
const days_in_week = 7; const daysinmonth = 30; const songs = ['back in black', 'stairway to heaven', 'hey jude']; const artists = ['acdc', 'led zeppelin', 'the beatles']; function erasedatabase() {} function restore_database() {} class animal {} class alpaca {}
good:
const days_in_week = 7; const days_in_month = 30; const songs = ['back in black', 'stairway to heaven', 'hey jude']; const artists = ['acdc', 'led zeppelin', 'the beatles']; function erasedatabase() {} function restoredatabase() {} class animal {} class alpaca {}
先声明后调用
就像我们看报纸文章一样,从上到下看,所以为了方便阅读把函数声明写在函数调用前面。
bad:
class performancereview { constructor(employee) { this.employee = employee; } lookuppeers() { return db.lookup(this.employee, 'peers'); } lookupmanager() { return db.lookup(this.employee, 'manager'); } getpeerreviews() { const peers = this.lookuppeers(); // ... } perfreview() { this.getpeerreviews(); this.getmanagerreview(); this.getselfreview(); } getmanagerreview() { const manager = this.lookupmanager(); } getselfreview() { // ... } } const review = new performancereview(employee); review.perfreview();
good:
class performancereview { constructor(employee) { this.employee = employee; } perfreview() { this.getpeerreviews(); this.getmanagerreview(); this.getselfreview(); } getpeerreviews() { const peers = this.lookuppeers(); // ... } lookuppeers() { return db.lookup(this.employee, 'peers'); } getmanagerreview() { const manager = this.lookupmanager(); } lookupmanager() { return db.lookup(this.employee, 'manager'); } getselfreview() { // ... } } const review = new performancereview(employee); review.perfreview();
注释
只有业务逻辑需要注释
代码注释不是越多越好。
bad:
function hashit(data) { // 这是初始值 let hash = 0; // 数组的长度 const length = data.length; // 循环数组 for (let i = 0; i < length; i++) { // 获取字符代码 const char = data.charcodeat(i); // 修改 hash hash = ((hash << 5) - hash) + char; // 转换为32位整数 hash &= hash; } }
good:
function hashit(data) { let hash = 0; const length = data.length; for (let i = 0; i < length; i++) { const char = data.charcodeat(i); hash = ((hash << 5) - hash) + char; // 转换为32位整数 hash &= hash; } }
删掉注释的代码
git 存在的意义就是保存你的旧代码,所以注释的代码赶紧删掉吧。
bad:
dostuff(); // dootherstuff(); // dosomemorestuff(); // dosomuchstuff();
good:
dostuff();
不要记日记
记住你有 git!,git log 可以帮你干这事。
bad:
/** * 2016-12-20: 删除了 xxx * 2016-10-01: 改进了 xxx * 2016-02-03: 删除了第12行的类型检查 * 2015-03-14: 增加了一个合并的方法 */ function combine(a, b) { return a + b; }
good:
function combine(a, b) { return a + b; }
注释不需要高亮
注释高亮,并不能起到提示的作用,反而会干扰你阅读代码。
bad:
//////////////////////////////////////////////////////////////////////////////// // scope model instantiation //////////////////////////////////////////////////////////////////////////////// $scope.model = { menu: 'foo', nav: 'bar' }; //////////////////////////////////////////////////////////////////////////////// // action setup //////////////////////////////////////////////////////////////////////////////// const actions = function() { // ... };
good:
$scope.model = { menu: 'foo', nav: 'bar' }; const actions = function() { // ... };
翻译自 的 《》,本文对原文进行了一些修改。