avatar

目录
如何写出优雅的代码

渔:https://github.com/ryanmcdermott/clean-code-javascript

简介

一张用你阅读代码时吐槽的数量来评估软件质量的搞笑图片

文章作者根据 Robert C. Martin《代码整洁之道》 ,撰写一份适用于 JavaScript 的原则。本文不是风格指南(Style Guide),而是教导你编写出可阅读、可重复使用与可重构的 JavaScript 代码。

注意!你不必严格遵守每一项原则,有些甚至不被大众所认同。虽然这只是份指南,却是来自 《代码整洁之道》 作者多年的结晶。

软件工程只发展了 50 年,仍然有很多地方值得去探讨。当软件与建筑一样古老时,也许会有一些墨守成规的原则。但现在,先让这份指南当试金石,尝试将他们作为团队代码质量考核的标准之一吧。

还有一件事情:知道这些原则,并不会立刻让你成为优秀的工程师,长期奉行它们,不代表你能高枕无忧不再犯错。 但是,千里之行,始于足下,时常与志同道合们进行讨论(Code Review),改善不完备之处。不要因为自己写出来的代码很糟糕而害怕分享,而是要畏惧自己居然写出了这样的代码!

变量

使用有意义,可读性好的变量名

bad:

javascript
1
const yyyymmdstr = moment().format('YYYY/MM/DD');

good:

javascript
1
const currentDate = moment().format('YYYY/MM/DD');

使用 ES6 的 const 定义常量

反例中使用”var”定义的”常量”是可变的。

在声明一个常量时,该常量在整个程序中都应该是不可变的。

bad::

javascript
1
var FIRST_US_PRESIDENT = "George Washington";

good::

javascript
1
const FIRST_US_PRESIDENT = "George Washington";

对功能类似的变量名采用统一的命名风格

bad:

javascript
1
2
3
getUserInfo();
getClientData();
getCustomerRecord();

good:

javascript
1
getUser();

使用可搜索的名称

我们需要阅读的代码远比自己写的要多,使代码拥有良好的可读性且易于检索非常重要。阅读变量名晦涩难懂的代码对读者来说是一种相当糟糕的体验。
让你的变量名易于检索。

bad:

javascript
1
2
// 86400000 是什么鬼?
setTimeout(blastOff, 86400000);

good:

javascript
1
2
3
4
// 将它们声明为全局常量 `const` 。
const MILLISECONDS_IN_A_DAY = 86_400_000;

setTimeout(blastOff, MILLISECONDS_IN_A_DAY);

使用说明变量(即有意义的变量名)

bad:

javascript
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]
);

good:

javascript
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);

不要绕太多的弯子

显式优于隐式。

bad:

javascript
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);
});

good:

javascript
1
2
3
4
5
6
7
8
9
const locations = ["Austin", "New York", "San Francisco"];
locations.forEach(location => {
doStuff();
doSomeOtherStuff();
// ...
// ...
// ...
dispatch(location);
});

避免重复的描述

当类/对象名已经有意义时,对其变量进行命名不需要再次重复。

bad:

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

function paintCar(car) {
car.carColor = "Red";
}

good:

javascript
1
2
3
4
5
6
7
8
9
const Car = {
make: "Honda",
model: "Accord",
color: "Blue"
};

function paintCar(car) {
car.color = "Red";
}

避免无意义的条件判断

bad:

javascript
1
2
3
4
function createMicrobrewery(name) {
const breweryName = name || "Hipster Brew Co.";
// ...
}

good:

javascript
1
2
3
function createMicrobrewery(name = "Hipster Brew Co.") {
// ...
}

函数

函数参数(理想情况下应不超过 2 个)

限制函数参数数量很有必要,这么做使得在测试函数时更加轻松。过多的参数将导致难以采用有效的测试用例对函数的各个参数进行测试。

应避免三个以上参数的函数。通常情况下,参数超过两个意味着函数功能过于复杂,这时需要重新优化你的函数。当确实需要多个参数时,大多情况下可以考虑这些参数封装成一个对象。

JavaScript 定义对象非常方便,当需要多个参数时,可以使用一个对象进行替代。

bad:

javascript
1
2
3
4
5
function createMenu(title, body, buttonText, cancellable) {
// ...
}

