本文是原作者根据《代码整洁之道》总结的适用于 JavaScript 软件工程的原则。
本文是对作者英文原文《Clean Code JavaScript》的翻译整理。
变量使用有意义,可读性好的变量名
Bad:
var yyyymmdstr = moment().format('YYYY/MM/DD');
Good:
var yearMonthDay = moment().format('YYYY/MM/DD');
使用 ES6 的 const 定义常量
反例中使用"var"定义的"常量"是可变的。
在声明一个常量时,该常量在整个程序中都应该是不可变的。
Bad:
var FIRST_US_PRESIDENT = "George Washington";
Good:
const FIRST_US_PRESIDENT = "George Washington";
对功能类似的变量名采用统一的命名风格
Bad:
getUserInfo();
getClientData();
getCustomerRecord();
Good:
getUser();
使用易于搜索名称
我们需要阅读的代码远比自己写的要多,使代码拥有良好的可读性且易于搜索非常重要。阅读变量名晦涩难懂的代码对读者来说是一种相当糟糕的体验。
让你的变量名易于搜索。
Bad:
// 525600 是什么?
for (var i = 0; i < 525600; i++) {
runCronJob();
}
Good:
// Declare them as capitalized `var` globals.
var MINUTES_IN_A_YEAR = 525600;
for (var i = 0; i < MINUTES_IN_A_YEAR; i++) {
runCronJob();
}
使用说明变量(即有意义的变量名)
Bad:
const cityStateRegex = /^(.+)[,\\s]+(.+?)\s*(\d{5})?$/;
saveCityState(cityStateRegex.match(cityStateRegex)[1], cityStateRegex.match(cityStateRegex)[2]);
Good:
const cityStateRegex = /^(.+)[,\\s]+(.+?)\s*(\d{5})?$/;
const match = cityStateRegex.match(cityStateRegex)
const city = match[1];
const state = match[2];
saveCityState(city, state);
不要绕太多的弯子
显式优于隐式。
Bad:
var locations = ['Austin', 'New York', 'San Francisco'];
locations.forEach((l) => {
doStuff();
doSomeOtherStuff();
...
...
...
// l是什么?
dispatch(l);
});
Good:
var locations = ['Austin', 'New York', 'San Francisco'];
locations.forEach((location) => {
doStuff();
doSomeOtherStuff();
...
...
...
dispatch(location);
});
避免重复的描述
当类/对象名已经有意义时,对其变量进行命名不需要再次重复。
Bad:
var Car = {
carMake: 'Honda',
carModel: 'Accord',
carColor: 'Blue'
};
function paintCar(car) {
car.carColor = 'Red';
}
Good:
var Car = {
make: 'Honda',
model: 'Accord',
color: 'Blue'
};
function paintCar(car) {
car.color = 'Red';
}
避免无意义的条件判断
Bad:
function createMicrobrewery(name) {
var breweryName;
if (name) {
breweryName = name;
}
else {
breweryName = 'Hipster Brew Co.';
}
}
Good:
function createMicrobrewery(name) {
var breweryName = name || 'Hipster Brew Co.'
}
函数
函数参数 (理想情况下应不超过 2 个)
限制函数参数数量很有必要,这么做使得在测试函数时更加轻松。过多的参数将导致难以采用有效的测试用例对函数的各个参数进行测试。
应避免三个以上参数的函数。通常情况下,参数超过两个意味着函数功能过于复杂,这时需要重新优化你的函数。当确实需要多个参数时,大多情况下可以考虑这些参数封装成一个对象。
JS 定义对象非常方便,当需要多个参数时,可以使用一个对象进行替代。
Bad:
function createMenu(title, body, buttonText, cancellable) {
...
}
Good:
var menuConfig = {
title: 'Foo',
body: 'Bar',
buttonText: 'Baz',
cancellable: true
}
function createMenu(menuConfig) {
...
}
函数功能的单一性
这是软件功能中最重要的原则之一。
功能不单一的函数将导致难以重构、测试和理解。功能单一的函数易于重构,并使代码更加干净。
Bad:
function emailClients(clients) {
clients.forEach(client => {
let clientRecord = database.lookup(client);
if (clientRecord.isActive()) {
email(client);
}
});
}
Good:
function emailClients(clients) {
clients.forEach(client => {
emailClientIfNeeded(client);
});
}
function emailClientIfNeeded(client) {
if (isClientActive(client)) {
email(client);
}
}
function isClientActive(client) {
let clientRecord = database.lookup(client);
return clientRecord.isActive();
}
函数名应明确表明其功能
Bad:
function dateAdd(date, month) {
// ...
}
let date = new Date();
// 很难理解dateAdd(date, 1)是什么意思
Good:
function dateAddMonth(date, month) {
// ...
}
let date = new Date();
dateAddMonth(date, 1);
函数应该只做一层抽象
当函数的需要的抽象多于一层时通常意味着函数功能过于复杂,需将其进行分解以提高其可重用性和可测试性。
Bad:
function parseBetterJSAlternative(code) {
let REGEXES = [
// ...
];
let statements = code.split(' ');
let tokens;
REGEXES.forEach((REGEX) => {
statements.forEach((statement) => {
// ...
})
});
let ast;
tokens.forEach((token) => {
// lex...
});
ast.forEach((node) => {
// parse...
})
}
Good:
function tokenize(code) {
let REGEXES = [
// ...
];
let statements = code.split(' ');
let tokens;
REGEXES.forEach((REGEX) => {
statements.forEach((statement) => {
// ...
})
});
return tokens;
}
function lexer(tokens) {
let ast;
tokens.forEach((token) => {
// lex...
});
return ast;
}
function parseBetterJSAlternative(code) {
let tokens = tokenize(code);
let ast = lexer(tokens);
ast.forEach((node) => {
// parse...
})
}
移除重复的代码
永远不要在任何循环下有重复的代码。
这种做法毫无意义且潜在危险极大。重复的代码意味着逻辑变化时需要对不止一处进行修改。JS 弱类型的特点使得函数拥有更强的普适性。考虑好好利用这一优点。
Bad:
function showDeveloperList(developers) {
developers.forEach(developer => {
var expectedSalary = developer.calculateExpectedSalary();
var experience = developer.getExperience();
var githubLink = developer.getGithubLink();
var data = {
expectedSalary: expectedSalary,
experience: experience,
githubLink: githubLink
};
render(data);
});
}
function showManagerList(managers) {
managers.forEach(manager => {
var expectedSalary = manager.calculateExpectedSalary();
var experience = manager.getExperience();
var portfolio = manager.getMBAProjects();
var data = {
expectedSalary: expectedSalary,
experience: experience,
portfolio: portfolio
};
render(data);
});
}
Good:
function showList(employees) {
employees.forEach(employee => {
var expectedSalary = employee.calculateExpectedSalary();
var experience = employee.getExperience();
var portfolio;
if (employee.type === 'manager') {
portfolio = employee.getMBAProjects();
}
else {
portfolio = employee.getGithubLink();
}
var data = {
expectedSalary: expectedSalary,
experience: experience,
portfolio: portfolio
};
render(data);
});
}
采用默认参数精简代码
Bad:
function writeForumComment(subject, body) {
subject = subject || 'No Subject';
body = body || 'No text';
}
Good:
function writeForumComment(subject = 'No subject', body = 'No text') {
//...
}
使用 Object.assign 设置默认属性
Bad:
var menuConfig = {
title: null,
body: 'Bar',
buttonText: null,
cancellable: true
}
function createMenu(config) {
config.title = config.title || 'Foo'
config.body = config.body || 'Bar'
config.buttonText = config.buttonText || 'Baz'
config.cancellable = config.cancellable === undefined ? config.cancellable : true;
}
createMenu(menuConfig);
Good:
var menuConfig = {
title: 'Order',
// User did not include 'body' key
buttonText: 'Send',
cancellable: true
}
function createMenu(config) {
config = Object.assign({
title: 'Foo',
body: 'Bar',
buttonText: 'Baz',
cancellable: true
}, config);
// config now equals: {title: "Order", body: "Bar", buttonText: "Send", cancellable: true}
// ...
}
createMenu(menuConfig);
不要使用标记(Flag)作为函数参数
这通常意味着函数的功能的单一性已经被破坏。此时应考虑对函数进行再次拆分。
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);
}
避免副作用
当函数产生了除了“接受一个值并返回一个结果”之外的行为时,称该函数产生了副作用。比如写文件、修改全局变量或将你的钱全转给了一个陌生人等。
程序在某些情况下确实需要副作用这一行为,如先前例子中的写文件。这时应该将这些功能集中在一起,不要用多个函数/类修改某个文件,用且只用一个 service 完成这一需求。
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.
var name = 'Ryan McDermott';
function splitIntoFirstAndLastName() {
name = name.split(' ');
}
splitIntoFirstAndLastName();
console.log(name); // ['Ryan', 'McDermott'];
Good:
function splitIntoFirstAndLastName(name) {
return name.split(' ');
}
var name = 'Ryan McDermott'
var newName = splitIntoFirstAndLastName(name);
console.log(name); // 'Ryan McDermott';
console.log(newName); // ['Ryan', 'McDermott'];
不要写全局函数
在 JS 中污染全局是一个非常不好的实践,这样做可能和其他库起冲突,且调用你的 API 的用户很难获知。
想象以下例子:
如果你想扩展 JS 中的 Array,为其添加一个 diff
函数显示两个数组间的差异,此时应如何去做?你可以将 diff 写入 Array.prototype
,但这么做会和其他有类似需求的库造成冲突。如果另一个库对 diff 的需求为比较一个数组中首尾元素间的差异呢?
使用 ES6 中的 class 对全局的 Array 做简单的扩展显然是更好的选择。
Bad:
Array.prototype.diff = function(comparisonArray) {
var values = [];
var hash = {};
for (var i of comparisonArray) {
hash[i] = true;
}
for (var i of this) {
if (!hash[i]) {
values.push(i);
}
}
return values;
}
Good:
class SuperArray extends Array {
constructor(...args) {
super(...args);
}
diff(comparisonArray) {
var values = [];
var hash = {};
for (var i of comparisonArray) {
hash[i] = true;
}
for (var i of this) {
if (!hash[i]) {
values.push(i);
}
}
return values;
}
}
采用函数式编程
函数式的编程具有更干净且便于测试的特点,尽可能的使用函数式编程。
Bad:
const programmerOutput = [
{
name: 'Uncle Bobby',
linesOfCode: 500
}, {
name: 'Suzie Q',
linesOfCode: 1500
}, {
name: 'Jimmy Gosling',
linesOfCode: 150
}, {
name: 'Gracie Hopper',
linesOfCode: 1000
}
];
var totalOutput = 0;
for (var i = 0; i < programmerOutput.length; i++) {
totalOutput += programmerOutput[i].linesOfCode;
}
Good:
const programmerOutput = [
{
name: 'Uncle Bobby',
linesOfCode: 500
}, {
name: 'Suzie Q',
linesOfCode: 1500
}, {
name: 'Jimmy Gosling',
linesOfCode: 150
}, {
name: 'Gracie Hopper',
linesOfCode: 1000
}
];
var totalOutput = programmerOutput
.map((programmer) => programmer.linesOfCode)
.reduce((acc, linesOfCode) => acc + linesOfCode, 0);
封装判断条件
Bad:
if (fsm.state === 'fetching' && isEmpty(listNode)) {
/// ...
}
Good:
function shouldShowSpinner(fsm, listNode) {
return fsm.state === 'fetching' && isEmpty(listNode);
}
if (shouldShowSpinner(fsmInstance, listNodeInstance)) {
// ...
}
避免“否定情况”的判断
Bad:
function isDOMNodeNotPresent(node) {
// ...
}
if (!isDOMNodeNotPresent(node)) {
// ...
}
Good:
function isDOMNodePresent(node) {
// ...
}
if (isDOMNodePresent(node)) {
// ...
}
避免条件判断
这看起来似乎不太可能。
大多人听到这的第一反应是:“怎么可能不用 if 完成功能区分?” 其实,许多情况下通过多态(polymorphism)可以达到同样目的。
第二个问题在于避免条件判断的原因是什么?答案我们之前提到过:保持函数功能的单一性。
Bad:
class Airplane {
//...
getCruisingAltitude() {
switch (this.type) {
case '777':
return getMaxAltitude() - getPassengerCount();
case 'Air Force One':
return getMaxAltitude();
case 'Cessna':
return getMaxAltitude() - getFuelExpenditure();
}
}
}
Good:
class Airplane {
//...
}
class Boeing777 extends Airplane {
//...
getCruisingAltitude() {
return getMaxAltitude() - getPassengerCount();
}
}
class AirForceOne extends Airplane {
//...
getCruisingAltitude() {
return getMaxAltitude();
}
}
class Cessna extends Airplane {
//...
getCruisingAltitude() {
return getMaxAltitude() - getFuelExpenditure();
}
}
避免类型判断(part 1)
JS 是弱类型语言,这意味着函数可接受任意类型的参数。
这有时带来麻烦,你需要参数做一些类型判断。但有许多方法可以避免这些情况。
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'));
}
避免类型判断(part 2)
如果需处理的数据为字符串,整型,数组等类型,无法使用多态并仍有必要对其进行类型检测时,可以考虑使用 TypeScript。
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:
// 这里使用变量len是因为在老式浏览器中,
// 直接使用正例中的方式会导致每次循环均重复计算list.length的值,
// 而在现代浏览器中会自动完成优化,这一行为是没有必要的
for (var i = 0, len = list.length; i < len; i++) {
// ...
}
Good:
for (var i = 0; i < list.length; i++) {
// ...
}
删除无效的代码
不再被调用的代码应及时删除。
Bad:
function oldRequestModule(url) {
// ...
}
function newRequestModule(url) {
// ...
}
var req = newRequestModule;
inventoryTracker('apples', req, 'www.inventory-awesome.io');
Good:
function newRequestModule(url) {
// ...
}
var req = newRequestModule;
inventoryTracker('apples', req, 'www.inventory-awesome.io');
对象和数据结构
使用 getters 和 setters
JS 没有接口或类型,因此实现这一模式是很困难的,因为我们并没有类似 public
和 private
的关键词。
而使用 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:
var Employee = function(name) {
this.name = name;
}
Employee.prototype.getName = function() {
return this.name;
}
var employee = new Employee('John Doe');
console.log('Employee name: ' + employee.getName()); // Employee name: John Doe
delete employee.name;
console.log('Employee name: ' + employee.getName()); // Employee name: undefined
Good:
var Employee = (function() {
function Employee(name) {
this.getName = function() {
return name;
};
}
return Employee;
}());
var employee = new Employee('John Doe');
console.log('Employee name: ' + employee.getName()); // Employee name: John Doe
delete employee.name;
console.log('Employee name: ' + employee.getName()); // Employee name: John Doe