概述
Robert C. Martin 在 《代碼整潔之道》{:target="_blank"} 中提到的軟件工程原則,同樣適用于 JavaScript。這不是一個風格參考。它指導如何用 JavaScript 編寫可讀、可復用、可重構的軟件。
并不是每一個原則都必須嚴格遵循,甚至很少得到大家的認同。它們僅用于參考,不過要知道這些原則都是《代碼整潔之道》的作者們累積多年的集體經驗。
我們在軟件工程方面的技術發展剛剛超過 50 年,我們仍然在學習很多東西。當軟件架構和架構本身一樣古老的時候,我們應該遵循更為嚴格規則。現在,對于你和你的團隊編寫的 JavaScript 代碼,不妨依據這些準則來進行質量評估。
還有一件事:知道這些不會馬上讓你成為更好的軟件開發者,在工作中常年使用這些準則不能讓你避免錯誤。每一段代碼都從最初的草圖開始到最終成型,就像為濕粘土塑形一樣。最后,當我們與同行一起審查的時候,再把不完美的地方消除掉。不要因為初稿需要改善而否定自己,需要要否定的只是那些代碼!
變量
使用有準確意義的變量名
不好:
var yyyymmdstr = moment().format('YYYY/MM/DD');
好:
var yearMonthDay = moment().format('YYYY/MM/DD');
在變量的值不會改變時使用 ES6 的常量
在不好的示例中,變量可以被改變。如果你申明一個常量,它會在整個程序中始終保持不變。
不好:
var FIRST_US_PRESIDENT = "George Washington";
好:
const FIRST_US_PRESIDENT = "George Washington";
對同一類型的變量使用相同的詞匯
不好:
getUserInfo();getClientData();getCustomerRecord();
好:
getUser();
使用可檢索的名稱
我們閱讀的代碼永遠比寫的折。寫可讀性強、易于檢索的的代碼非常重要。在程序中使用無明確意義的變量名會難以理解,對讀者造成傷害。所以,把名稱定義成可檢索的。
不好:
// 見鬼,525600 是個啥?
for (var i = 0; i < 525600; i++) {
runCronJob();
}
好:
// 用 `var` 申明為大寫的全局變量
var MINUTES_IN_A_YEAR = 525600;for (var i = 0; i < MINUTES_IN_A_YEAR; i++) {
runCronJob();
}
使用解釋性的變量
不好:
const cityStateRegex = /^(.+)[,\\s]+(.+?)\s*(\d{5})?$/;
saveCityState(cityStateRegex.match(cityStateRegex)[1], cityStateRegex.match(cityStateRegex)[2]);
好:
const cityStateRegex = /^(.+)[,\\s]+(.+?)\s*(\d{5})?$/;
const match = cityStateRegex.match(cityStateRegex);
const city = match[1];
const state = match[2];saveCityState(city, state);
避免暗示
顯式優于隱式。
不好:
var locations = ['Austin', 'New York', 'San Francisco'];locations.forEach((l) => {
doStuff();
doSomeOtherStuff();
...
...
...
// 等等,`l` 又是什么?
dispatch(l);
});
好:
var locations = ['Austin', 'New York', 'San Francisco'];locations.forEach((location) => {
doStuff();
doSomeOtherStuff();
...
...
...
dispatch(location);
});
不要添加沒必要的上下文
如果你的類名稱/對象名稱已經說明了它們是什么,不要在(屬性)變量名里重復。
不好:
var Car = {
carMake: 'Honda',
carModel: 'Accord',
carColor: 'Blue'
};
function paintCar(car) {
car.carColor = 'Red';
}
好:
var Car = {
make: 'Honda',
model: 'Accord',
color: 'Blue'
};
function paintCar(car) {
car.color = 'Red';
}
短路語法比條件語句更清晰
不好:
function createMicrobrewery(name) {
var breweryName;
if (name) {
breweryName = name;
} else {
breweryName = 'Hipster Brew Co.';
}
}
好:
function createMicrobrewery(name) {
var breweryName = name || 'Hipster Brew Co.'
}
函數
函數參數 (理論上少于等于2個)
限制函數參數的數量極為重要,它會讓你更容易測試函數。超過3個參數會導致組合膨脹,以致于你必須根據不同的參數對大量不同的情況進行測試。
理想情況下是沒有參數。有一個或者兩個參數也還好,三個就應該避免了。多于那個數量就應該考慮合并。通常情況下,如果你有多于2個參數,你的函數會嘗試做太多事情。如果不是這樣,大多數時候可以使用一個高階對象作為參數使用。
既然 JavaScript 允許我們在運行時隨意創建對象,而不需要預先定義樣板,那么你在需要很多參數的時候就可以使用一個對象來處理。
不好:
function createMenu(title, body, buttonText, cancellable) {
...
}
好:
var menuConfig = {
title: 'Foo',
body: 'Bar',
buttonText: 'Baz',
cancellable: true
}
function createMenu(menuConfig) {
...
}
一個函數只做一件事
目前這是軟件工程中最重要的原則。如果函數做了較多的事情,它就難以組合、測試和推測。當你讓函數只做一件事情的時候,它們就很容易重構,而且代碼讀起來也會清晰得多。你只需要遵循本指南的這一條,就能領先于其他很多開發者。
不好:
function emailClients(clients) {
clients.forEach(client => {
let clientRecord = database.lookup(client);
if (clientRecord.isActive()) {
email(client);
}
});
}
好:
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();
}
函數名稱要說明它做的事
不好:
function dateAdd(date, month) {
// ...
}
let date = new Date();
// 很難從函數名了解到加了什么dateAdd(date, 1);
好:
function dateAddMonth(date, month) {
// ...
}
let date = new Date();
dateAddMonth(date, 1);
函數應該只抽象一個層次
如果你有多個層次的抽象,那么你的函數通常做了太多事情,此時應該拆分函數使其易于復用和易于測試。
不好:
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...
})
}
好:
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...
})
}
刪除重復代碼
任何情況下,都不要有重復的代碼。沒有任何原因,它很可能是阻礙你成為專業開發者的最糟糕的一件事。重復代碼意味著你要修改某些邏輯的時候要修改不止一個地方的代碼。JavaScript 是弱類型語句,所以它很容易寫通用性強的函數。記得利用這一點!
不好:
function showDeveloperList(developers) {
developers.forEach(developers => {
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);
});
}
好:
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);
});
}
使用默認參數代替短路表達式
不好:
function writeForumComment(subject, body) {
subject = subject || 'No Subject';
body = body || 'No text';
}
好:
function writeForumComment(subject = 'No subject', body = 'No text') {
...
}
用 Object.assign 設置默認對象
不好:
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);
好:
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 等于: {title: "Foo", body: "Bar", buttonText: "Baz", cancellable: true}
// ...
}
createMenu(menuConfig);
不要把標記用作函數參數
標記告訴你的用戶這個函數做的事情不止一件。但是函數應該只做一件事。如果你的函數中會根據某個布爾參數產生不同的分支,那就拆分這個函數。
不好:
function createFile(name, temp) {
if (temp) {
fs.create('./temp/' + name);
} else {
fs.create(name);
}
}
好:
function createTempFile(name) {
fs.create('./temp/' + name);
}
function createFile(name) {
fs.create(name);
}
避免副作用
如果一個函數不是獲取一個輸入的值并返回其它值,它就有可能產生副作用。這些副作用可能是寫入文件、修改一些全局變量,或者意外地把你所有錢轉給一個陌生人。
現在你確實需要在程序中有副作用。像前面提到的那樣,你可能需要寫入文件。現在你需要做的事情是搞清楚在哪里集中完成這件事情。不要使用幾個函數或類來完成寫入某個特定文件的工作。采用一個,就一個服務來完成。
關鍵點是避免覺的陷阱,比如在沒有結構的對象間共享狀態,使用可以被任意修改的易變的數據類型,沒有集中處理發生的副作用等。如果你能做到,你就能比其他大多數程序員更愉快。
不好:
// 下面的函數使用了全局變量。
// 如果有另一個函數在使用 name,現在可能會因為 name 變成了數組而不能正常運行。
var name = 'Ryan McDermott';
function splitIntoFirstAndLastName() {
name = name.split(' ');
}
splitIntoFirstAndLastName();
console.log(name);
// ['Ryan', 'McDermott'];
好:
function splitIntoFirstAndLastName(name) {
return name.split(' ');
}
var name = 'Ryan McDermott';
var newName = splitIntoFirstAndLastName(name);
console.log(name);
// 'Ryan McDermott';console.log(newName);
// ['Ryan', 'McDermott'];
不要寫入全局函數
JavaScript 中全局污染是一件糟糕的事情,因為它可能和另外庫發生沖突,然而使用你 API 的用戶卻不會知道——直到他們在生產中遇到一個異常。來思考一個例子:你想擴展 JavaScript 的原生 Array,使之擁有一個 diff 方法,用來展示兩數據之前的區別,這時你會怎么做?你可以給 Array.prototype 添加一個新的函數,但它可能會與其它想做同樣事情的庫發生沖突。如果那個庫實現的 diff 只是比如數組中第一個元素和最后一個元素的異同會發生什么事情呢?這就是為什么最好是使用 ES6 的類語法從全局的 Array 派生一個類來做這件事。
不好:
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;
}
好:
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;
}
}
喜歡上命令式編程之上的函數式編程
如果 Haskell 是 IPA 那么 JavaScript 就是 O'Douls。就是說,與 Haskell 不同,JavaScript 不是函數式編程語言,不過它仍然有一點函數式的意味。函數式語言更整潔也更容易測試,所以你最好能喜歡上這種編程風格。
不好:
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;
}
好:
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);
封裝條件
不好:
if (fsm.state === 'fetching' && isEmpty(listNode)) {
/// ...
}
好:
function shouldShowSpinner(fsm, listNode) {
return fsm.state === 'fetching' && isEmpty(listNode);
}
if (shouldShowSpinner(fsmInstance, listNodeInstance)) {
// ...
}
避免否定條件
不好:
function isDOMNodeNotPresent(node) {
// ...
}
if (!isDOMNodeNotPresent(node)) {
// ...
}
好:
function isDOMNodePresent(node) {
// ...
}
if (isDOMNodePresent(node)) {
// ...
}
避免條件
這似乎是個不可能完成的任務。大多數人第一次聽到這個的時候會說,“沒有 if 語句我該怎么辦?”回答是在多數情況下都可以使用多態來實現相同的任務。第二個問題通常是,“那太好了,不過我為什么要這么做呢?”答案在于我們之前了解過整潔的概念:一個函數應該只做一件事情。如果你的類和函數有 if 語句,就意味著你的函數做了更多的事。記住,只做一件事。
不好:
class Airplane {
//...
getCruisingAltitude() {
switch (this.type) {
case '777':
return getMaxAltitude() - getPassengerCount();
case 'Air Force One':
return getMaxAltitude();
case 'Cessna':
return getMaxAltitude() - getFuelExpenditure();
}
}
}
好:
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();
}
}
避免類型檢查(第1部分)
JavaScript 是無類型的,也就是說函數可以獲取任意類型的參數。有時候你會覺得這種自由是種折磨,因而會不由自主地在函數中使用類型檢查。有很多種方法可以避免類型檢查。首先要考慮的就是 API 的一致性。
不好:
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'));
}
}
好:
function travelToTexas(vehicle) {
vehicle.move(this.currentLocation, new Location('texas'));
}
避免類型檢查(第2部分)
如果你在處理基本類型的數據,比如字符串,整數和數組,又不能使用多態,這時你會覺得需要使用類型檢查,那么可以考慮 TypeScript。這是普通 JavaScript 的完美替代品,它在標準的 JavaScript 語法之上提供了靜態類型。普通 JavaScript 手工檢查類型的問題在于這樣會寫很多廢話,而人為的“類型安全”并不能彌補損失的可讀性。讓你的 JavaScript 保持整潔,寫很好的測試,并保持良好的代碼審查。否則讓 TypeScript (我說過,這是很好的替代品)來做所有事情。
不好:
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');
}
}
好:
function combine(val1, val2) {
return val1 + val2;
}
不要過度優化
現在瀏覽器在運行時悄悄地做了很多優化工作。很多時候你的優化都是在浪費時間。這里有很好的資源 可以看看哪些優化比較缺乏。把它們作為目標,直到他們能固定下來的時候。
不好:
// 在舊瀏覽器中,每次循環的成本都比較高,因為每次都會重算 `len`。
// 現在瀏覽器中,這已經被優化了。
for (var i = 0, len = list.length; i < len; i++) {
// ...
}
好:
for (var i = 0; i < list.length; i++) {
// ...
}
刪除不用的代碼
不用的代碼和重復的代碼一樣糟糕。在代碼庫中保留無用的代碼是毫無道理的事情。如果某段代碼用不到,那就刪掉它!如果你以后需要它,仍然可以從代碼庫的歷史版本中找出來。
不好:
function oldRequestModule(url) {
// ...
}
function newRequestModule(url) {
// ...
}
var req = newRequestModule;
inventoryTracker('apples', req, 'www.inventory-awesome.io');
好:
function newRequestModule(url) {
// ...
}
var req = newRequestModule;
inventoryTracker('apples', req, 'www.inventory-awesome.io');
未完~
PS:這本書在前面跟大家分享過,關注公號,輸入關鍵字:2010107,則可以獲取《代碼整潔之道》電子版圖書的下載地址,進行下載PDF版。
原文:https://github.com/ryanmcdermott/clean-code-javascript/blob/master/README.md
譯文:http://www.zcfy.cc/article/clean-code-javascript-readme-md-at-master-ryanmcdermott-clean-code-javascript-github-2273.html
譯者:邊城