《Clean Code》代码整洁之道, 是每个 Java 开发者编码规范的圣经。但是属于JS的代码整洁之道似乎很少有人提及,更多的只是所谓的“约定”。在github上偶遇 clean-code-javascript 后有受启发,作者主要从变量(self-documentation),函数(一个函数只做一件事),类等方面展开了论述。
变量 Variables
- 使用有意义语义化的变量名
不推荐示例:
const yyyymmdstr = moment().format("YYYY/MM/DD"); // 当前年-月-天的字符串
推荐示例:
const currentDate = moment().format("YYYY/MM/DD"); // 当前日期
ps: moment.js 是一款功能非常强大的 日期/时间 库。
- 对于同样功能的方法使用同样的方法名,体现在不同的 js 文件中
不推荐示例:
getUserInfo(); // 获取用户信息
getClientData(); // 获取顾客数据
getCustomerRecord(); // 获取客户记录
推荐示例:
getUser(); // 获取用户,上述反例中,哪怕信息不是类似的,对用户的命名能否统一叫 user/client/customer 中的一个呢?
- 定义常量,让其更具语义化,更易于搜索
不推荐示例:
setTimeout(fly, 86400000); // WTF 86400000 ?
推荐示例:
const MILLSECONDS_IN_A_DAY = 86400000; // 86400000 是一天的毫秒数
setTimeout(fly, MILLSECONDS_IN_A_DAY ); // 该类常量,作者是大写加下划线的方式的格式来定义
- 让函数的参数传递变量,且让变量的命名更有意义
不推荐示例:
const address = "One Infinite Loop, Cupertino 950140";
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 areas = ["南山区", “福田区”, “罗湖区”];
areas.forEach(a => {
buyIt(a)
})
推荐示例:
const areas = ["南山区", “福田区”, “罗湖区”];
areas.forEach(area => {
buyIt(area)
})
- 通过 对象/类 来管理数据时,不必添加一些没必要的字符
不推荐示例:
const Car = {
carMake: "BYD",
carModel: "唐",
carColor: "Black"
}
推荐示例:
// 已经定义为已知的 Car 类了,它的属性就不必使用 carXX 的命名了
const Car = {
make: "BYD",
model: "唐",
color: "Black"
}
- 根据使用情境,合理使用默认值
不推荐示例:
function getUser (name) {
const girlName = name || "Lucy";
// ...
}
推荐示例:
function getUser (name = "Lucy") {
// ...
}
// 合理利用结构赋值
function getUser ({ name = 'Lucy', age = 18, sex: gender }) {}
函数 Functions
- 参数尽可能避免超过3个,多参数时,可以使用对象管理
不推荐示例:
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 => {
tokens.push(/* ... */)
})
})
const ast = [];
tokens.forEach(token => {
// ...
})
ast.forEach(node=> {
// ...
})
}
推荐示例:
function parseBetterJSAlternative (code) {
const tokens = tokenize(code);
const syntaxTress = 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;
}
- 删除重复的代码。否则涉及到变更时,需要变更多处
通常,重复的代码因为有两个或稍多个不同的事物,它们有很多共同点,但是它们之间仍有一些细微的差别,是其书写多个函数来完成许多相同的事情。删除重复代码意味着创建一个仅用一个函数、模块、类就可以处理这组不同事物的抽象。
推荐示例:
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;
default:
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) {
config = Object.assign(
{
title: "Foo",
body: "Bar",
buttonText: "Baz",
cancellable: true
},
config
)
}
createMenu(menuConfig);
- 当标记位作为函数的参数之一且为Bolean值时,拆分成2个函数
作者指的是temp标记位当可以用布尔值来区分(if they are following different code paths based on a boolean)时,应该拆分成单个函数。我的理解是,如果是 temp 是 type 用来区分类型,例如 redux 中的 actions/dispatch,应该不在此范畴。
不推荐示例:
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}`)
}
- 避免副作用(part 1)
一个函数除了接受值并返回值,或者不执行任务其他操作以外,都会产生副作用。副作用可能是写入文件,修改某些全局变量等等。现在,确实需要偶尔在程序中产生副作用,但是要避免常见的陷阱。
不推荐示例:
// name的结构被破坏,String变成了Array
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']
- 避免副作用(part 2)
作为函数参数时,基本类型的参数是值的传递,但是对象、数组是引用的传递。如果直接对参数进行修改,该引用的所有变量都会发生变化,造成非预期的风险。最好的做法是始终返回克隆。
警告:某些情况下,实际上可能就想修改输入对象,但是这种习惯并不好,推荐返回更没有副作用的克隆值;克隆大对象相较于直接修改输入对象,会消耗更多性能,作者推荐使用工具 Immutable.js,这个在 React 项目中经常用到。
这个可能是很多人容易忽视的,起码我在以往的项目中,遇见的太多这样的问题了。尤其还是在使用的 Vue 框架下,导致watch/bus/Dep 的触发很难定位,代码重构的非预期风险也很高。
不推荐示例:
const addItemToCart = (cart, item) => {
// 所有cart的引用的数组均会发生变化
cart.push({ item, date: Date.now() })
}
推荐示例:
const addItemToCart = (cart, item) => {
return [...cart, { item, date: Date.now() }]
}
- 不要定义全局函数,污染全局变量在 JavaScript 中是一种不好的做法
不推荐示例:
// 可能与某些库产生冲突,发生问题后很难定位
Array.prototype.diff = function diff (comparisonArray) {
const hash = new Set(comparisonArray);
return this.filter(elem => !hash.has(elem));
};
推荐的示例:
class SuperArray extends Array {
diff (comparisonArray) {
const hash = new Set(comparisonArray);
return this.filter(elem => !hash.has(elem));
}
}
- 使用函数式编程而非命令式编程
不推荐示例:
const programmerOutput = [
{
name: 'Uncle Bobby',
linesOfCode: 500
},
{
name: 'Suzie Q',
linesOfCode: 1500
}
]
let totalOutput = 0;
for (let i = 0; i < programmerOutput.length; i++) {
totalOutput += programmerOutput[i].linesOfCode;
}
推荐示例:
const programmerOutput = [
{
name: 'Uncle Bobby',
linesOfCode: 500
},
{
name: 'Suzie Q',
linesOfCode: 1500
}
]
const totalOutput = programmerOutput.reduce(
(totalLines, output) => totalLines + output.linesOfCode,
0
);
- 使用函数封装判断条件
不推荐示例:
if (fsm.state === 'fetching' && isEmpty(listNode)) {
// ...
}
推荐示例:
function shouldShowSpinner (fsm, listNode) {
return fsm.state === 'fetching' && isEmpty(listNode);
}
if (shouldShowSpinner(fsmInstance, listNodeInstance)) {
// ...
}
- 避免使用false判断条件
这不仅应该体现在返回函数中,应该体现在各处的单一判断中,尽可能使用true判断,因为undefined, null, false, 0, '', 转换为Boolean都是false,这可能不会是你所期望的。
不推荐示例:
function isDOMNode(node) {
// ...
}
if (!isDOMNode(node)) {
// ...
}
推荐示例:
function isDOMNode(node) {
// ...
}
if (isDOMNode(node)) {
// ...
}
- 避免使用条件判断
这个说法初听起来似乎莫名其妙,但这也是作者“函数只处理一件事”的体现。
不推荐示例:
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();
}
}
}
推荐示例:
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();
}
}
- 避免类型检查
除非使用TypeScript,或者“多态”共有的属性或方法,否则类型判断无法避免。空指针异常导致代码的阻塞,风险较大。所以对作者的该观点,各位仁者见仁吧。以下是作者提供的示例:
不推荐示例:
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");
}
推荐示例:
function combine(val1, val2) {
return val1 + val2;
}
- 不要过度优化
现代浏览器在运行时在后台进行了大量的优化。很多时候的优化,只是浪费时间。列如for循环中的遍历对象的长度的重新计算,在现代浏览器中,这是经过优化的。
- 删除无效代码(没什么好说的)
类 Classes
- 通过类 class extends 来实现继承(没什么好说的)
- 使用方法链
这种模式在JavaScript中非常有用,可以在jQuery和Lodash等许多库中看到它。它使得代码更具表现力,并且不再那么冗长。
不推荐示例:
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();
推荐示例:
class Car {
constructor (make, model, color) {
this.make = make;
this.model = model;
this.color = color;
}
setMake (make) {
this.make = make;
return this;
}
setModel (model) {
this.model = model;
return this;
}
setColor (color) {
this.color = color;
return this;
}
save () {
console.log(this.make, this.model, this.color);
return this;
}
}
// 通过返回this,实现了方法的链式调用。
const car = new Car("Ford", "F-150", "red").setColor("pink").save();
异步/并发 Async/Concurrency
- 使用回调Callback不如使用promise,使用resolve.then,不如使用async/await。async/await 更符合代码阅读习惯。
Callback:
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");
}
});
}
}
);
Promise:
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:
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()
异常处理 Error Handling
- 不要不处理捕获的异常
不推荐示例:
try {
functionThatMightThrow();
} catch (error) {
console.log(error);
}
推荐示例:
try {
functionThatMightThrow();
} catch (error) {
console.error(error);
notifyUserOfError(error);
reportErrorToService(error);
}
- 不要不处理Promise中的reject状态
不推荐示例:
getdata()
.then(data => {
functionThatMightThrow(data);
})
.catch(error => {
console.log(error);
});
推荐示例:
getdata()
.then(data => {
functionThatMightThrow(data);
})
.catch(error => {
console.error(error);
notifyUserOfError(error);
reportErrorToService(error);
});
格式 Formatting
格式化是主观的。像本文中的许多规则一样,并没有必须遵循的一成不变的规则。要点是不要过分格式化。
- 使用统一的规范。使用驼峰、大小写、下划线,是主观的,但是团队要保持统一、一致。
不推荐示例:
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() {}
推荐示例:
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() {}
- 函数的调用者与被调用者的书写顺序保持在上下方。这样更符合代码的阅读习惯。
注释 Comments
- 仅注释业务逻辑教复杂的事物,注释并不是必需的,好的代码本身就是说明文档。
- 不要在代码库遗留注释过的代码,版本控制本身会保留历史代码。