ryanmcdermott/clean-code-javascript
摘要
一本基于罗伯特·C·马丁《Clean Code》原则的指南,教你编写清洁、可读且易于维护的JavaScript代码,涵盖变量、函数、类、测试等内容。
查看缓存全文
缓存时间: 2026/07/02 11:33
ryanmcdermott/clean-code-javascript
来源:https://github.com/ryanmcdermott/clean-code-javascript
干净代码 JavaScript
目录
简介
软件质量评估的幽默图片:根据阅读代码时骂人的次数来衡量
软件工程原则来自 Robert C. Martin 的著作《Clean Code》(https://www.amazon.com/Clean-Code-Handbook-Software-Craftsmanship/dp/0132350882),并针对 JavaScript 进行了改编。这不是一份风格指南,而是一份指导如何用 JavaScript 编写可读、可复用且可重构(https://github.com/ryanmcdermott/3rs-of-software-architecture)软件的指南。
并非每条原则都必须严格遵守,更不会得到所有人的一致认可。它们只是指导方针,但也是《Clean Code》作者多年集体经验所总结出来的。
我们的软件工程工艺才刚过 50 年,我们仍在学习大量东西。当软件架构像建筑学本身一样古老时,也许我们才会有更严格的规则可循。目前,让这些指南作为评估你和团队所编写的 JavaScript 代码质量的试金石吧。
还有一件事:知道这些不会立刻让你成为更好的软件开发者,而且与它们共事多年也不意味着你不会犯错。每段代码都始于初稿,就像湿粘土被塑造成最终形态。最后,我们通过同伴审查来雕琢掉瑕疵。不要因为初稿需要改进而自责,要改进的是代码本身!
变量
使用有意义的、可发音的变量名
差:
const yyyymmdstr = moment().format("YYYY/MM/DD");
好:
const currentDate = moment().format("YYYY/MM/DD");
对同一类型的变量使用相同的词汇
差:
getUserInfo();
getClientData();
getCustomerRecord();
好:
getUser();
使用可搜索的名称
我们读的代码会比写的多得多。确保我们写的代码可读且可搜索非常重要。如果变量名对理解程序不起作用,就会伤害读者。让你的名称易于搜索。像 buddy.js(https://github.com/danielstjules/buddy.js)和 ESLint(https://github.com/eslint/eslint/blob/660e0918933e6e7fede26bc675a0763a6b357c94/docs/rules/no-magic-numbers.md)这样的工具可以帮助识别未命名的常量。
差:
// 86400000 到底是干嘛的?
setTimeout(blastOff, 86400000);
好:
// 将它们声明为大写的命名常量
const MILLISECONDS_PER_DAY = 60 * 60 * 24 * 1000; //86400000;
setTimeout(blastOff, MILLISECONDS_PER_DAY);
使用解释性变量
差:
const address = "One Infinite Loop, Cupertino 95014";
const cityZipCodeRegex = /^[^,\\]+[,\\\s]+(.+?)\s*(\d{5})?$/;
saveCityZipCode(
address.match(cityZipCodeRegex)[1],
address.match(cityZipCodeRegex)[2]
);
好:
const address = "One Infinite Loop, Cupertino 95014";
const cityZipCodeRegex = /^[^,\\]+[,\\\s]+(.+?)\s*(\d{5})?$/;
const [_, city, zipCode] = address.match(cityZipCodeRegex) || [];
saveCityZipCode(city, zipCode);
避免心理映射
显式优于隐式。
差:
const locations = ["Austin", "New York", "San Francisco"];
locations.forEach(l => {
doStuff();
doSomeOtherStuff();
// ...
// ...
// ...
// 等等,`l` 到底代表什么?
dispatch(l);
});
好:
const locations = ["Austin", "New York", "San Francisco"];
locations.forEach(location => {
doStuff();
doSomeOtherStuff();
// ...
// ...
// ...
dispatch(location);
});
不要添加不必要的上下文
如果你的类/对象名已经表明了含义,就不要在变量名中重复它。
差:
const Car = {
carMake: "Honda",
carModel: "Accord",
carColor: "Blue"
};
function paintCar(car, color) {
car.carColor = color;
}
好:
const Car = {
make: "Honda",
model: "Accord",
color: "Blue"
};
function paintCar(car, color) {
car.color = color;
}
使用默认参数代替短路或条件判断
默认参数通常比短路更简洁。注意:如果你使用默认参数,函数只会在参数为 undefined 时提供默认值。其他“假值”如 ''、""、false、null、0 和 NaN 不会被默认值替代。
差:
function createMicrobrewery(name) {
const breweryName = name || "Hipster Brew Co.";
// ...
}
好:
function createMicrobrewery(name = "Hipster Brew Co.") {
// ...
}
函数
函数参数(理想情况下少于或等于 2 个)
限制函数参数的数量极为重要,因为它能让函数更易于测试。超过三个参数会导致组合爆炸,你需要针对每个独立参数测试大量不同的情况。理想情况是一个或两个参数,应尽量避免三个。超过三个应该合并。通常,如果你有两个以上参数,说明函数试图做的事情太多。如果并非如此,大多数情况下一个更上层的对象就足以作为参数。
由于 JavaScript 允许你在不编写大量类模板的情况下创建对象,当你发现自己需要大量参数时,可以使用一个对象。为了让函数期望的属性一目了然,你可以使用 ES2015/ES6 的解构语法。这有几个优点:
- 当有人查看函数签名时,可以立即清楚正在使用哪些属性。
- 它可以用来模拟命名参数。
- 解构还会克隆传递给函数的参数对象中的指定原始值。这有助于防止副作用。注意:从参数对象解构出来的对象和数组不会被克隆。
- 代码检查工具可以警告你未使用的属性,否则不可能做到这一点。
差:
function createMenu(title, body, buttonText, cancellable) {
// ...
}
createMenu("Foo", "Bar", "Baz", true);
好:
function createMenu({ title, body, buttonText, cancellable }) {
// ...
}
createMenu({
title: "Foo",
body: "Bar",
buttonText: "Baz",
cancellable: true
});
函数应该只做一件事
这绝对是软件工程中最重要的规则。当函数做多件事时,它们就更难组合、测试和推理。当你能将函数隔离到只做一个动作时,就能轻松重构,代码也会更清晰。如果你从本指南中只学到这一条,你已经领先许多开发者了。
差:
function emailClients(clients) {
clients.forEach(client => {
const clientRecord = database.lookup(client);
if (clientRecord.isActive()) {
email(client);
}
});
}
好:
function emailActiveClients(clients) {
clients.filter(isActiveClient).forEach(email);
}
function isActiveClient(client) {
const clientRecord = database.lookup(client);
return clientRecord.isActive();
}
函数名应该说明它做什么
差:
function addToDate(date, month) {
// ...
}
const date = new Date();
// 从函数名很难看出加了什么
addToDate(date, 1);
好:
function addMonthToDate(month, date) {
// ...
}
const date = new Date();
addMonthToDate(1, date);
函数应该只包含一层抽象
当你有超过一层抽象时,函数通常做太多事情。拆解函数可以提高复用性并简化测试。
差:
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...
});
}
好:
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 原则。糟糕的抽象可能比重复代码更糟糕,所以要小心!话虽如此,如果你能做出好的抽象,那就去做吧!不要重复自己,否则每次想改一个东西时,你都会发现需要更新多个地方。
差:
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);
});
}
好:
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 设置默认对象
差:
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);
好:
const menuConfig = {
title: "Order",
// 用户没有包含 'body' 键
buttonText: "Send",
cancellable: true
};
function createMenu(config) {
let finalConfig = Object.assign(
{
title: "Foo",
body: "Bar",
buttonText: "Baz",
cancellable: true
},
config
);
return finalConfig
// config 现在等于: {title: "Order", body: "Bar", buttonText: "Send", cancellable: true}
// ...
}
createMenu(menuConfig);
不要将标志作为函数参数
标志告诉用户这个函数做了不止一件事。函数应该只做一件事。如果函数根据布尔值走不同的代码路径,请拆分为多个函数。
差:
function createFile(name, temp) {
if (temp) {
fs.create(`./temp/${name}`);
} else {
fs.create(name);
}
}
好:
function createFile(name) {
fs.create(name);
}
function createTempFile(name) {
createFile(`./temp/${name}`);
}
避免副作用(第一部分)
如果一个函数除了接收一个值并返回另一个值之外还做了其他事情,它就产生了副作用。副作用可能包括写入文件、修改全局变量,或者意外地把所有钱转给陌生人。当然,程序中有时确实需要副作用。就像前面的例子,你可能需要写入文件。你需要做的是集中处理副作用的位置。不要让多个函数和类写入同一个特定文件。用一个服务来做这件事,且只有一个。
主要目的是避免常见的陷阱,比如在没有任何结构的情况下在对象之间共享状态,使用任何东西都可以写入的可变数据类型,以及不集中处理副作用发生的位置。如果你能做到这一点,你会比绝大多数其他程序员更快乐。
差:
// 被后面函数引用的全局变量
// 如果有另一个函数也使用了这个 name,它现在变成了数组,可能会导致问题
let name = "Ryan McDermott";
function splitIntoFirstAndLastName() {
name = name.split(" ");
}
splitIntoFirstAndLastName();
console.log(name); // ['Ryan', 'McDermott'];
好:
function splitIntoFirstAndLastName(name) {
return name.split(" ");
}
const name = "Ryan McDermott";
const newName = splitIntoFirstAndLastName(name);
console.log(name); // 'Ryan McDermott';
console.log(newName); // ['Ryan', 'McDermott'];
避免副作用(第二部分)
在 JavaScript 中,有些值是不可变的,有些是可变的。对象和数组是两种可变值,所以当它们作为参数传递给函数时要小心处理。JavaScript 函数可以更改对象的属性或修改数组的内容,这可能轻易导致其他地方的 bug。
假设有一个函数接受一个表示购物车的数组参数。如果这个函数修改了购物车数组——例如添加一个要购买的商品——那么任何其他使用同一个 cart 数组的函数都会受到这个添加的影响。这可能很好,但也可能很糟糕。
设想一个糟糕的情况:用户点击“购买”按钮,调用一个 purchase 函数,该函数发起网络请求并将 cart 数组发送到服务器。由于网络连接不好,purchase 函数必须不断重试请求。现在,如果在网络请求开始之前,用户不小心点击了某个他们不想要的商品上的“添加到购物车”按钮,会发生什么?如果发生这种情况,然后网络请求开始了,那么购买函数就会发送那个……
(由于内容被截断,此处为原文结束。但根据上下文,下一条将继续阐述。)
相似文章
@freeCodeCamp:编写整洁代码有助于构建可扩展且可维护的软件应用。在这本手册中,@shahancd 解释了…
一本解释整洁代码原则和模式的手册,用于构建可扩展的软件,包含 JavaScript 示例。
@vintcessun: 1个让无数团队头疼的问题终于有了解法。写JavaScript最怕代码一天后自己都看不懂,变量命名随性、函数又长又杂。这个项目把《Clean Code》的工程原则适配到JS,每个原则都配bad/good对比,告诉你为什么那样改。说白了,它解…
该项目将《Clean Code》的工程原则适配到JavaScript,提供每个原则的bad/good对比,帮助开发者写出可读、可复用、可重构的代码,解决团队协作中代码腐烂的问题。
我不再在 JavaScript 里把所有东西链在一起
开发者 Matt Smith 解释,为了调试更轻松、性能更好,他现在在 JavaScript 中更偏爱一步步写代码,而不是冗长的方法链。
为什么选择原生JavaScript
一篇讨论不使用库或框架的原生JavaScript的优点和用例的文章。
@javinpaul: 3 本塑造我职业生涯的书籍,感谢 @unclebobmartin 1. 《代码整洁之道》 https://amazon.com/Clean-Code-Handbook-Software-Cr…
一条来自 @javinpaul 的推文推荐了《代码整洁之道》及其第二版,重点介绍了对软件工匠精神和不同编程语言编码实践的更新见解。