最佳实践推荐 (02) -- clean-code-javascript

目录

clean-code-javascript

clean-code-javascript

它是用JavaScript编写可读、可重用和可重构软件的指南。

官网地址

学习笔记

变量

使用有意义可读出的变量名称

避免:
1
const yyyymmdstr = moment().format("YYYY/MM/DD");
推荐:
1
const currentDate = moment().format("YYYY/MM/DD");

对于对同类型的变量使用同一个词

避免:
1
2
3
getUserInfo();
getClientData();
getCustomerRecord();
推荐:
1
getUser();

使用可搜索的变量名

避免:
1
2
// What the heck is 86400000 for?
setTimeout(blastOff, 86400000);
推荐:
1
2
3
4
// Declare them as capitalized named constants.
const MILLISECONDS_PER_DAY = 60*60*24*1000; //86400000;

setTimeout(blastOff, MILLISECONDS_PER_DAY);

使用解构变量

避免:
1
2
3
4
5
6
const address = "One Infinite Loop, Cupertino 95014";
const cityZipCodeRegex = /^[^,\\]+[,\\\s]+(.+?)\s*(\d{5})?$/;
saveCityZipCode(
address.match(cityZipCodeRegex)[1],
address.match(cityZipCodeRegex)[2]
);
推荐:
1
2
3
4
const address = "One Infinite Loop, Cupertino 95014";
const cityZipCodeRegex = /^[^,\\]+[,\\\s]+(.+?)\s*(\d{5})?$/;
const [_, city, zipCode] = address.match(cityZipCodeRegex) || [];
saveCityZipCode(city, zipCode);

避免头脑映射

显性比隐性好。

避免:
1
2
3
4
5
6
7
8
9
10
const locations = ["Austin", "New York", "San Francisco"];
locations.forEach(l => {
doStuff();
doSomeOtherStuff();
// ...
// ...
// ...
// Wait, what is `l` for again?
dispatch(l);
});
推荐:
1
2
3
4
5
6
7
8
9
const locations = ["Austin", "New York", "San Francisco"];
locations.forEach(location => {
doStuff();
doSomeOtherStuff();
// ...
// ...
// ...
dispatch(location);
});

不要添加多余的内容

如果你的类/对象名告诉你一些东西,不要在变量名中重复。

避免:
1
2
3
4
5
6
7
8
9
const Car = {
carMake: "Honda",
carModel: "Accord",
carColor: "Blue"
};

function paintCar(car, color) {
car.carColor = color;
}
推荐:
1
2
3
4
5
6
7
8
9
const Car = {
make: "Honda",
model: "Accord",
color: "Blue"
};

function paintCar(car, color) {
car.color = color;
}

使用默认参数值,而不是条件

默认参数值通常比条件更简洁。注意,如果使用它们,函数只会为未定义的参数提供默认值。其他“falsy”值,如“,”,false, null, 0和NaN,将不会被默认值替换。

避免:
1
2
3
4
function createMicrobrewery(name) {
const breweryName = name || "Hipster Brew Co.";
// ...
}
推荐:
1
2
3
function createMicrobrewery(name = "Hipster Brew Co.") {
// ...
}

函数

限制入参个数为2个或2个以下

为了使函数期望的属性更明显,可以使用ES2015/ES6解构语法。这有几个优点:

  1. 当有人查看函数签名时,立即就能清楚使用了什么属性。
  2. 它可以用来模拟已命名的参数。
  3. 解构还克隆传入函数的参数对象的指定原始值。这有助于防止副作用。注意:从参数对象解构的对象和数组不会被克隆。
  4. Linters可以警告未使用的属性,如果不进行解构,这是不可能的。
避免:
1
2
3
4
5
function createMenu(title, body, buttonText, cancellable) {
// ...
}

createMenu("Foo", "Bar", "Baz", true);
推荐:
1
2
3
4
5
6
7
8
9
10
function createMenu({ title, body, buttonText, cancellable }) {
// ...
}

createMenu({
title: "Foo",
body: "Bar",
buttonText: "Baz",
cancellable: true
});

一个函数做一件事

避免:
1
2
3
4
5
6
7
8
function emailClients(clients) {
clients.forEach(client => {
const clientRecord = database.lookup(client);
if (clientRecord.isActive()) {
email(client);
}
});
}
推荐:
1
2
3
4
5
6
7
8
function emailActiveClients(clients) {
clients.filter(isActiveClient).forEach(email);
}

