Clean JavaScript
Introduction: 简介
很多开发者都会推崇
最后还需要强调的,好的代码、好的架构都是慢慢衍化而来,切不可操之过急。千里之行,始于足下,在前行的道路上势必会走很多的弯路、错误,但是只要不断调整方向总会回到正确的道路上。我们不能畏惧改变,也不能把他人的话完全奉为圭臬,无论多少年的老程序员也是会犯错的、
Variables : 变量
使用有意义的可发音的变量名
Bad:
const yyyymmdstr = moment().format("YYYY/MM/DD");
Good:
const yearMonthDay = moment().format("YYYY/MM/DD");
对相同类型的变量使用相同的关键字
Bad:
getUserInfo();
getClientData();
getCustomerRecord();
Good:
getUser();
使用可搜索的命名
在开发过程中,我们阅读代码的时间会远远超过编写代码的时间,因此保证代码的可读性与可搜索会非常重要。切记,没事不要坑自己。
Bad:
//525600到底啥意思?
for (const i = 0; i < 525600; i++) {
runCronJob();
}
Good:
// 声明为全局变量
const MINUTES_IN_A_YEAR = 525600;
for (const 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);
避免摸不着头脑的临时变量
在遍历或者
Bad:
const locations = ['Austin', 'New York', 'San Francisco'];
locations.forEach((l) => {
doStuff();
doSomeOtherStuff();
...
...
...
// Wait, what is `l` for again?
dispatch(l);
});
Good:
const locations = ['Austin', 'New York', 'San Francisco'];
locations.forEach((location) => {
doStuff();
doSomeOtherStuff();
...
...
...
dispatch(location);
});
避免添加不需要的内容
如果你的类名
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";
}
Short-circuiting 优于条件选择
Bad:
function createMicrobrewery(name) {
const breweryName;
if (name) {
breweryName = name;
} else {
breweryName = "Hipster Brew Co.";
}
}
Good:
function createMicrobrewery(name) {
const breweryName = name || "Hipster Brew Co.";
}
函数
函数参数最好不超过两个
限制函数的参数数目还是比较重要的,它能够方便对于函数的测试,避免需要进行不同的
Bad:
function createMenu(title, body, buttonText, cancellable) {
...
}
Good:
const 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...
});
}
移除重复代码
在任何情况下都不要去容许重复代码的存在。重复代码指那些修改单一逻辑时需要修改多个代码片的代码交集,
Bad:
function showDeveloperList(developers) {
developers.forEach((developers) => {
const expectedSalary = developer.calculateExpectedSalary();
const experience = developer.getExperience();
const githubLink = developer.getGithubLink();
const data = {
expectedSalary: expectedSalary,
experience: experience,
githubLink: githubLink,
};
render(data);
});
}
function showManagerList(managers) {
managers.forEach((manager) => {
const expectedSalary = manager.calculateExpectedSalary();
const experience = manager.getExperience();
const portfolio = manager.getMBAProjects();
const data = {
expectedSalary: expectedSalary,
experience: experience,
portfolio: portfolio,
};
render(data);
});
}
Good:
function showList(employees) {
employees.forEach(employee => {
const expectedSalary = employee.calculateExpectedSalary();
const experience = employee.getExperience();
const portfolio;
if (employee.type === "manager") {
portfolio = employee.getMBAProjects();
} else {
portfolio = employee.getGithubLink();
}
const 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') {
...
}
使用Object.assign 设置默认值
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",
// 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
有的开发者会使用
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:
// 定义全局变量
// 如果我们有其他的函数引用了该变量,那么我们就无法预测该变量类型
const name = "Ryan McDermott";
function splitIntoFirstAndLastName() {
name = name.split(" ");
}
splitIntoFirstAndLastName();
console.log(name); // ['Ryan', 'McDermott'];
Good:
function splitIntoFirstAndLastName(name) {
return name.split(" ");
}
const name = "Ryan McDermott";
const newName = splitIntoFirstAndLastName(name);
console.log(name); // 'Ryan McDermott';
console.log(newName); // ['Ryan', 'McDermott'];
避免污染全局函数
Array.prototype
,不过很有可能跟其他打算占用这个位置的库起冲突。我们更建议使用
Bad:
Array.prototype.diff = function (comparisonArray) {
const values = [];
const hash = {};
for (const i of comparisonArray) {
hash[i] = true;
}
for (const i of this) {
if (!hash[i]) {
values.push(i);
}
}
return values;
};
Good:
class SuperArray extends Array {
constructor(...args) {
super(...args);
}
diff(comparisonArray) {
const values = [];
const hash = {};
for (const i of comparisonArray) {
hash[i] = true;
}
for (const i of this) {
if (!hash[i]) {
values.push(i);
}
}
return values;
}
}
优先选择函数式编程而不是命令式编程
Bad:
const programmerOutput = [
{
name: "Uncle Bobby",
linesOfCode: 500,
},
{
name: "Suzie Q",
linesOfCode: 1500,
},
{
name: "Jimmy Gosling",
linesOfCode: 150,
},
{
name: "Gracie Hopper",
linesOfCode: 1000,
},
];
const totalOutput = 0;
for (const 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,
},
];
const 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();
}
}
避免依赖于类型检测
很多时候我们会依赖于
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"));
}
避免依赖于类型检测
如果你需要操作像字符串、数值、列表这样的基础数据类型,你就无法依赖于多态性来实现类型检测。那么建议是使用
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 (const i = 0, len = list.length; i < len; i++) {
// ...
}
Good:
for (const 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");
对象与数据结构
使用getters 与setters
在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:
const Employee = function (name) {
this.name = name;
};
Employee.prototype.getName = function () {
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:
const Employee = (function () {
function Employee(name) {
this.getName = function () {
return name;
};
}
return Employee;
})();
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: John Doe
类
单一职责原则
便如
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()) {
// ...
}
}
}
开放封闭原则
正如
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);
}
}
里氏替换原则
这个原则听起来有点拗口,不过概念却很好理解。其形式化描述为如果
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);
接口隔离原则
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 depend 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 an InventoryTracker
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();
优先选择ES6 类而不是ES5 的基本函数定义
传统
Bad:
const Animal = function (age) {
if (!(this instanceof Animal)) {
throw new Error("Instantiate Animal with `new`");
}
this.age = age;
};
Animal.prototype.move = function () {};
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 () {};
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 () {};
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() {}
}
Use method chaining
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();
Prefer composition over inheritance
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” relationship (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;
}
// ...
}
测试
测试是代码部署前不可避免的重要步骤,如果你没有添加任何的测试,那么你在每次部署之前你压根不敢确定是否会产生什么意外情况。不同的团队对于测试覆盖率的需求不太一致,不过保持
每个测试用例单一目标
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);
});
});
并发
使用Promise 替代回调
回调含义不清晰,还会导致过深的代码嵌套,就是所谓的回调地狱。在
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);
});
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 {
const request = await require("request-promise");
const response = await request.get(
"https://en.wikipedia.org/wiki/Robert_Cecil_Martin"
);
const fileHandle = await require("fs-promise");
await fileHandle.writeFile("article.html", response);
console.log("File written");
} catch (err) {
console.log(err);
}
}
格式化
就像本文的很多建议一样,格式化本身是非常主观的原则。建议是使用工具 来自动完成格式化操作,而不是争论具体的格式化的细节。
相似含义变量的大写一致性
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");
}
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
const hash = 0;
// Length of string
const length = data.length;
// Loop through every character in data
for (const i = 0; i < length; i++) {
// Get character code.
const char = data.charCodeAt(i);
// Make the hash
hash = (hash << 5) - hash + char;
// Convert to 32-bit integer
hash = hash & hash;
}
}
Good:
function hashIt(data) {
const hash = 0;
const length = data.length;
for (const i = 0; i < length; i++) {
const 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() {
// ...
}
错误处理
在
不要忽略被捕获的错误
如果我们只是简单地捕获错误而没有将其反馈给相对应的开发人员或者将该错误记录下来,那么我们进行错误处理就没有什么意义。
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!
}
不要忽略被拒绝的Promises
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!
});
延伸阅读
知乎专栏:某熊的全栈之路 >知乎专栏:前端当自强 >知乎专栏:
lotuc 的编程之路 >2016- 我的技术之路: 编程知识体系结构 >2016- 我的前端之路: 工具化与工程化 >某熊周刊系列: 一周推荐外文技术资料(12.1)