簡介
將源自 Robert C. Martin 的 Clean Code 的軟件工程原則適配到 JavaScript 。 這不是一個代碼風格指南, 它是一個使用 JavaScript 來生產 可讀的, 可重用的, 以及可重構的軟件的指南。
這裏的每一項原則都不是必須遵守的, 甚至只有更少的能夠被廣泛認可。 這些僅僅是指南而已, 但是卻是 Clean Code 作者多年經驗的結晶。
我們的軟件工程行業只有短短的 50 年, 依然有很多要我們去學習。 當軟件架構與建築架構一樣古老時, 也許我們將會有硬性的規則去遵守。 而現在, 讓這些指南做爲你和你的團隊生產的 JavaScript 代碼的 質量的標準。
還有一件事: 知道這些指南並不能馬上讓你成爲一個更加出色的軟件開發者, 並且使用它們工作多年也並 不意味着你不再會犯錯誤。 每一段代碼最開始都是草稿, 像溼粘土一樣被打造成最終的形態。 最後當我們 和搭檔們一起審查代碼時清除那些不完善之處, 不要因爲最初需要改善的草稿代碼而自責, 而是對那些代 碼下手。
變量
使用有意義並且可讀的變量名稱
不好的:
const yyyymmdstr = moment().format('YYYY/MM/DD');
好的:
const currentDate = moment().format('YYYY/MM/DD');
爲相同類型的變量使用相同的詞彙
不好的:
getUserInfo();
getClientData();
getCustomerRecord();
好的:
getUser();
使用可搜索的名稱
我們要閱讀的代碼比要寫的代碼多得多, 所以我們寫出的代碼的可讀性和可搜索性是很重要的。 使用沒有 意義的變量名將會導致我們的程序難於理解, 將會傷害我們的讀者, 所以請使用可搜索的變量名。 類似 buddy.js 和 ESLint 的工具可以幫助我們找到未命名的常量。
不好的:
// 艹, 86400000 是什麼鬼?
setTimeout(blastOff, 86400000);
好的:
// 將它們聲明爲全局常量 `const` 。
const MILLISECONDS_IN_A_DAY = 86400000;
setTimeout(blastOff, MILLISECONDS_IN_A_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) {
car.carColor = 'Red';
}
好的:
const Car = {
make: 'Honda',
model: 'Accord',
color: 'Blue'
};
function paintCar(car) {
car.color = 'Red';
}
使用默認變量替代短路運算或條件
不好的:
function createMicrobrewery(name) {
const breweryName = name || 'Hipster Brew Co.';
// ...
}
好的:
function createMicrobrewery(breweryName = 'Hipster Brew Co.') {
// ...
}
函數
函數參數 (兩個以下最理想)
限制函數參數的個數是非常重要的, 因爲這樣將使你的函數容易進行測試。 一旦超過三個參數將會導致組 合爆炸, 因爲你不得不編寫大量針對每個參數的測試用例。
沒有參數是最理想的, 一個或者兩個參數也是可以的, 三個參數應該避免, 超過三個應該被重構。 通常, 如果你有一個超過兩個函數的參數, 那就意味着你的函數嘗試做太多的事情。 如果不是, 多數情況下一個 更高級對象可能會滿足需求。
由於 JavaScript 允許我們不定義類型/模板就可以創建對象, 當你發現你自己需要大量的參數時, 你 可以使用一個對象。
不好的:
function createMenu(title, body, buttonText, cancellable) {
// ...
}
好的:
const menuConfig = {
title: 'Foo',
body: 'Bar',
buttonText: 'Baz',
cancellable: true
};
function createMenu(config) {
// ...
}
函數應當只做一件事情
這是軟件工程中最重要的一條規則, 當函數需要做更多的事情時, 它們將會更難進行編寫、 測試和推理。 當你能將一個函數隔離到只有一個動作, 他們將能夠被容易的進行重構並且你的代碼將會更容易閱讀。 如 果你嚴格遵守本指南中的這一條, 你將會領先於許多開發者。
不好的:
function emailClients(clients) {
clients.forEach((client) => {
const clientRecord = database.lookup(client);
if (clientRecord.isActive()) {
email(client);
}
});
}
好的:
function emailClients(clients) {
clients
.filter(isClientActive)
.forEach(email);
}
function isClientActive(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 tokenize(code) {
const REGEXES = [
// ...
];
const statements = code.split(' ');
const tokens = [];
REGEXES.forEach((REGEX) => {
statements.forEach((statement) => {
tokens.push( /* ... */ );
});
});
return tokens;
}
function lexer(tokens) {
const ast = [];
tokens.forEach((token) => {
ast.push( /* ... */ );
});
return ast;
}
function parseBetterJSAlternative(code) {
const tokens = tokenize(code);
const ast = lexer(tokens);
ast.forEach((node) => {
// parse...
});
}
移除冗餘代碼
竭盡你的全力去避免冗餘代碼。 冗餘代碼是不好的, 因爲它意味着當你需要修改一些邏輯時會有多個地方 需要修改。
想象一下你在經營一家餐館, 你需要記錄所有的庫存西紅柿, 洋蔥, 大蒜, 各種香料等等。 如果你有多 個記錄列表, 當你用西紅柿做一道菜時你得更新多個列表。 如果你只有一個列表, 就只有一個地方需要更 新!
你有冗餘代碼通常是因爲你有兩個或多個稍微不同的東西, 它們共享大部分, 但是它們的不同之處迫使你使 用兩個或更多獨立的函數來處理大部分相同的東西。 移除冗餘代碼意味着創建一個可以處理這些不同之處的 抽象的函數/模塊/類。
讓這個抽象正確是關鍵的, 這是爲什麼要你遵循 Classes 那一章的 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 showList(employees) {
employees.forEach((employee) => {
const expectedSalary = employee.calculateExpectedSalary();
const experience = employee.getExperience();
let portfolio = employee.getGithubLink();
if (employee.type === 'manager') {
portfolio = employee.getMBAProjects();
}
const data = {
expectedSalary,
experience,
portfolio
};
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',
// 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);
不要使用標記位做爲函數參數
標記位是告訴你的用戶這個函數做了不只一件事情。 函數應該只做一件事情。 如果你的函數因爲一個布爾值 出現不同的代碼路徑, 請拆分它們。
不好的:
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}`);
}
避免副作用
如果一個函數做了除接受一個值然後返回一個值或多個值之外的任何事情, 它將會產生副作用, 它可能是 寫入一個文件, 修改一個全局變量, 或者意外的把你所有的錢連接到一個陌生人那裏。
現在在你的程序中確實偶爾需要副作用, 就像上面的代碼, 你也許需要寫入到一個文件, 你需要做的是集 中化你要做的事情, 不要讓多個函數或者類寫入一個特定的文件, 用一個服務來實現它, 一個並且只有一 個。
重點是避免這些常見的易犯的錯誤, 比如在對象之間共享狀態而不使用任何結構, 使用任何地方都可以寫入 的可變的數據類型, 沒有集中化導致副作用。 如果你能做到這些, 那麼你將會比其它的碼農大軍更加幸福。
不好的:
// 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'];
好的:
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 中是一個不好的做法, 因爲你可能會和另外一個類庫衝突, 你的 API 的用戶 可能不夠聰明, 直到他們得到在生產環境得到一個異常。 讓我們來考慮這樣一個例子: 假設你要擴展 JavaScript 的 原生 Array
, 添加一個可以顯示兩個數組的不同之處的 diff
方法, 你可以在 Array.prototype
中寫一個新的方法, 但是它可能會和嘗試做相同事情的其它類庫發生衝突。 如果有 另外一個類庫僅僅使用 diff
方法來查找數組的第一個元素和最後一個元素之間的不同之處呢? 這就是 爲什麼使用 ES2015/ES6 的類是一個更好的做法的原因, 只要簡單的擴展全局的 Array
即可。
不好的:
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));
}
}
函數式編程優於指令式編程
JavaScript 不是 Haskell 那種方式的函數式語言, 但是它有它的函數式風格。 函數式語言更加簡潔 並且更容易進行測試, 當你可以使用函數式編程風格時請盡情使用。
不好的:
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;
}
好的:
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
.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 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();
}
}
避免類型檢查 (part 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'));
}
避免類型檢查 (part 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;
}
throw new Error('Must be of type String or Number');
}
好的:
function combine(val1, val2) {
return val1 + val2;
}
不要過度優化
現代化瀏覽器運行時在幕後做大量的優化, 在大多數的時間, 做優化就是在浪費你的時間。 這些是好的 資源, 用來 查看那些地方需要優化。 爲這些而優化, 直到他們被修正。
不好的:
// On old browsers, each iteration with uncached `list.length` would be costly
// because of `list.length` recomputation. In modern browsers, this is optimized.
// 在舊的瀏覽器上, 每次循環 `list.length` 都沒有被緩存, 會導致不必要的開銷, 因爲要重新計
// 算 `list.length` 。 在現代化瀏覽器上, 這個已經被優化了。
for (let i = 0, len = list.length; i < len; i++) {
// ...
}
好的:
for (let i = 0; i < list.length; i++) {
// ...
}
移除殭屍代碼
僵死代碼和冗餘代碼同樣糟糕。 沒有理由在代碼庫中保存它。 如果它不會被調用, 就刪掉它。 當你需要 它時, 它依然保存在版本歷史記錄中。
不好的:
function oldRequestModule(url) {
// ...
}
function newRequestModule(url) {
// ...
}
const req = newRequestModule;
inventoryTracker('apples', req, 'www.inventory-awesome.io');
好的:
function newRequestModule(url) {
// ...
}
const req = newRequestModule;
inventoryTracker('apples', req, 'www.inventory-awesome.io');
對象和數據結構
使用 getters 和 setters
JavaScript 沒有接口或類型, 所以堅持這個模式是非常困難的, 因爲我們沒有 public
和 private
關鍵字。 正因爲如此, 使用 getters 和 setters 來訪問對象上的數據比簡單的在一個對象上查找屬性 要好得多。 “爲什麼?” 你可能會問, 好吧, 原因請看下面的列表:
- 當你想在獲取一個對象屬性的背後做更多的事情時, 你不需要在代碼庫中查找和修改每一處訪問;
- 使用
set
可以讓添加驗證變得容易; - 封裝內部實現;
- 使用 getting 和 setting 時, 容易添加日誌和錯誤處理;
- 繼承這個類, 你可以重寫默認功能;
- 你可以延遲加載對象的屬性, 比如說從服務器獲取。
不好的:
class BankAccount {
constructor() {
this.balance = 1000;
}
}
const bankAccount = new BankAccount();
// Buy shoes...
bankAccount.balance -= 100;
好的:
class BankAccount {
constructor(balance = 1000) {
this._balance = balance;
}
// It doesn't have to be prefixed with `get` or `set` to be a getter/setter
set balance(amount) {
if (verifyIfAmountCanBeSetted(amount)) {
this._balance = amount;
}
}
get balance() {
return this._balance;
}
verifyIfAmountCanBeSetted(val) {
// ...
}
}
const bankAccount = new BankAccount();
// Buy shoes...
bankAccount.balance -= shoesPrice;
// Get balance
let balance = bankAccount.balance;
讓對象擁有私有成員
這個可以通過閉包來實現(針對 ES5 或更低)。
不好的:
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
好的:
const Employee = function (name) {
this.getName = function getName() {
return 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: John Doe
類
ES2015/ES6 類優先與 ES5 純函數
很難爲經典的 ES5 類創建可讀的的繼承、 構造和方法定義。 如果你需要繼承(並且感到奇怪爲啥你不需 要), 則優先用 ES2015/ES6的類。 不過, 短小的函數優先於類, 直到你發現你需要更大並且更復雜的 對象。
不好的:
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 Mammal 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() {};
好的:
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 中見到。 它使你的代碼變得富有表現力, 並減少囉嗦。 因爲這個原因, 我說, 使用方法鏈然後再看看你的代碼 會變得多麼簡潔。 在你的類/方法中, 簡單的在每個方法的最後返回 this
, 然後你就能把這個類的 其它方法鏈在一起。
不好的:
class Car {
constructor() {
this.make = 'Honda';
this.model = 'Accord';
this.color = 'white';
}
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();
car.setColor('pink');
car.setMake('Ford');
car.setModel('F-150');
car.save();
好的:
class Car {
constructor() {
this.make = 'Honda';
this.model = 'Accord';
this.color = 'white';
}
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()
.setColor('pink')
.setMake('Ford')
.setModel('F-150')
.save();
組合優先於繼承
正如設計模式四人幫所述, 如果可能, 你應該優先使用組合而不是繼承。 有許多好的理由去使用繼承, 也有許多好的理由去使用組合。這個格言 的重點是, 如果你本能的觀點是繼承, 那麼請想一下組合能否更好的爲你的問題建模。 很多情況下它真的 可以。
那麼你也許會這樣想, “我什麼時候改使用繼承?” 這取決於你手上的問題, 不過這兒有一個像樣的列表說 明什麼時候繼承比組合更好用:
- 你的繼承表示"是一個"的關係而不是"有一個"的關係(人類->動物 vs 用戶->用戶詳情);
- 你可以重用來自基類的代碼(人可以像所有動物一樣行動);
- 你想通過基類對子類進行全局的修改(改變所有動物行動時的熱量消耗);
不好的:
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;
}
// ...
}
好的:
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)
正如代碼整潔之道所述, “永遠不要有超過一個理由來修改一個類”。 給一個類塞滿許多功能, 就像你在航 班上只能帶一個行李箱一樣, 這樣做的問題你的類不會有理想的內聚性, 將會有太多的理由來對它進行修改。 最小化需要修改一個類的次數時很重要的, 因爲如果一個類擁有太多的功能, 一旦你修改它的一小部分, 將會很難弄清楚會對代碼庫中的其它模塊造成什麼影響。
不好的:
class UserSettings {
constructor(user) {
this.user = user;
}
changeSettings(settings) {
if (this.verifyCredentials()) {
// ...
}
}
verifyCredentials() {
// ...
}
}
好的:
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)
Bertrand Meyer 說過, “軟件實體 (類, 模塊, 函數等) 應該爲擴展開放, 但是爲修改關閉。” 這 是什麼意思呢? 這個原則基本上說明了你應該允許用戶添加功能而不必修改現有的代碼。
不好的:
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 === 'httpNodeAdapter') {
return makeHttpCall(url).then((response) => {
// transform response and return
});
}
}
}
function makeAjaxCall(url) {
// request and return promise
}
function makeHttpCall(url) {
// request and return promise
}
好的:
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)
這是針對一個非常簡單的裏面的一個恐怖意圖, 它的正式定義是: “如果 S 是 T 的一個子類型, 那麼類 型爲 T 的對象可以被類型爲 S 的對象替換(例如, 類型爲 S 的對象可作爲類型爲 T 的替代品)兒不需 要修改目標程序的期望性質 (正確性、 任務執行性等)。” 這甚至是個恐怖的定義。
最好的解釋是, 如果你又一個基類和一個子類, 那個基類和字類可以互換而不會產生不正確的結果。 這可 能還有有些疑惑, 讓我們來看一下這個經典的正方形與矩形的例子。 從數學上說, 一個正方形是一個矩形, 但是你用 "is-a" 的關係用繼承來實現, 你將很快遇到麻煩。
不好的:
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: Will return 25 for Square. Should be 20.
rectangle.render(area);
});
}
const rectangles = [new Rectangle(), new Rectangle(), new Square()];
renderLargeRectangles(rectangles);
好的:
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 中能比較好的說明這個原則的是一個類需要一個巨大的配置對象。 不需要客戶端去設置大 量的選項是有益的, 因爲多數情況下他們不需要全部的設置。 讓它們變成可選的有助於防止出現一個“胖接 口”。
不好的:
class DOMTraverser {
constructor(settings) {
this.settings = settings;
this.setup();
}
setup() {
this.rootNode = this.settings.rootNode;
this.animationModule.setup();
}
traverse() {
// ...
}
}
const $ = new DOMTraverser({
rootNode: document.getElementsByTagName('body'),
animationModule() {} // Most of the time, we won't need to animate when traversing.
// ...
});
好的:
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)
這個原則闡述了兩個重要的事情:
- 高級模塊不應該依賴於低級模塊, 兩者都應該依賴與抽象;
- 抽象不應當依賴於具體實現, 具體實現應當依賴於抽象。
這個一開始會很難理解, 但是如果你使用過 Angular.js , 你應該已經看到過通過依賴注入來實現的這 個原則, 雖然他們不是相同的概念, 依賴反轉原則讓高級模塊遠離低級模塊的細節和創建, 可以通過 DI 來實現。 這樣做的巨大益處是降低模塊間的耦合。 耦合是一個非常糟糕的開發模式, 因爲會導致代碼難於 重構。
如上所述, JavaScript 沒有接口, 所以被依賴的抽象是隱式契約。 也就是說, 一個對象/類的方法和 屬性直接暴露給另外一個對象/類。 在下面的例子中, 任何一個 Request 模塊的隱式契約 InventoryTracker
將有一個 requestItems
方法。
不好的:
class InventoryRequester {
constructor() {
this.REQ_METHODS = ['HTTP'];
}
requestItem(item) {
// ...
}
}
class InventoryTracker {
constructor(items) {
this.items = items;
// 不好的: 我們已經創建了一個對請求的具體實現的依賴, 我們只有一個 requestItems 方法依
// 賴一個請求方法 'request'
this.requester = new InventoryRequester();
}
requestItems() {
this.items.forEach((item) => {
this.requester.requestItem(item);
});
}
}
const inventoryTracker = new InventoryTracker(['apples', 'bananas']);
inventoryTracker.requestItems();
好的:
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) {
// ...
}
}
// 通過外部創建依賴項並將它們注入, 我們可以輕鬆的用一個嶄新的使用 WebSockets 的請求模塊進行
// 替換。
const inventoryTracker = new InventoryTracker(['apples', 'bananas'], new InventoryRequesterV2());
inventoryTracker.requestItems();
測試
測試比發佈更加重要。 如果你沒有測試或者測試不夠充分, 每次發佈時你就不能確認沒有破壞任何事情。 測試的量由你的團隊決定, 但是擁有 100% 的覆蓋率(包括所有的語句和分支)是你爲什麼能達到高度自信 和內心的平靜。 這意味着需要一個額外的偉大的測試框架, 也需要一個好的覆蓋率工具。
沒有理由不寫測試。 這裏有大量的優秀的 JS 測試框架, 選一個適合你的團隊的即可。 當爲團隊選擇了測試框架之後, 接下來的目標是爲生產的每一個新的功能/模 塊編寫測試。 如果你傾向於測試驅動開發(TDD), 那就太棒了, 但是要點是確認你在上線任何功能或者重 構一個現有功能之前, 達到了需要的目標覆蓋率。
一個測試一個概念
不好的:
const assert = require('assert');
describe('MakeMomentJSGreatAgain', () => {
it('handles date boundaries', () => {
let date;
date = new MakeMomentJSGreatAgain('1/1/2015');
date.addDays(30);
date.shouldEqual('1/31/2015');
date = new MakeMomentJSGreatAgain('2/1/2016');
date.addDays(28);
assert.equal('02/29/2016', date);
date = new MakeMomentJSGreatAgain('2/1/2015');
date.addDays(28);
assert.equal('03/01/2015', date);
});
});
好的:
const assert = require('assert');
describe('MakeMomentJSGreatAgain', () => {
it('handles 30-day months', () => {
const date = new MakeMomentJSGreatAgain('1/1/2015');
date.addDays(30);
date.shouldEqual('1/31/2015');
});
it('handles leap year', () => {
const date = new MakeMomentJSGreatAgain('2/1/2016');
date.addDays(28);
assert.equal('02/29/2016', date);
});
it('handles non-leap year', () => {
const date = new MakeMomentJSGreatAgain('2/1/2015');
date.addDays(28);
assert.equal('03/01/2015', date);
});
});
併發
使用 Promises, 不要使用回調
回調不夠簡潔, 因爲他們會產生過多的嵌套。 在 ES2015/ES6 中, Promises 已經是內置的全局類型 了,使用它們吧!
不好的:
require('request').get('https://en.wikipedia.org/wiki/Robert_Cecil_Martin', (requestErr, response) => {
if (requestErr) {
console.error(requestErr);
} else {
require('fs').writeFile('article.html', response.body, (writeErr) => {
if (writeErr) {
console.error(writeErr);
} else {
console.log('File written');
}
});
}
});
好的:
require('request-promise').get('https://en.wikipedia.org/wiki/Robert_Cecil_Martin')
.then((response) => {
return require('fs-promise').writeFile('article.html', response);
})
.then(() => {
console.log('File written');
})
.catch((err) => {
console.error(err);
});
Async/Await 比 Promises 更加簡潔
Promises 是回調的一個非常簡潔的替代品, 但是 ES2017/ES8 帶來的 async 和 await 提供了一個 更加簡潔的解決方案。 你需要的只是一個前綴爲 async
關鍵字的函數, 接下來就可以不需要 then
函數鏈來編寫邏輯了。 如果你能使用 ES2017/ES8 的高級功能的話, 今天就使用它吧!
不好的:
require('request-promise').get('https://en.wikipedia.org/wiki/Robert_Cecil_Martin')
.then((response) => {
return require('fs-promise').writeFile('article.html', response);
})
.then(() => {
console.log('File written');
})
.catch((err) => {
console.error(err);
});
好的:
async function getCleanCodeArticle() {
try {
const response = await require('request-promise').get('https://en.wikipedia.org/wiki/Robert_Cecil_Martin');
await require('fs-promise').writeFile('article.html', response);
console.log('File written');
} catch(err) {
console.error(err);
}
}
錯誤處理
拋出錯誤是一件好事情! 他們意味着當你的程序有錯時運行時可以成功確認, 並且通過停止執行當前堆棧 上的函數來讓你知道, 結束當前進程(在 Node 中), 在控制檯中用一個堆棧跟蹤提示你。
不要忽略捕捉到的錯誤
對捕捉到的錯誤不做任何處理不能給你修復錯誤或者響應錯誤的能力。 向控制檯記錄錯誤 (console.log
) 也不怎麼好, 因爲往往會丟失在海量的控制檯輸出中。 如果你把任意一段代碼用 try/catch
包裝那就 意味着你想到這裏可能會錯, 因此你應該有個修復計劃, 或者當錯誤發生時有一個代碼路徑。
不好的:
try {
functionThatMightThrow();
} catch (error) {
console.log(error);
}
好的:
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
的錯誤的原因相同。
不好的:
getdata()
.then((data) => {
functionThatMightThrow(data);
})
.catch((error) => {
console.log(error);
});
好的:
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 是無類型的, 所以大小寫告訴你關於你的變量、 函數等的很多事情。 這些規則是主觀的, 所以你的團隊可以選擇他們想要的。 重點是, 不管你們選擇了什麼, 要保持一致。
不好的:
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 {}
好的:
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 {}
函數的調用方與被調用方應該靠近
如果一個函數調用另一個, 則在代碼中這兩個函數的豎直位置應該靠近。 理想情況下,保持被調用函數在被 調用函數的正上方。 我們傾向於從上到下閱讀代碼, 就像讀一章報紙。 由於這個原因, 保持你的代碼可 以按照這種方式閱讀。
不好的:
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(user);
review.perfReview();
好的:
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();
註釋
僅僅對包含複雜業務邏輯的東西進行註釋
註釋是代碼的辯解, 不是要求。 多數情況下, 好的代碼就是文檔。
不好的:
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;
}
}
好的:
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;
}
}
不要在代碼庫中保存註釋掉的代碼
因爲有版本控制, 把舊的代碼留在歷史記錄即可。
不好的:
doStuff();
// doOtherStuff();
// doSomeMoreStuff();
// doSoMuchStuff();
好的:
doStuff();
不要有日誌式的註釋
記住, 使用版本控制! 不需要殭屍代碼, 註釋掉的代碼, 尤其是日誌式的註釋。 使用 git log
來 獲取歷史記錄。
不好的:
/**
* 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;
}
好的:
function combine(a, b) {
return a + b;
}
避免佔位符
它們僅僅添加了干擾。 讓函數和變量名稱與合適的縮進和格式化爲你的代碼提供視覺結構。
不好的:
////////////////////////////////////////////////////////////////////////////////
// Scope Model Instantiation
////////////////////////////////////////////////////////////////////////////////
$scope.model = {
menu: 'foo',
nav: 'bar'
};
////////////////////////////////////////////////////////////////////////////////
// Action setup
////////////////////////////////////////////////////////////////////////////////
const actions = function() {
// ...
};
好的:
$scope.model = {
menu: 'foo',
nav: 'bar'
};
const actions = function() {
// ...
};