createMenu("Foo", "Bar", "Baz", true);

good:

javascript
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
});

函数功能的单一性

这是软件功能中最重要的原则之一。

功能不单一的函数将导致难以重构、测试和理解。功能单一的函数易于重构,并使代码更加干净。

bad:

javascript
1
2
3
4
5
6
7
8
function emailClients(clients) {
clients.forEach(client => {
const clientRecord = database.lookup(client);
if (clientRecord.isActive()) {
email(client);
}
});
}

good:

javascript
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();
}

函数名应明确表明其功能

bad:

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

const date = new Date();

// 很难从函数名判断添加了什么
addToDate(date, 1);

good:

javascript
1
2
3
4
5
6
function addMonthToDate(month, date) {
// ...
}

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

函数应该只做一层抽象

当在你的函数中有多于一个抽象级别时,你的函数通常做了太多事情。拆分函数将会提升重用性和测试性。

bad:

javascript
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...
});
}

good:

javascript
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;
}

移除重复的代码

永远、永远、永远不要在任何循环下有重复的代码。

这种做法毫无意义且潜在危险极大。重复的代码意味着逻辑变化时需要对不止一处进行修改。JavaScript 弱类型的特点使得函数拥有更强的普适性。好好利用这一优点吧。

bad:

javascript
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);
});
}

good:

javascript
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 设置默认对象

bad:

javascript
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);

good:

javascript
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)作为函数参数

这通常意味着函数的功能的单一性已经被破坏,此时应考虑对函数进行再次划分。

bad:

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

good:

javascript
1
2
3
4
5
6
7
function createFile(name) {
fs.create(name);
}

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

避免副作用 (part 1)

当函数产生了除了“接受一个值并返回一个结果”之外的行为时,称该函数产生了副作用。比如写文件、修改全局变量或将你的钱全转给了一个陌生人等。

程序在某些情况下确实需要副作用这一行为,如先前例子中的写文件。这时应该将这些功能集中在一起,不要用多个函数/类修改某个文件。用且只用一个 service 完成这一需求。

bad:

javascript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 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.
// 如果我们有另一个函数使用这个 name , 现在它应该是一个数组, 这可能会出现错误。
let name = "Ryan McDermott";

function splitIntoFirstAndLastName() {
name = name.split(" ");
}

splitIntoFirstAndLastName();

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

good:

javascript
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'];

避免副作用 (part 2)

在 JavaScript 中,基本类型赋值是值传递,引用类型(对象、数组)赋值专递是内存地址。在本案例中,你的函数改变了 购物车清单 中的数组,像是你增加了一个商品,其他使用购物车清单的函数将会被影响,这做法有利有弊。让我们想象一下情况:

用户点击“提交订单”按钮,调用“提交订单”功能生成一个网络请求并发送 cart 数组到服务器。因网络卡顿,“提交订单” 请求在不断重试。同时,如果用户不小心点击了 addItemToCart 怎么办?如果发生这种情况,那么意外添加的 cart 商品也会提交到结算订单。那还得了?

解决方案应该是在 addItemToCart 方法中先拷贝 cart 信息,然后对其拷贝操作并返回,这样可以确保其他引用 cart 信息不受影响。

关于解决方案,有两点需要注意:

  1. 当采用这种做法后,你会发现,需要修改输入对象的情况非常少。大多数的代码可以在没有副作用的情况下重构!
  2. 就性能而言,拷贝大型对象操作非常昂贵。但幸运的是这在实践中并不是一个大问题,参考:immutable-js

bad:

javascript
1
2
3
const addItemToCart = (cart, item) => {
cart.push({ item, date: Date.now() });
};

good:

javascript
1
2
3
const addItemToCart = (cart, item) => {
return [...cart, { item, date: Date.now() }];
};

不要写全局函数

在 JavaScript 中污染全局是一个非常不好的实践,这么做可能和其他库起冲突,且调用你的 API 的用户在实际环境中得到一个 exception 前对这一情况是一无所知的。

想象以下例子:如果你想扩展 JavaScript 中的 Array,为其添加一个 diff 函数显示两个数组间的差异,此时应如何去做?你可以将 diff 写入 Array.prototype,但这么做会和其他有类似需求的库造成冲突。如果另一个库对 diff 的需求为比较一个数组中首尾元素间的差异呢?