function isActiveClient(client) {
const clientRecord = database.lookup(client);
return clientRecord.isActive();
}

函数名称应该告诉读者其目的

避免:
1
2
3
4
5
6
7
8
function addToDate(date, month) {
// ...
}

const date = new Date();

// It's hard to tell from the function name what is added
addToDate(date, 1);
推荐:
1
2
3
4
5
6
function addMonthToDate(month, date) {
// ...
}

const date = new Date();
addMonthToDate(1, date);

函数应该只是一种抽象级别

避免:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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...
});
}
推荐:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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;
}

删除重复代码

通常情况下,你有重复的代码是因为你有两个或两个以上略有不同的东西,它们有很多共同点,但它们的差异迫使你有两个或两个以上单独的函数,它们做很多相同的事情。删除重复代码意味着创建一个抽象,它可以用一个函数/模块/类处理这组不同的事情。

避免:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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);
});
}
推荐:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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 设置默认对象

避免:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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);
推荐:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const menuConfig = {
title: "Order",
// User did not include 'body' key
buttonText: "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);

不要使用 flag 作为函数参数

标志告诉用户这个函数不止做一件事。函数只做一件事。如果函数遵循基于布尔值的不同代码路径,则将它们分开。

避免:
1
2
3
4
5
6
7
function createFile(name, temp) {
if (temp) {
fs.create(`./temp/${name}`);
} else {
fs.create(name);
}
}
推荐:
1
2
3
4
5
6
7
function createFile(name) {
fs.create(name);
}

function createTempFile(name) {
createFile(`./temp/${name}`);
}

避免副作用

如果函数除了接收一个值并返回另一个或多个值之外做了其他事情,则会产生副作用。副作用可能是写入一个文件,修改一些全局变量,或者不小心把你所有的钱都汇给一个陌生人。

你要做的是把你做这件事的地方集中起来。不要有多个函数和类写入特定文件。让一个服务来做这件事。一个,只有一个。

避免:
1
2
3
4
5
6
7
8
9
10
11
// 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'];
推荐:
1
2
3
4
5
6
7
8
9
function splitIntoFirstAndLastName(name) {
return name.split(" ");
}

const name = "Ryan McDermott";
const newName = splitIntoFirstAndLastName(name);

console.log(name); // 'Ryan McDermott';
console.log(newName); // ['Ryan', 'McDermott'];

避免直接修改函数入参

  1. 在某些情况下,您可能确实需要修改输入对象,但是当您采用这种编程实践时,您会发现这种情况非常罕见。大多数东西可以重构到没有副作用!

  2. 就性能而言,克隆大对象的代价非常高昂。幸运的是,这在实践中并不是一个大问题,因为有很多很棒的库可以让这种编程方法更快,而且不像手动克隆对象和数组那样需要大量内存。

避免:
1
2
3
const addItemToCart = (cart, item) => {
cart.push({ item, date: Date.now() });
};
推荐:
1
2
3
const addItemToCart = (cart, item) => {
return [...cart, { item, date: Date.now() }];
};

不要污染全局

避免:
1
2
3
4
Array.prototype.diff = function diff(comparisonArray) {
const hash = new Set(comparisonArray);
return this.filter(elem => !hash.has(elem));
};
推荐:
1
2
3
4
5
6
class SuperArray extends Array {
diff(comparisonArray) {
const hash = new Set(comparisonArray);
return this.filter(elem => !hash.has(elem));
}
}

函数式编程优于命令式编程

函数式语言更简洁,更容易测试。如果可以,请尽量使用这种编程风格。

避免:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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;
}
推荐:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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
);

封装条件

避免:
1
2
3
if (fsm.state === "fetching" && isEmpty(listNode)) {
// ...
}
推荐:
1
2
3
4
5
6
7
function shouldShowSpinner(fsm, listNode) {
return fsm.state === "fetching" && isEmpty(listNode);
}

if (shouldShowSpinner(fsmInstance, listNodeInstance)) {
// ...
}

避免使用否定条件句

避免:
1
2
3
4
5
6
7
function isDOMNodeNotPresent(node) {
// ...
}

if (!isDOMNodeNotPresent(node)) {
// ...
}
推荐:
1
2
3
4
5
6
7
function isDOMNodePresent(node) {
// ...
}

if (isDOMNodePresent(node)) {
// ...
}

避免条件

