代码简洁之道(译)
变量
使用有意义且表达清晰的变量
Bad:
const yyyymmdstr = moment().format("YYYY/MM/DD");
Good:
const currentDate = moment().format("YYYY/MM/DD");
相同类型的变量使用相同的词
Bad:
getUserInfo();
getClientData();
getCustomerRecord();
Good:
getUser();
使用可搜索/定位的名称
Bad:
// What the heck is 86400000 for?
setTimeout(blastOff, 86400000);
Good:
// Declare them as capitalized named constants.
const MILLISECONDS_PER_DAY = 60 * 60 * 24 * 1000; //86400000;
setTimeout(blastOff, MILLISECONDS_PER_DAY);
使用解释性变量
Bad:
const address = "One Infinite Loop, Cupertino 95014";
const cityZipCodeRegex = /^[^,\\]+[,\\\s]+(.+?)\s*(\d{5})?$/;
saveCityZipCode(
address.match(cityZipCodeRegex)[1],
address.match(cityZipCodeRegex)[2]
);
Good:
const address = "One Infinite Loop, Cupertino 95014";
const cityZipCodeRegex = /^[^,\\]+[,\\\s]+(.+?)\s*(\d{5})?$/;
const [_, city, zipCode] = address.match(cityZipCodeRegex) || [];
saveCityZipCode(city, zipCode);
避免隐式映射
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, color) {
car.carColor = color;
}
Good:
const Car = {
make: "Honda",
model: "Accord",
color: "Blue"
};
function paintCar(car, color) {
car.color = color;
}
使用默认参数,而不是判断或条件
默认参数通常更简洁。请注意,如果使用它们,函数只会为未定义的参数提供默认值。其他“假”值,如“,”,false, null, 0和NaN,将不会被默认值替换。
Bad:
function createMicrobrewery(name) {
const breweryName = name || "Hipster Brew Co.";
// ...
}
Good:
function createMicrobrewery(name = "Hipster Brew Co.") {
// ...
}
函数
函数参数(理想情况下2个或更少)
限制函数参数的数量是非常重要的,因为这样可以更容易地测试函数。超过三个会导致组合爆炸,你必须用每个单独的参数测试大量不同的情况。
通常,如果你有两个以上的参数,那么你的函数尝试做太多。大多数情况下,一个更高级的对象作为参数就足够了。
Bad:
function createMenu(title, body, buttonText, cancellable) {
// ...
}
createMenu("Foo", "Bar", "Baz", true);
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();
// It's hard to tell from the function name what is addedaddToDate(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 syntaxTree = parse(tokens);
syntaxTree.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 parse(tokens) {
const syntaxTree = [];
tokens.forEach(token => {
syntaxTree.push(/* ... */);
});
return syntaxTree;
}
删除重复的代码
通常你会有重复的代码,因为你有两个或更多稍微不同的东西,它们有很多共同点,但它们的差异迫使你有两个或更多单独的函数,它们做很多相同的事情。删除重复的代码意味着创建一个抽象,它可以只用一个函数/模块/类来处理这组不同的东西。
获得正确的抽象是至关重要的,这就是为什么您应该遵循类部分中列出的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 "manager":
data.portfolio = employee.getMBAProjects();
break;
case "developer":
data.githubLink = employee.getGithubLink();
break;
}
render(data);
});
}
使用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'
keybuttonText: "Send",
cancellable: true
};
function createMenu(config) {
let finalConfig = Object.assign({
title: "Foo",
body: "Bar",
buttonText: "Baz",
cancellable: true
},config);
return finalConfig
// config now equals: {title: "Order", body: "Bar", buttonText: "Send", cancellable: true}
}
createMenu(menuConfig);
不要使用标志作为函数参数
标志告诉你的用户这个函数做不止一件事。函数应该做一件事。如果函数遵循基于布尔值的不同代码路径,则将它们分开。
Bad:
function createFile(name, temp) {
if (temp) {
fs.create(`./temp/${name}`);
} else {
fs.create(name);
}
}
Good:
function createFile(name) {
fs.create(name);
}
function createTempFile(name) {
createFile(`./temp/${name}`);
}
避免副作用(1)
如果函数不接受一个值并返回另一个或多个值,它就会产生副作用。它的副作用可能是写入文件、修改某些全局变量。
Bad:
// Global variable referenced by following function.
// If we had another function that used this name, now it'd be an array and it could break it.
let 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'];
避免副作用(2)
在JavaScript中,有些值是不可变的(immutable),有些值是可变的(mutable)。对象和数组是两种可变值,所以当它们作为参数传递给函数时,仔细处理它们是很重要的。JavaScript函数可以改变对象的属性或数组的内容,这很容易在其他地方引起bug。
Bad:
const addItemToCart = (cart, item) => {
cart.push({
item,
date: Date.now()
});
};
Good:
const addItemToCart = (cart, item) => {
return [
...cart,
{
item,
date: Date.now()
}
];
};
不要写全局函数
污染全局变量在JavaScript中是一种糟糕的做法,因为你可能会与另一个库发生冲突,而你的API用户会毫无察觉,直到他们在生产中得到异常。
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));
}
}
函数式编程优于命令式编程
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
}
];
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
}
];
const totalOutput = programmerOutput.reduce((totalLines, output) => totalLines + output.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 this.getMaxAltitude() - this.getPassengerCount();
case "Air Force One":
return this.getMaxAltitude();
case "Cessna":
return this.getMaxAltitude() - this.getFuelExpenditure();
}
}
}
Good:
class Airplane {
// ...
}
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();
}
}
避免类型检查(1)
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"));
}
避免类型检查(2)
如果你使用的是基本的原始值,如字符串和整数,你不能使用多态性,但你仍然觉得有必要进行类型检查,你应该考虑使用TypeScript。它是普通JavaScript的优秀替代品,因为它在标准JavaScript语法之上为您提供静态类型。普通JavaScript手动类型检查的问题在于,要做好它需要太多额外的赘语,所以你得到的伪“类型安全”并不能弥补失去的可读性。保持JavaScript的干净,编写良好的测试,并进行良好的代码评审。否则,所有这些都可以用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:
// On old browsers, each iteration with uncached `list.length` would be costly
// because of `list.length` recomputation. In modern browsers, this is optimized.
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");
对象和数据结构
使用getter和setter
使用getter和setter访问对象上的数据可能比简单地查找对象上的属性要好。你可能会问:“为什么?”这里列出了一些原因:
当您想要做的不仅仅是获取对象属性时,您不必查找并更改代码库中的每个访问器。
使得在执行集合时添加验证变得简单。
封装内部表示。
在获取和设置时很容易添加日志记录和错误处理。
你可以惰性加载你的对象属性,比方说从服务器获取它。
Bad:
function makeBankAccount() {
// ...
return {
balance: 0
// ...
};
}
const account = makeBankAccount();
account.balance = 100;
Good:
function makeBankAccount() {
// this one is privatelet
balance = 0;
// a "getter", made public via the returned object below
function getBalance() {
return balance;
}
// a "setter", made public via the returned object below
function setBalance(amount) {
// ... validate before updating the balance
balance = amount;
}
return {
// ...
getBalance,
setBalance
};
}
const account = makeBankAccount();
account.setBalance(100);
使对象具有私有成员
这可以通过闭包来完成(对于ES5及以下版本)
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
类
比起ES5的普通函数,ES2015/ES6类或许更好
要获得经典ES5类的可读的类继承、构造和方法定义非常困难。如果你需要继承(注意你可能不需要),那就选择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() {
/* ... */
}
}
使用链式方法
这种模式在JavaScript中非常有用,在jQuery和Lodash等许多库中都可以看到。它允许您的代码具有表现力,并且不那么冗长。出于这个原因,我建议使用链式方法,看看代码会有多干净。在您的类函数中,只需在每个函数的末尾返回当前上下文这个值,并且您可以将更多的类方法链接到它上。
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;
// 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);
// NOTE: Returning this for chaining
return this;
}
}
const car = new Car("Ford", "F-150", "red").setColor("pink").save();
选择组合而不是继承
正如Gang of Four在著名的设计模式中所述,在可能的情况下,您应该更喜欢组合而不是继承。有很多很好的理由来使用继承和组合。这句格言的主要观点是,如果你本能地想要继承,试着想想组合是否能更好地模拟你的问题。在某些情况下是可以的。
您可能会想,“什么时候应该使用继承?”这取决于你手头的问题,继承比组合更有意义:
- 你的继承代表了一种“is-a”关系而不是“has-a”关系(人类->动物 vs 用户->UserDetails)。
- 您可以重用基类中的代码(人类可以像所有动物一样移动)。
- 您希望通过更改基类对派生类进行全局更改。(改变所有动物移动时的热量消耗)。
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 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
单一责任原则(SRP)
正如《干净代码》中所说的,“一个类改变的原因不应该超过一个”。人们很容易把一个功能丰富的类打包,比如当你只能带一个行李箱上飞机的时候。这样做的问题是,你的类在概念上不会具有内聚性,这会给它带来很多改变的理由。尽量减少更改类很重要。它之所以重要,是因为如果在一个类中有太多的功能,而您修改了其中的一部分,那么很难理解这将如何影响代码库中的其他依赖模块。
Bad:
class UserSettings {
constructor(user) {
this.user = user;
}
changeSettings(settings) {
if (this.verifyCredentials()) {
// ...
}
}
verifyCredentials() {
// ...
}
}
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()) {
// ...
}
}
}
开闭原则((OCP)
这一原则的基本意思是,您应该允许用户在不更改现有代码的情况下添加新功能。
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 => {
// transform response and return
});
} else if (this.adapter.name === "nodeAdapter") {
return makeHttpCall(url).then(response => {
// transform response and return
});
}
}
}
function makeAjaxCall(url) {
// request and return promise
}
function makeHttpCall(url) {
// request and return promise
}
Good:
class AjaxAdapter extends Adapter {
constructor() {
super();
this.name = "ajaxAdapter";
}
request(url) {
// request and return promise
}
}
class NodeAdapter extends Adapter {
constructor() {
super();
this.name = "nodeAdapter";
}
request(url) {
// request and return promise
}
}
class HttpRequester {
constructor(adapter) {
this.adapter = adapter;
}
fetch(url) {
return this.adapter.request(url).then(response => {
// transform response and return
});
}
}
里氏替换原则
对于一个非常简单的概念来说,这是一个可怕的术语。它的正式定义是:“如果S是T的子类型,那么T类型的对象可以用S类型的对象替换(也就是说,S类型的对象可以替换T类型的对象),而不改变程序的任何期望属性(正确性、任务执行情况等)。”这是一个更可怕的定义。
对此最好的解释是,如果您有基类和子类,那么基类和子类可以互换使用,而不会得到不正确的结果。
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(); // BAD: Returns 25 for Square. Should be 20.
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);
接口隔离原理(ISP)
JavaScript没有接口,所以这一原则不像其他原则那样严格适用。然而,即使JavaScript缺乏类型系统,它也很重要且相关。
ISP声明:“表明类不应该被迫依赖他们不使用的方法,也就是说一个接口应该拥有尽可能少的行为,它是精简的,也是单一的。”
在JavaScript中演示这一原则的一个很好的例子是需要大型设置对象的类。不需要客户设置大量的选项是有益的,因为大多数时候他们不需要所有的设置。使它们可选有助于防止“胖界面”。
Bad:
class DOMTraverser {
constructor(settings) {
this.settings = settings;
this.setup();
}
setup() {
this.rootNode = this.settings.rootNode;
this.settings.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() {}
}
});
依赖倒置原则(DIP)
这一原则规定了两个基本事项:
- 高级模块不应该依赖于低级模块。两者都应该依赖于抽象。
- 抽象不应该依赖于细节。细节应该依赖于抽象。
刚开始可能很难理解,但如果你使用过AngularJS,你会看到这个原理以依赖注入(DI)的形式实现。虽然它们是不同的概念,但DIP使高级模块无法了解其低级模块的细节并设置它们。它可以通过DI来完成。这样做的一个巨大好处是它减少了模块之间的耦合。耦合是一种非常糟糕的开发模式,因为它使代码难以重构。
如前所述,JavaScript没有接口,因此所依赖的抽象是隐式契约。也就是说,一个对象/类向另一个对象/类公开的方法和属性。在下面的例子中,隐含的约定是InventoryTracker的任何Request模块都将有一个requesttems方法。
Bad:
class InventoryRequester {
constructor() {
this.REQ_METHODS = ["HTTP"];
}
requestItem(item) {
// ...
}
}
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);
});
}
}
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);
});
}
}
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.
const inventoryTracker = new InventoryTracker(
["apples", "bananas"],
new InventoryRequesterV2()
);
inventoryTracker.requestItems();
并发性
使用Promises,而不是callback
callback并不干净,并且会导致过多的嵌套。在ES2015/ES6中,Promises是内置的全局类型。
Bad:
import { get } from "request";
import { writeFile } from "fs";
get(
"https://en.wikipedia.org/wiki/Robert_Cecil_Martin",
(requestErr, response, body) => {
if (requestErr) {
console.error(requestErr);
} else {
writeFile("article.html", body, writeErr => {
if (writeErr) {
console.error(writeErr);
} else {
console.log("File written");
}
});
}
}
);
Good:
import { get } from "request-promise";
import { writeFile } from "fs-extra";
get("https://en.wikipedia.org/wiki/Robert_Cecil_Martin")
.then(body => {
return writeFile("article.html", body);
})
.then(() => {
console.log("File written");
})
.catch(err => {
console.error(err);
});
Async/Await甚至比Promise更好
Promise是callback的一个非常干净的替代方案,但ES2017/ES8带来了async和await,提供了一个更干净的解决方案。您所需要的只是一个以async关键字作为前缀的函数,然后您就可以在没有函数链的情况下命令式地编写逻辑。如果你今天就能充分利用ES2017/ES8的特性。
Bad:
import { get } from "request-promise";
import { writeFile } from "fs-extra";
get("https://en.wikipedia.org/wiki/Robert_Cecil_Martin")
.then(body => {
return writeFile("article.html", body);
})
.then(() => {
console.log("File written");
})
.catch(err => {
console.error(err);
});
Good:
import { get } from "request-promise";
import { writeFile } from "fs-extra";
async function getCleanCodeArticle() {
try {
const body = await get(
"https://en.wikipedia.org/wiki/Robert_Cecil_Martin"
);
await writeFile("article.html", body);
console.log("File written");
} catch (err) {
console.error(err);
}
}
getCleanCodeArticle()
错误处理
抛出错误是一件好事!它们意味着运行时已经成功地识别出程序中出现的错误,并通过停止当前堆栈上的函数执行、终止进程(在Node中)以及在控制台中使用堆栈跟踪通知您来让您知道。
不要忽略捕捉到的错误
对捕获的错误什么都不做并不会让您有能力修复或对该错误作出反应。将错误记录到控制台(console.log)并不是很好,因为它经常会在打印到控制台的大量东西中丢失。如果您在try/catch中封装了任何代码,这意味着您认为那里可能发生错误,因此您应该为错误发生时制定计划或创建代码路径。
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!
}
不要忽视 rejected 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!
});
格式化
格式化是主观的。就像这里的许多规则一样,没有你必须遵守的硬性规则。重点是不要争论格式。有很多工具可以实现自动化。使用一个!对于工程师来说,为格式而争论是在浪费时间和金钱。
使用一致的规范
JavaScript是无类型的,所以大写可以告诉你很多关于变量、函数等的信息。这些规则是主观的,所以您的团队可以选择他们想要的任何内容。关键是,无论你们的选择是什么,都要始终如一。
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) {
// The hash
let hash = 0;
// Length of string
const length = data.length;
// Loop through every character in data
for (let 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;
}
}
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;
// Convert to 32-bit integer
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
$scope.model = {
menu: "foo",
nav: "bar"
};
// Action setup
const actions = function() {
// ...
};
Good:
$scope.model = {
menu: "foo",
nav: "bar"
};
const actions = function() {
// ...
};