使用 ES6 中的 class 对全局的 Array 做简单的扩展显然是一个更棒的选择。

bad:

javascript
1
2
3
4
Array.prototype.diff = function diff(comparisonArray) {
const hash = new Set(comparisonArray);
return this.filter(elem => !hash.has(elem));
};

good:

javascript
1
2
3
4
5
6
class SuperArray extends Array {
diff(comparisonArray) {
const hash = new Set(comparisonArray);
return this.filter(elem => !hash.has(elem));
}
}

采用函数式编程

JavaScript 不是 Haskell 那种方式的函数式语言,但是它有它的函数式风格。函数式语言更加简洁并且更容易进行测试,当你可以使用函数式编程风格时请尽情使用。

bad:

javascript
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;
}

good:

javascript
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
);

封装判断条件

bad:

javascript
1
2
3
if (fsm.state === "fetching" && isEmpty(listNode)) {
// ...
}

good:

javascript
1
2
3
4
5
6
7
function shouldShowSpinner(fsm, listNode) {
return fsm.state === "fetching" && isEmpty(listNode);
}

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

避免“否定情况”的判断

bad:

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

if (!isDOMNodeNotPresent(node)) {
// ...
}

good:

javascript
1
2
3
4
5
6
7
function isDOMNodePresent(node) {
// ...
}

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

避免条件判断

这看起来似乎不太可能。

大多人听到这的第一反应是:“怎么可能不用 if 完成其他功能呢?”许多情况下通过使用多态(polymorphism)可以达到同样的目的。

第二个问题在于采用这种方式的原因是什么。答案是我们之前提到过的:保持函数功能的单一性。

bad:

javascript
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();
}
}
}

good:

javascript
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();
}
}

避免类型判断(part 1)

JavaScript 是弱类型语言,这意味着函数可接受任意类型的参数。

有时这会对你带来麻烦,你会对参数做一些类型判断。有许多方法可以避免这些情况。

bad:

javascript
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"));
}
}

good:

javascript
1
2
3
function travelToTexas(vehicle) {
vehicle.move(this.currentLocation, new Location("texas"));
}

避免类型判断(part 2)

如果需处理的数据为字符串,整型,数组等类型,无法使用多态并仍有必要对其进行类型检测时,可以考虑使用 TypeScript。

bad:

javascript
1
2
3
4
5
6
7
8
9
10
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:

javascript
1
2
3
function combine(val1, val2) {
return val1 + val2;
}

避免过度优化

现代的浏览器在运行时会对代码自动进行优化。有时人为对代码进行优化可能是在浪费时间。

这里可以找到许多真正需要优化的地方

bad:

javascript
1
2
3
4
5
6
// 这里使用变量 len 是因为在老式浏览器中,
// 直接使用正例中的方式会导致每次循环均重复计算 list.length 的值,
// 而在现代浏览器中会自动完成优化,这一行为是没有必要的
for (let i = 0, len = list.length; i < len; i++) {
// ...
}

good:

javascript
1
2
3
for (let i = 0; i < list.length; i++) {
// ...
}

删除无效的代码

不再被调用的代码应及时删除。

bad:

javascript
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");

good:

javascript
1
2
3
4
5
6
function newRequestModule(url) {
// ...
}

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

对象和数据结构

使用 getters 和 setters

JavaScript 没有接口或类型,因此实现这一模式是很困难的,因为我们并没有类似 publicprivate 的关键词。

然而,使用 getters 和 setters 获取对象的数据远比直接使用点操作符具有优势。为什么呢?

  1. 当需要对获取的对象属性执行额外操作时。
  2. 执行 set 时可以增加规则对要变量的合法性进行判断。
  3. 封装了内部逻辑。
  4. 在存取时可以方便的增加日志和错误处理。
  5. 继承该类时可以重载默认行为。
  6. 从服务器获取数据时可以进行懒加载。

bad:

javascript
1
2
3
4
5
6
7
8
9
10
11
function makeBankAccount() {
// ...

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

const account = makeBankAccount();
account.balance = 100;

good:

javascript
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 或更低)。

bad:

javascript
1
2
3
4
5
6
7
8
9
10
11
12
13

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:

javascript
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

使用 ES6 的 classes 而不是 ES5 的 Function

