Clean JavaScript:写出整洁的JavaScript代码翻译自clean-code-javascript。本文从属于笔者的Web 前端入门与工程实践。javascript
不少开发者都会推崇Robert C. Martin的Clean Code一书中说起的软件工程准则,本文就是对于这些准则在JavaScript开发领域中的实践应用总结。本文并不只仅是样式指南,而是对于如何编写出基于JavaScript实现的高可读性、高可用性以及可重构的软件系统。虽然本文对比的讲了不少好坏的实践,但并非说本文就建议你们强制遵循全部的指南。实际上对于Clean Code的概念不一样的团队、不一样的开发者都会有不一样的看法与见解,本文的不少观点也是充满争议。软件工程已经走过了五十多个年头,而咱们也一直在前行,很难说有什么原则是永恒正确的。做者更但愿这些指南与考量起到试金石的做用,成为评判团队JavaScript代码质量的考量标准之一。html
最后还须要强调的,好的代码、好的架构都是慢慢衍化而来,切不可操之过急。千里之行,始于足下,在前行的道路上势必会走不少的弯路、错误,可是只要不断调整方向总会回到正确的道路上。咱们不能畏惧改变,也不能把他人的话彻底奉为圭臬,不管多少年的老程序员也是会犯错的、前端
Bad:java
var yyyymmdstr = moment().format('YYYY/MM/DD');
Good:node
var yearMonthDay = moment().format('YYYY/MM/DD');
Bad:git
getUserInfo(); getClientData(); getCustomerRecord();
Good:程序员
getUser();
在开发过程当中,咱们阅读代码的时间会远远超过编写代码的时间,所以保证代码的可读性与可搜索会很是重要。切记,没事不要坑本身。github
Bad:express
//525600到底啥意思? for (var i = 0; i < 525600; i++) { runCronJob(); }
Good:编程
// 声明为全局变量 var MINUTES_IN_A_YEAR = 525600; for (var i = 0; i < MINUTES_IN_A_YEAR; i++) { runCronJob(); }
Bad:
let cityStateRegex = /^(.+)[,\\s]+(.+?)\s*(\d{5})?$/; saveCityState(cityStateRegex.match(cityStateRegex)[1], cityStateRegex.match(cityStateRegex)[2]);
Good:
let cityStateRegex = /^(.+)[,\\s]+(.+?)\s*(\d{5})?$/; let match = cityStateRegex.match(cityStateRegex) let city = match[1]; let state = match[2]; saveCityState(city, state);
在遍历或者mapping过程当中,须要避免短小无心义的变量命名。
Bad:
var locations = ['Austin', 'New York', 'San Francisco']; locations.forEach((l) => { doStuff(); doSomeOtherStuff(); ... ... ... // Wait, what is `l` for again? dispatch(l); });
Good:
var locations = ['Austin', 'New York', 'San Francisco']; locations.forEach((location) => { doStuff(); doSomeOtherStuff(); ... ... ... dispatch(location); });
若是你的类名/实例名已经可以表述某些信息,那么在类/实例的属性中就不须要重复命名。
Bad:
var Car = { carMake: 'Honda', carModel: 'Accord', carColor: 'Blue' }; function paintCar(car) { car.carColor = 'Red'; }
Good:
var Car = { make: 'Honda', model: 'Accord', color: 'Blue' }; function paintCar(car) { car.color = 'Red'; }
Bad:
function createMicrobrewery(name) { var breweryName; if (name) { breweryName = name; } else { breweryName = 'Hipster Brew Co.'; } }
Good:
function createMicrobrewery(name) { var breweryName = name || 'Hipster Brew Co.' }
限制函数的参数数目仍是比较重要的,它可以方便对于函数的测试,避免须要进行不一样的Case测试时把代码变得一团糟。咱们应该尽量地控制参数数目小于或等于两个,若是你的参数数目多于两个,那么建议使用高阶对象进行适当封装。
Bad:
function createMenu(title, body, buttonText, cancellable) { ... }
Good:
var menuConfig = { title: 'Foo', body: 'Bar', buttonText: 'Baz', cancellable: true } function createMenu(menuConfig) { ... }
这一条算是迄今为止软件工程中最重要的原则之一了。若是咱们给单一函数赋予了过多的职责,那么其很难被用于组合、测试等。而若是你保证函数的单一职责性质,那么相对其重构难度、代码可读性也会更好。
Bad:
function emailClients(clients) { clients.forEach(client => { let clientRecord = database.lookup(client); if (clientRecord.isActive()) { email(client); } }); }
Good:
function emailClients(clients) { clients.forEach(client => { emailClientIfNeeded(client); }); } function emailClientIfNeeded(client) { if (isClientActive(client)) { email(client); } } function isClientActive(client) { let clientRecord = database.lookup(client); return clientRecord.isActive(); }
Bad:
function dateAdd(date, month) { // ... } let date = new Date(); // 很难从函数名中获知该函数究竟是谁加上谁 dateAdd(date, 1);
Good:
function dateAddMonth(date, month) { // ... } let date = new Date(); dateAddMonth(date, 1);
这一条相似于单一职责原则,不过更倾向于关注函数的抽象程度,若是咱们在单一函数中添加了过多的抽象层,一样会下降的函数可读性、增长重构难度。
Bad:
function parseBetterJSAlternative(code) { let REGEXES = [ // ... ]; let statements = code.split(' '); let tokens; REGEXES.forEach((REGEX) => { statements.forEach((statement) => { // ... }) }); let ast; tokens.forEach((token) => { // lex... }); ast.forEach((node) => { // parse... }) }
Good:
function tokenize(code) { let REGEXES = [ // ... ]; let statements = code.split(' '); let tokens; REGEXES.forEach((REGEX) => { statements.forEach((statement) => { // ... }) }); return tokens; } function lexer(tokens) { let ast; tokens.forEach((token) => { // lex... }); return ast; } function parseBetterJSAlternative(code) { let tokens = tokenize(code); let ast = lexer(tokens); ast.forEach((node) => { // parse... }) }
在任何状况下都不要去允许重复代码的存在。重复代码指那些修改单一逻辑时须要修改多个代码片的代码交集,JavaScript自己是弱类型语言,相对而言编写泛型函数会更加容易。
Bad:
function showDeveloperList(developers) { developers.forEach(developers => { var expectedSalary = developer.calculateExpectedSalary(); var experience = developer.getExperience(); var githubLink = developer.getGithubLink(); var data = { expectedSalary: expectedSalary, experience: experience, githubLink: githubLink }; render(data); }); } function showManagerList(managers) { managers.forEach(manager => { var expectedSalary = manager.calculateExpectedSalary(); var experience = manager.getExperience(); var portfolio = manager.getMBAProjects(); var data = { expectedSalary: expectedSalary, experience: experience, portfolio: portfolio }; render(data); }); }
Good:
function showList(employees) { employees.forEach(employee => { var expectedSalary = employee.calculateExpectedSalary(); var experience = employee.getExperience(); var portfolio; if (employee.type === 'manager') { portfolio = employee.getMBAProjects(); } else { portfolio = employee.getGithubLink(); } var data = { expectedSalary: expectedSalary, experience: experience, portfolio: portfolio }; render(data); }); }
Bad:
function writeForumComment(subject, body) { subject = subject || 'No Subject'; body = body || 'No text'; }
Good:
function writeForumComment(subject = 'No subject', body = 'No text') { ... }
Bad:
var 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:
var menuConfig = { title: 'Order', // User did not include 'body' key buttonText: 'Send', cancellable: true } function createMenu(config) { config = Object.assign({ title: 'Foo', body: 'Bar', buttonText: 'Baz', cancellable: true }, config); // config now equals: {title: "Foo", body: "Bar", buttonText: "Baz", cancellable: true} // ... } createMenu(menuConfig);
有的开发者会使用Flags来控制函数执行不一样的逻辑流,不过就如咱们在上文中说起的单一职责原则,咱们应当将函数拆分为不一样的部分,而后在外层调用上根据Flags调用不一样的函数。
Bad:
function createFile(name, temp) { if (temp) { fs.create('./temp/' + name); } else { fs.create(name); } }
Good:
function createTempFile(name) { fs.create('./temp/' + name); } function createFile(name) { fs.create(name); }
若是某个函数除了接收输入值与返回值以外还作了其余事,那么就称其具备反作用。典型的反作用譬如写文件、修改某些全局变量、修改内存参数等等。在编程中咱们不可避免的须要产生反作用,譬如上面例子中咱们须要写入到某个外部文件。而你应当作的就是将全部的写文件操做由某个服务统一处理,而不该该将写文件的操做分散到数个类或者函数中。这一点最大的优点在于避免了不一样对象之间共享状态,共享的可变状态但是万恶之源啊。
Bad:
// 定义全局变量 // 若是咱们有其余的函数引用了该变量,那么咱们就没法预测该变量类型 var name = 'Ryan McDermott'; function splitIntoFirstAndLastName() { name = name.split(' '); } splitIntoFirstAndLastName(); console.log(name); // ['Ryan', 'McDermott'];
Good:
function splitIntoFirstAndLastName(name) { return name.split(' '); } var name = 'Ryan McDermott' var newName = splitIntoFirstAndLastName(name); console.log(name); // 'Ryan McDermott'; console.log(newName); // ['Ryan', 'McDermott'];
JavaScript中有个不太好的实践就是修改某个全局函数,将其指向其余的库或者自定义函数,不过这个会对某个懵懂的用户形成困恼。若是你想给JavaScript原生的Array添加一个diff函数支持,来展现两个数组的差别。你能够选择将函数挂载到Array.prototype
,不过颇有可能跟其余打算占用这个位置的库起冲突。咱们更建议使用ES6的classes,而且使用继承方式去添加新的功能函数。
Bad:
Array.prototype.diff = function(comparisonArray) { var values = []; var hash = {}; for (var i of comparisonArray) { hash[i] = true; } for (var i of this) { if (!hash[i]) { values.push(i); } } return values; }
Good:
class SuperArray extends Array { constructor(...args) { super(...args); } diff(comparisonArray) { var values = []; var hash = {}; for (var i of comparisonArray) { hash[i] = true; } for (var i of this) { if (!hash[i]) { values.push(i); } } return values; } }
JavaScript并不像Haskell这样纯粹的函数式编程语言,不过其对于实践函数式编程的理念仍是很推崇的。函数式编程可读性更好,也更易于测试。
Bad:
const programmerOutput = [ { name: 'Uncle Bobby', linesOfCode: 500 }, { name: 'Suzie Q', linesOfCode: 1500 }, { name: 'Jimmy Gosling', linesOfCode: 150 }, { name: 'Gracie Hopper', linesOfCode: 1000 } ]; var totalOutput = 0; for (var 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 } ]; var totalOutput = programmerOutput .map((programmer) => programmer.linesOfCode) .reduce((acc, linesOfCode) => acc + linesOfCode, 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)) { // ... }
不少人第一次听到这个概念都会以为难以想象,没有if
条件选择语句的话又该如何编程呢?在这里咱们推荐使用多态性来达成这一目标,由于若是在函数或类中嵌入过多的if
语句,会致使该函数或者类破坏单一职责原则。
Bad:
class Airplane { //... getCruisingAltitude() { switch (this.type) { case '777': return getMaxAltitude() - getPassengerCount(); case 'Air Force One': return getMaxAltitude(); case 'Cesna': return getMaxAltitude() - getFuelExpenditure(); } } }
Good:
class Airplane { //... } class Boeing777 extends Airplane { //... getCruisingAltitude() { return getMaxAltitude() - getPassengerCount(); } } class AirForceOne extends Airplane { //... getCruisingAltitude() { return getMaxAltitude(); } } class Cesna extends Airplane { //... getCruisingAltitude() { return getMaxAltitude() - getFuelExpenditure(); } }
不少时候咱们会依赖于JavaScript输入的参数类型来进入不一样的控制流,不过鉴于JavaScript自己是弱类型语言,咱们仍是应该避免这种实践。第一个方法就是使用较为一致性的接口。
Bad:
function travelToTexas(vehicle) { if (vehicle instanceof Bicycle) { vehicle.peddle(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,它为普通的JavaScript添加了静态类型支持。
Bad:
function combine(val1, val2) { if (typeof val1 == "number" && typeof val2 == "number" || typeof val1 == "string" && typeof val2 == "string") { return val1 + val2; } else { throw new Error('Must be of type String or Number'); } }
Good:
function combine(val1, val2) { return val1 + val2; }
现代浏览器已经在运行时作了不少的优化,所以不少时候若是咱们要遵循那些流传已久的优化策略不过是浪费时间。能够参考这个来获取建议的优化要点。
Bad:
// On old browsers, each iteration would be costly because `len` would be // recomputed. In modern browsers, this is optimized. for (var i = 0, len = list.length; i < len; i++) { // ... }
Good:
for (var i = 0; i < list.length; i++) { // ... }
弃用的代码就和重复的代码同样,咱们没有任何理由保留他们。不过为防万一建议不要完全从Git的历史记录中删除它们。
Bad:
function oldRequestModule(url) { // ... } function newRequestModule(url) { // ... } var req = newRequestModule; inventoryTracker('apples', req, 'www.inventory-awesome.io');
Good:
function newRequestModule(url) { // ... } var req = newRequestModule; inventoryTracker('apples', req, 'www.inventory-awesome.io');
在JavaScript的对象属性读写中,建议使用getter或者setter,而不是直接读取或者赋值。不过JavaScript并无相似于public或者private这样的关键字,所以很难经过接口方式进行强限制。不过鉴于如下优点咱们仍是强烈建议使用getter或者setter:
1.若是你打算不只仅是直接获取原始值,使用getter可以避免去修改每一个取值的地方。
2.使用set
可以方便地添加校验。
封装内部表述。
便于添加日志与错误处理。
经过继承可以复写默认功能。
支持属性懒加载。
Bad:
class BankAccount { constructor() { this.balance = 1000; } } let bankAccount = new BankAccount(); // Buy shoes... bankAccount.balance = bankAccount.balance - 100;
Good:
class BankAccount { constructor() { this.balance = 1000; } // It doesn't have to be prefixed with `get` or `set` to be a getter/setter withdraw(amount) { if (verifyAmountCanBeDeducted(amount)) { this.balance -= amount; } } } let bankAccount = new BankAccount(); // Buy shoes... bankAccount.withdraw(100);
能够经过闭包方式添加私有属性:
Bad:
var Employee = function(name) { this.name = name; } Employee.prototype.getName = function() { return this.name; } var 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:
var Employee = (function() { function Employee(name) { this.getName = function() { return name; }; } return Employee; }()); var 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: John Doe
便如Clean Code中所述,不该该为了多个理由去更改某个类的代码,这样会把某个类塞入过多的功能。最小化你须要去改变某个类的次数对于保证代码的稳定性相当重要,过多的改变代码会影响代码库中依赖于该类的其余模块。
Bad:
class UserSettings { constructor(user) { this.user = user; } changeSettings(settings) { if (this.verifyCredentials(user)) { // ... } } verifyCredentials(user) { // ... } }
Good:
class UserAuth { constructor(user) { this.user = user; } verifyCredentials() { // ... } } class UserSettings { constructor(user) { this.user = user; this.auth = new UserAuth(user) } changeSettings(settings) { if (this.auth.verifyCredentials()) { // ... } } }
正如Bertrand Meyer所述,譬如类、模块、函数这样的实体应该面向扩展开放,而拒绝修改。换言之,咱们推荐去继承扩展某个函数或模块,而不是每次都去修改源代码。
Bad:
class AjaxRequester { constructor() { // What if we wanted another HTTP Method, like DELETE? We would have to // open this file up and modify this and put it in manually. this.HTTP_METHODS = ['POST', 'PUT', 'GET']; } get(url) { // ... } }
Good:
class AjaxRequester { constructor() { this.HTTP_METHODS = ['POST', 'PUT', 'GET']; } get(url) { // ... } addHTTPMethod(method) { this.HTTP_METHODS.push(method); } }
这个原则听起来有点拗口,不过概念却很好理解。其形式化描述为若是S为T的子类型,那么类型T的实例能够被类型S的实例替换而不须要修改任何的代码。形象而言,咱们建立的父类与其子类应当可交换地使用而不会引发异常,譬以下文的Square-Rectangle这个例子。Square也是Rectangle:
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 { constructor() { super(); } 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); let area = rectangle.getArea(); // BAD: Will return 25 for Square. Should be 20. rectangle.render(area); }) } let rectangles = [new Rectangle(), new Rectangle(), new Square()]; renderLargeRectangles(rectangles);
Good:
class Shape { constructor() {} setColor(color) { // ... } render(area) { // ... } } class Rectangle extends Shape { constructor() { super(); this.width = 0; this.height = 0; } setWidth(width) { this.width = width; } setHeight(height) { this.height = height; } getArea() { return this.width * this.height; } } class Square extends Shape { constructor() { super(); this.length = 0; } setLength(length) { this.length = length; } getArea() { return this.length * this.length; } } function renderLargeShapes(shapes) { shapes.forEach((shape) => { switch (shape.constructor.name) { case 'Square': shape.setLength(5); case 'Rectangle': shape.setWidth(4); shape.setHeight(5); } let area = shape.getArea(); shape.render(area); }) } let shapes = [new Rectangle(), new Rectangle(), new Square()]; renderLargeShapes(shapes);
JavaScript自己并不包含对于接口语法的支持,所以也没法像其余语言那样达到严格限制的程度。不过鉴于JavaScript自己类型系统的缺失,遵循接口隔离原则仍是蛮重要的。ISP的表述为不该该强制客户端去依赖于他们不须要的接口,这一点在JavaScript中较为典型的例子就是那些须要大量配置信息的对象。其实使用者并不须要去关心每个配置项,容许他们动态的设置可以节省大量的时间,代码的可读性也会更好。
Bad:
class DOMTraverser { constructor(settings) { this.settings = settings; this.setup(); } setup() { this.rootNode = this.settings.rootNode; this.animationModule.setup(); } traverse() { // ... } } let $ = new DOMTraverser({ rootNode: document.getElementsByTagName('body'), animationModule: function() {} // 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() { // ... } } let $ = new DOMTraverser({ rootNode: document.getElementsByTagName('body'), options: { animationModule: function() {} } });
This principle states two essential things:
High-level modules should not depend on low-level modules. Both should
on abstractions.
Abstractions should not depend upon details. Details should depend on
abstractions.
This can be hard to understand at first, but if you've worked with Angular.js,
you've seen an implementation of this principle in the form of Dependency
Injection (DI). While they are not identical concepts, DIP keeps high-level
modules from knowing the details of its low-level modules and setting them up.
It can accomplish this through DI. A huge benefit of this is that it reduces
the coupling between modules. Coupling is a very bad development pattern because
it makes your code hard to refactor.
As stated previously, JavaScript doesn't have interfaces so the abstractions
that are depended upon are implicit contracts. That is to say, the methods
and properties that an object/class exposes to another object/class. In the
example below, the implicit contract is that any Request module for anInventoryTracker
will have a requestItems
method.
Bad:
class InventoryTracker { constructor(items) { this.items = items; // BAD: We have created a dependency on a specific request implementation. // We should just have requestItems depend on a request method: `request` this.requester = new InventoryRequester(); } requestItems() { this.items.forEach((item) => { this.requester.requestItem(item); }); } } class InventoryRequester { constructor() { this.REQ_METHODS = ['HTTP']; } requestItem(item) { // ... } } let 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); }); } } class InventoryRequesterV1 { constructor() { this.REQ_METHODS = ['HTTP']; } requestItem(item) { // ... } } class InventoryRequesterV2 { constructor() { this.REQ_METHODS = ['WS']; } requestItem(item) { // ... } } // By constructing our dependencies externally and injecting them, we can easily // substitute our request module for a fancy new one that uses WebSockets. let inventoryTracker = new InventoryTracker(['apples', 'bananas'], new InventoryRequesterV2()); inventoryTracker.requestItems();
传统ES5的类实现语法对于类的继承、构建以及方法定义的可读性都不是很好。若是你考虑在类中实现继承,那么建议优先考虑ES6的类语法糖。若是你只是须要构建简单的对象,那么能够考虑使用ES5的基本函数定义来构造类对象。
Bad:
var Animal = function(age) { if (!(this instanceof Animal)) { throw new Error("Instantiate Animal with `new`"); } this.age = age; }; Animal.prototype.move = function() {}; var 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() {}; var 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() {};
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() {} }
Against the advice of Clean Code, this is one place where we will have to differ.
It has been argued that method chaining is unclean and violates the Law of Demeter.
Maybe it's true, but this pattern is very useful in JavaScript and you see it in
many libraries such as jQuery and Lodash. It allows your code to be expressive,
and less verbose. For that reason, I say, use method chaining and take a look at
how clean your code will be. In your class functions, simply return this
at
the end of every function, and you can chain further class methods onto it.
Bad:
class Car { constructor() { this.make = 'Honda'; this.model = 'Accord'; this.color = 'white'; } setMake(make) { this.name = name; } setModel(model) { this.model = model; } setColor(color) { this.color = color; } save() { console.log(this.make, this.model, this.color); } } let car = new Car(); car.setColor('pink'); car.setMake('Ford'); car.setModel('F-150') car.save();
Good:
class Car { constructor() { this.make = 'Honda'; this.model = 'Accord'; this.color = 'white'; } setMake(make) { this.name = name; // NOTE: Returning this for chaining return this; } setModel(model) { this.model = model; // NOTE: Returning this for chaining return this; } setColor(color) { this.color = color; // NOTE: Returning this for chaining return this; } save() { console.log(this.make, this.model, this.color); } } let car = new Car() .setColor('pink') .setMake('Ford') .setModel('F-150') .save();
As stated famously in the Gang of Four,
you should prefer composition over inheritance where you can. There are lots of
good reasons to use inheritance and lots of good reasons to use composition.
The main point for this maxim is that if your mind instinctively goes for
inheritance, try to think if composition could model your problem better. In some
cases it can.
You might be wondering then, "when should I use inheritance?" It
depends on your problem at hand, but this is a decent list of when inheritance
makes more sense than composition:
Your inheritance represents an "is-a" relationship and not a "has-a"
(Animal->Human vs. User->UserDetails).
You can reuse code from the base classes (Humans can move like all animals).
You want to make global changes to derived classes by changing a base class.
(Change the caloric expenditure of all animals when they move).
Bad:
class Employee { constructor(name, email) { this.name = name; this.email = email; } // ... } // Bad because Employees "have" tax data. EmployeeTaxData is not a type of Employee class EmployeeTaxData extends Employee { constructor(ssn, salary) { super(); this.ssn = ssn; this.salary = salary; } // ... }
Good:
class Employee { constructor(name, email) { this.name = name; this.email = email; } setTaxData(ssn, salary) { this.taxData = new EmployeeTaxData(ssn, salary); } // ... } class EmployeeTaxData { constructor(ssn, salary) { this.ssn = ssn; this.salary = salary; } // ... }
测试是代码部署前不可避免的重要步骤,若是你没有添加任何的测试,那么你在每次部署以前你压根不敢肯定是否会产生什么意外状况。不一样的团队对于测试覆盖率的需求不太一致,不过保持100%的覆盖率可以让你的团队对于代码保持较好的掌控与信赖。咱们可使用不少优秀的测试工具与测试覆盖率检测工具,建议是对于每一个新的特征或者模块都添加测试用例。若是更倾向于使用测试驱动开发,必定要注意在你打算添加新的特性或者重构当前代码以前保证测试覆盖率已经达到了预期。
Bad:
const assert = require('assert'); describe('MakeMomentJSGreatAgain', function() { it('handles date boundaries', function() { let date; date = new MakeMomentJSGreatAgain('1/1/2015'); date.addDays(30); date.shouldEqual('1/31/2015'); 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:
const assert = require('assert'); describe('MakeMomentJSGreatAgain', function() { it('handles 30-day months', function() { let date = new MakeMomentJSGreatAgain('1/1/2015'); date.addDays(30); date.shouldEqual('1/31/2015'); }); it('handles leap year', function() { let date = new MakeMomentJSGreatAgain('2/1/2016'); date.addDays(28); assert.equal('02/29/2016', date); }); it('handles non-leap year', function() { let date = new MakeMomentJSGreatAgain('2/1/2015'); date.addDays(28); assert.equal('03/01/2015', date); }); });
回调含义不清晰,还会致使过深的代码嵌套,就是所谓的回调地狱。在ES6中,Promises已是内置的全局类型。
Bad:
require('request').get('https://en.wikipedia.org/wiki/Robert_Cecil_Martin', function(err, response) { if (err) { console.error(err); } else { require('fs').writeFile('article.html', response.body, function(err) { if (err) { console.error(err); } else { console.log('File written'); } }) } })
Good:
require('request-promise').get('https://en.wikipedia.org/wiki/Robert_Cecil_Martin') .then(function(response) { return require('fs-promise').writeFile('article.html', response); }) .then(function() { console.log('File written'); }) .catch(function(err) { console.log(err); })
Promises自己已是对于回调的不错的替代,而ES7中的async与await则是更为清晰的解决方案,能够避免你编写大量的then
调用链。
Bad:
require('request-promise').get('https://en.wikipedia.org/wiki/Robert_Cecil_Martin') .then(function(response) { return require('fs-promise').writeFile('article.html', response); }) .then(function() { console.log('File written'); }) .catch(function(err) { console.log(err); })
Good:
async function getCleanCodeArticle() { try { var request = await require('request-promise') var response = await request.get('https://en.wikipedia.org/wiki/Robert_Cecil_Martin'); var fileHandle = await require('fs-promise'); await fileHandle.writeFile('article.html', response); console.log('File written'); } catch(err) { console.log(err); } }
就像本文的不少建议同样,格式化自己是很是主观的原则。建议是使用工具 来自动完成格式化操做,而不是争论具体的格式化的细节。
JavaScript自己是无类型的,所以变量名大写也能传递不少有用的信息。这个规则算是比较主观的,建议团队能够根据本身的内部规范将相同含义变量的大小写保持一致性。
Bad:
var DAYS_IN_WEEK = 7; var daysInMonth = 30; var songs = ['Back In Black', 'Stairway to Heaven', 'Hey Jude']; var Artists = ['ACDC', 'Led Zeppelin', 'The Beatles']; function eraseDatabase() {} function restore_database() {} class animal {} class Alpaca {}
Good:
var DAYS_IN_WEEK = 7; var DAYS_IN_MONTH = 30; var songs = ['Back In Black', 'Stairway to Heaven', 'Hey Jude']; var 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'); } lookupMananger() { return db.lookup(this.employee, 'manager'); } getPeerReviews() { let peers = this.lookupPeers(); // ... } perfReview() { getPeerReviews(); getManagerReview(); getSelfReview(); } getManagerReview() { let manager = this.lookupManager(); } getSelfReview() { // ... } } let review = new PerformanceReview(user); review.perfReview();
Good:
class PerformanceReview { constructor(employee) { this.employee = employee; } perfReview() { getPeerReviews(); getManagerReview(); getSelfReview(); } getPeerReviews() { let peers = this.lookupPeers(); // ... } lookupPeers() { return db.lookup(this.employee, 'peers'); } getManagerReview() { let manager = this.lookupManager(); } lookupMananger() { return db.lookup(this.employee, 'manager'); } getSelfReview() { // ... } } let review = new PerformanceReview(employee); review.perfReview();
好的代码应该是见名知义,注释更多的是对于业务逻辑的描述说明。
Bad:
function hashIt(data) { // The hash var hash = 0; // Length of string var length = data.length; // Loop through every character in data for (var i = 0; i < length; i++) { // Get character code. var char = data.charCodeAt(i); // Make the hash hash = ((hash << 5) - hash) + char; // Convert to 32-bit integer hash = hash & hash; } }
Good:
function hashIt(data) { var hash = 0; var length = data.length; for (var i = 0; i < length; i++) { var char = data.charCodeAt(i); hash = ((hash << 5) - hash) + char; // Convert to 32-bit integer hash = hash & hash; } }
Bad:
doStuff(); // doOtherStuff(); // doSomeMoreStuff(); // doSoMuchStuff();
Good:
doStuff();
千万记住,要使用版本控制工具,而不是在你的代码前面添加日记形式的注释,使用git log
查看历史记录。
Bad:
/** * 2016-12-20: Removed monads, didn't understand them (RM) * 2016-10-01: Improved using special monads (JP) * 2016-02-03: Removed type-checking (LI) * 2015-03-14: Added combine with type-checking (JR) */ function combine(a, b) { return a + b; }
Good:
function combine(a, b) { return a + b; }
建议是让函数与变量名来表述其功能,避免添加过多额外的注释。
Bad:
//////////////////////////////////////////////////////////////////////////////// // Scope Model Instantiation //////////////////////////////////////////////////////////////////////////////// let $scope.model = { menu: 'foo', nav: 'bar' }; //////////////////////////////////////////////////////////////////////////////// // Action setup //////////////////////////////////////////////////////////////////////////////// let actions = function() { // ... }
Good:
let $scope.model = { menu: 'foo', nav: 'bar' }; let actions = function() { // ... }
Bad:
/* The MIT License (MIT) Copyright (c) 2016 Ryan McDermott Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE */ function calculateBill() { // ... }
Good:
function calculateBill() { // ... }
在JavaScript中抛出错误是个不错的实践,不只能够帮助开发者即时感知程序中出现的错误,还能马上终止程序执行而且打印出其调用栈。
若是咱们只是简单地捕获错误而没有将其反馈给相对应的开发人员或者将该错误记录下来,那么咱们进行错误处理就没有什么意义。
Bad:
try { functionThatMightThrow(); } catch (error) { console.log(error); }
Good:
try { functionThatMightThrow(); } catch (error) { // One option (more noisy than console.log): console.error(error); // Another option: notifyUserOfError(error); // Another option: reportErrorToService(error); // OR do all three! }
Bad:
getdata() .then(data => { functionThatMightThrow(data); }) .catch(error => { console.log(error); });
Good:
getdata() .then(data => { functionThatMightThrow(data); }) .catch(error => { // One option (more noisy than console.log): console.error(error); // Another option: notifyUserOfError(error); // Another option: reportErrorToService(error); // OR do all three! });
延伸阅读