这似乎是一个不可能完成的任务。第一次听到这个,大多数人会说:“没有 if 语句,我怎么能做任何事情呢?”答案是,您可以在许多情况下使用多态性来实现相同的任务。第二个问题通常是,“这很好,但我为什么要这么做?”答案是我们之前学过的干净代码概念:函数应该只做一件事。当你的类和函数有 if 语句时,你是在告诉你的用户你的函数不止做一件事。记住,只做一件事。

避免:
1
2
3
4
5
6
7
8
9
10
11
12
13
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();
}
}
}
推荐:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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
2
3
4
5
6
7
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"));
}
}
推荐:
1
2
3
function travelToTexas(vehicle) {
vehicle.move(this.currentLocation, new Location("texas"));
}

避免类型检查:使用 Typescript

避免:
1
2
3
4
5
6
7
8
9
function combine(val1, val2) {
if (
typeof val1 === "number" && typeof val2 === "number"
) {
return val1 + val2;
}

throw new Error("Must be of type String or Number");
}
推荐:
1
2
3
function combine(val1: number, val2: number) {
return val1 + val2;
}

不要过度优化

现代浏览器在运行时进行了大量的优化。很多时候,如果你在优化,那么你只是在浪费时间。

避免:
1
2
3
4
5
// 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++) {
// ...
}
推荐:
1
2
3
for (let i = 0; i < list.length; i++) {
// ...
}

移除不可调用的代码

如果它没有被调用,就删除它!如果您仍然需要它,那么它在版本历史记录中仍然是安全的。

避免:
1
2
3
4
5
6
7
8
9
10
function oldRequestModule(url) {
// ...
}

function newRequestModule(url) {
// ...
}

const req = newRequestModule;
inventoryTracker("apples", req, "www.inventory-awesome.io");
推荐:
1
2
3
4
5
6
function newRequestModule(url) {
// ...
}

const req = newRequestModule;
inventoryTracker("apples", req, "www.inventory-awesome.io");

Objects and Data Structures

使用getter和setter

  1. 当您想要做更多的事情,而不仅仅是获取对象属性时,您不必查找并更改代码库中的每个访问器。
  2. 当使用 set 是,添加验证会变得简单。
  3. 封装内部表示。
  4. 当使用 setget 容易添加日志和错误处理。
  5. 你可以延迟加载对象的属性,比如从服务器获取。
避免:
1
2
3
4
5
6
7
8
9
10
11
function makeBankAccount() {
// ...

return {
balance: 0
// ...
};
}

const account = makeBankAccount();
account.balance = 100;
推荐:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function makeBankAccount() {
// this one is private
let 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及以下版本)。

避免:
1
2
3
4
5
6
7
8
9
10
11
12
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
推荐:
1
2
3
4
5
6
7
8
9
10
11
12
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

更多使用ES2015/ES6类,而不是ES5普通函数

对于经典的ES5类,很难获得可读的类继承、构造和方法定义。如果你需要继承,那么最好选择ES2015/ES6类。

避免:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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() {};
推荐:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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。它让你的代码更有表现力,更简洁。在类函数中,只需在每个函数的末尾返回this,就可以将进一步的类方法链接到它上面。

避免:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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();
推荐:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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();

更多地使用组件而不是继承

如果你的思维本能地倾向于继承,试着思考组件是否可以更好地模拟你的问题。

  1. 继承代表一个“is-a”关系 (Human -> Animal),组件是一个“has-a”关系。(User -> UserDetails)
  2. 当可以重用基类中的代码,使用继承(Human可以像所有Animal一样move)。否则使用组件
  3. 当需要通过修改基类从而修改所有派生类,使用继承(修改动物move的卡路里消耗)。否则使用组件
避免:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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;
}

// ...
}
推荐:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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);
}
// ...
}

定理

单一功能原则

修改一个类的原因不应该有多个。

就像做飞机只带一个行李箱一样,开发者会容易在一个类中塞很多方法。这样做的问题是,这种类在概念上不会是内聚的,它会有很多改变的理由。尽量减少更改类的次数是很重要的。因为如果一个类中有太多的功能,而你修改了其中的一部分,就很难理解这会如何影响代码库中的其他相关模块。

避免:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class UserSettings {
constructor(user) {
this.user = user;
}

changeSettings(settings) {
if (this.verifyCredentials()) {
// ...
}
}

verifyCredentials() {
// ...
}
}
推荐:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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

这个原则基本上是说你应该允许用户在不改变现有代码的情况下添加新的功能。