典型的 ES5 的类(function)在继承、构造和方法定义方面可读性较差。

当需要继承时,优先选用 classes。

但是,当在需要更大更复杂的对象时,最好优先选择更小的 function 而非 classes。

bad:

javascript
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 Animal 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:

javascript
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)中显得非常实用,它使你的代码变得富有表现力,并减少啰嗦。

因此,我认为在 JavaScript 中使用方法链是非常合适的。在 class 的函数中返回 this,能够方便的将类需要执行的多个方法链接起来。

bad:

javascript
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();

good:

javascript
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. 继承关系表现为”是一个”而非”有一个”(如动物 → 人 和 用户 → 用户细节)
  2. 可以复用基类的代码(”Human”可以看成是”All animal”的一种)
  3. 希望当基类改变时所有派生类都受到影响(如修改”all animals”移动时的卡路里消耗量)

bad:

javascript
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;
}

// ...
}

// 不好是因为雇员“有”税率数据, EmployeeTaxData 不是一个 Employee 类型。
class EmployeeTaxData extends Employee {
constructor(ssn, salary) {
super();
this.ssn = ssn;
this.salary = salary;
}

// ...
}

good:

javascript
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);
}
// ...
}

SOLID

单一职责原则 (SRP)

如《代码整洁之道》一书中所述,“修改一个类的理由不应该超过一个”。

将多个功能塞进一个类的想法很诱人,但这将导致你的类无法达到概念上的内聚,并经常不得不进行修改。

最小化对一个类需要修改的次数是非常有必要的。如果一个类具有太多太杂的功能,当你对其中一小部分进行修改时,将很难想象到这一修够对代码库中依赖该类的其他模块会带来什么样的影响。

bad:

javascript
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() {
// ...
}
}

good:

javascript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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)

“代码实体(类,模块,函数等)应该易于扩展,难于修改。”

这一原则指的是我们应允许用户方便的扩展我们代码模块的功能,而不需要打开 js 文件源码手动对其进行修改。

bad:

javascript
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
}

good:

javascript
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
});
}
}

里氏代换原则 (LSP)

“子类对象应该能够替换其超类对象被使用”。

也就是说,如果有一个父类和一个子类,当采用子类替换父类时不应该产生错误的结果。

bad:

javascript
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);

good:

javascript
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);

接口隔离原则 (ISP)

JavaScript 没有接口,所以这个原则不想其它语言那么严格。不过,对于 JavaScript 这种缺少类型的语言来说,它依然是重要并且有意义的。

接口隔离原则说的是 “客户端不应该依赖它不需要的接口;一个类对另一个类的依赖应该建立在最小的接口上。” 在 JavaScript 这种弱类型语言中,接口是隐式的契约。

在 JavaScript 中能比较好的说明这个原则:当一个类需要许多参数设置才能生成一个对象时,或许大多时候不需要设置这么多的参数。此时减少对配置参数数量的需求是有益的。

bad:

javascript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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:

javascript
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
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)

该原则有两个核心点:

  1. 高层模块不应该依赖于低层模块。他们都应该依赖于抽象接口;
  2. 抽象接口应该脱离具体实现,具体实现应该依赖于抽象接口。

这个一开始会很难理解,但是如果你使用过 Angular.js,你应该已经看到过通过依赖注入来实现的这个原则,虽然他们不是相同的概念,依赖反转原则让高级模块远离低级模块的细节和创建,可以通过 DI 来实现。这样做的巨大益处是降低模块间的耦合。耦合是一个非常糟糕的开发模式,因为会导致代码难于重构。

如上所述,JavaScript 没有接口,所以被依赖的抽象是隐式契约。也就是说,一个对象/类的方法和属性直接暴露给另外一个对象/类。在下面的例子中,任何一个 Request 模块的隐式契约 InventoryTracker 将有一个 requestItems 方法。

bad:

javascript
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:我们已经创建了一个对请求的具体实现的依赖,我们只有一个 requestItems 方法依
// 赖一个请求方法 'request'
this.requester = new InventoryRequester();
}

requestItems() {
this.items.forEach(item => {
this.requester.requestItem(item);
});
}
}

const inventoryTracker = new InventoryTracker(["apples", "bananas"]);
inventoryTracker.requestItems();

good:

javascript
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();

测试

测试比发布更加重要。当发布时,如果你没有测试或是测试不够充分,你会无法确认有没有任何功能被破坏。测试的量,由团队决定,但是拥有 100% 的测试覆盖率(包含状态与分支),是你为什么能有高度自信与内心平静的原因。所以你需要一个伟大的测试框架,也需要一个好的覆盖率(coverage)工具。

没有任何借口不写测试。这里有很多好的 JavaScript 测试框架,选一个你的团队喜欢的。选择好之后,接下来的目标是为任何新功能或是模组撰写测试。如果你喜好测试驱动开发(Test Driven Development)的方式,那就太棒了,重点是确保上线前或是重构之前,达到足够的覆盖率。

译者附注。

测试是一种保障,当你赶着修正错误时,测试会告诉你会不会改了 A 坏了 B。 确保每次上线前的功能皆可正常运作。 另外测试有分种类,详情见链接测试的种类

每个测试只测试一个概念

bad:

javascript
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);
});
});

good:

javascript
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);
});
});

并发

用 Promises 替代回调

回调不够整洁并会造成大量的嵌套。在 ES2015/ES6 中,Promises 已经是内置的全局类型(global type),使用它们吧!

bad:

javascript
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");
}
});
}
}
);

good:

javascript
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 是比 Promises 更好的选择

Promises 是较回调而言更好的一种选择,但 ES2017/ES8 中的 Async/Await 更胜过 Promises。

在能使用 ES8 特性的情况下可以尽量使用他们替代 Promises。

bad:

javascript
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);
});

good:

javascript
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()

错误处理

抛出错误是一件好事情!他们意味着当你的程序有错时运行时可以成功确认,并且通过停止执行当前堆栈上的函数来让你知道,结束当前进程(在 Node 中),在控制台中用一个堆栈跟踪提示你。

别忘了捕获错误

对捕捉到的错误不做任何处理不能给你修复错误或者响应错误的能力。向控制台记录错误(console.log)也不怎么好,因为往往会丢失在海量的控制台输出中。如果你把任意一段代码用 try/catch 包装那就意味着你想到这里可能会错,因此你应该有个修复计划,或者当错误发生时有一个代码路径。

bad:

javascript
1
2
3
4
5
try {
functionThatMightThrow();
} catch (error) {
console.log(error);
}

good:

javascript
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

理由同 try/catch

bad:

javascript
1
2
3
4
5
6
7
getdata()
.then(data => {
functionThatMightThrow(data);
})
.catch(error => {
console.log(error);
});

good:

javascript
1
2
3
4
5
6
7
8
9
10
11
12
13
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:

javascript
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 {}

good:

javascript
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 {}

调用函数的函数和被调函数应放在较近的位置

如果一个函数调用另一个,则在代码中这两个函数的竖直位置应该靠近。理想情况下,保持被调用函数在被调用函数的正上方。我们倾向于从上到下阅读代码,就像读一章报纸。由于这个原因,保持你的代码可以按照这种方式阅读。

bad:

javascript
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();

good:

javascript
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();

注释

只对存在一定业务逻辑复杂性的代码进行注释

注释并不是必须的,好的代码是能够让人一目了然,不用过多无谓的注释。

bad:

javascript
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;
}
}

good:

javascript
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;
}
}

不要在代码库中遗留被注释掉的代码

版本控制的存在是有原因的。让旧代码存在于你的 history 里吧。

bad:

javascript
1
2
3
4
doStuff();
// doOtherStuff();
// doSomeMoreStuff();
// doSoMuchStuff();

good:

javascript
1
doStuff();

不需要版本更新类型注释

记住,使用版本控制!不需要僵尸代码,注释掉的代码,尤其是日志式的注释。使用 git log 来获取历史记录。

bad:

javascript
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;
}

good:

javascript
1
2
3
function combine(a, b) {
return a + b;
}

避免位置标记

这些东西通常只能代码麻烦,采用适当的缩进就可以了。

bad:

javascript
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() {
// ...
};

good:

javascript
1
2
3
4
5
6
7
8
$scope.model = {
menu: "foo",
nav: "bar"
};

const actions = function() {
// ...
};
文章作者: Tim
文章链接: http://w3ctim.com/post/d60987a2.html
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Tim

评论