避免:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
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
}
推荐:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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的对象),而不改变程序的任何理想属性(正确性、执行的任务等)。”这个定义更可怕。

对此最好的解释是,如果您有一个父类和一个子类,那么基类和子类可以互换使用,而不会得到错误的结果。这可能仍然令人困惑,所以让我们看一看经典的方-矩形示例。从数学上讲,正方形是矩形,但如果你通过继承使用“is-a”关系来建模,你很快就会陷入麻烦。

避免:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
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);
推荐:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
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);

接口隔离原则

没看懂。原文

依赖倒置原则

  1. 高级模块不应该依赖于低级模块。两者都应该依赖于抽象。
  2. 抽象不应该依赖于细节。细节应该依赖于抽象。

该原则使高级模块无法了解其低级模块的详细信息并对其进行设置。这样做的一个巨大好处是减少了模块之间的耦合。

避免:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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();
推荐:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
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();

测试

每次测试只有一个概念

避免:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import assert from "assert";

describe("MomentJS", () => {
it("handles date boundaries", () => {
let date;

date = new MomentJS("1/1/2015");
date.addDays(30);
assert.equal("1/31/2015", date);

date = new MomentJS("2/1/2016");
date.addDays(28);
assert.equal("02/29/2016", date);

date = new MomentJS("2/1/2015");
date.addDays(28);
assert.equal("03/01/2015", date);
});
});
推荐:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import assert from "assert";

describe("MomentJS", () => {
it("handles 30-day months", () => {
const date = new MomentJS("1/1/2015");
date.addDays(30);
assert.equal("1/31/2015", date);
});

it("handles leap year", () => {
const date = new MomentJS("2/1/2016");
date.addDays(28);
assert.equal("02/29/2016", date);
});

it("handles non-leap year", () => {
const date = new MomentJS("2/1/2015");
date.addDays(28);
assert.equal("03/01/2015", date);
});
});

并发

使用 promise ,而不是 callback

避免:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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");
}
});
}
}
);
推荐:
1
2
3
4
5
6
7
8
9
10
11
12
13
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更简洁

避免:
1
2
3
4
5
6
7
8
9
10
11
12
13
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);
});
推荐:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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()

错误处理

Don’t ignore caught errors

将错误记录到控制台(console.log)并没有好到哪里去,因为它经常会被打印到控制台的大量内容所淹没。如果你在try/catch中封装了任何一段代码,这意味着你认为那里可能会发生错误,因此你应该有一个处理错误的计划。

避免:
1
2
3
4
5
try {
functionThatMightThrow();
} catch (error) {
console.log(error);
}
推荐:
1
2
3
4
5
6
7
8
9
10
11
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!
}

不要忽视 promise 的 reject

原文示例代码有误。

格式化

只读变量用大写表示

避免:
1
2
3
4
5
6
7
8
9
10
11
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 {}
推荐:
1
2
3
4
5
6
7
8
9
10
11
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 {}

调用者在被调用者的上方

避免:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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();
推荐:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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();

注释

只注释具有业务逻辑复杂性的内容

注释是一种道歉,而不是必须。好的代码大多是本身就是文档。

避免:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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;
}
}
推荐:
1
2
3
4
5
6
7
8
9
10
11
12
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;
}
}

不要在代码库中保留带注释的代码

版本控制的存在是有原因的。在历史记录中保留旧代码。

避免:
1
2
3
4
doStuff();
// doOtherStuff();
// doSomeMoreStuff();
// doSoMuchStuff();
推荐:
1
doStuff();

不要写日志注释

记住,使用版本控制!不需要死代码、注释代码,尤其是日志注释。使用git日志获取历史!

避免:
1
2
3
4
5
6
7
8
9
/**
* 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;
}
推荐:
1
2
3
function combine(a, b) {
return a + b;
}

避免位置标记

让函数和变量名以及适当的缩进和格式为代码提供可视化结构。

避免:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
////////////////////////////////////////////////////////////////////////////////
// Scope Model Instantiation
////////////////////////////////////////////////////////////////////////////////
$scope.model = {
menu: "foo",
nav: "bar"
};

////////////////////////////////////////////////////////////////////////////////
// Action setup
////////////////////////////////////////////////////////////////////////////////
const actions = function() {
// ...
};
推荐:
1
2
3
4
5
6
7
8
$scope.model = {
menu: "foo",
nav: "bar"
};

const actions = function() {
// ...
};