JavaScript程式碼簡潔之道
摘要: 可以說是《Clean Code》的JS程式碼示例了,值得參考。
- 原文:JavaScript 程式碼簡潔之道
- 作者:繆宇
Fundebug經授權轉載,版權歸原作者所有。
測試程式碼質量的唯一方式:別人看你程式碼時說 f * k 的次數。
程式碼質量與其整潔度成正比。乾淨的程式碼,既在質量上較為可靠,也為後期維護、升級奠定了良好基礎。
本文並不是程式碼風格指南,而是關於程式碼的可讀性
、複用性
、擴充套件性
探討。
我們將從幾個方面展開討論:
- 變數
- 函式
- 物件和資料結構
- 類
- SOLID
- 測試
- 非同步
- 錯誤處理
- 程式碼風格
- 註釋
變數
用有意義且常用的單詞命名變數
Bad:
const yyyymmdstr = moment().format(`YYYY/MM/DD`);
Good:
const currentDate = moment().format(`YYYY/MM/DD`);
保持統一
可能同一個專案對於獲取使用者資訊,會有三個不一樣的命名。應該保持統一,如果你不知道該如何取名,可以去 codelf 搜尋,看別人是怎麼取名的。
Bad:
getUserInfo();
getClientData();
getCustomerRecord();
Good
getUser()
每個常量都該命名
可以用 buddy.js 或者 ESLint 檢測程式碼中未命名的常量。
Bad:
// 三個月之後你還能知道 86400000 是什麼嗎?
setTimeout(blastOff, 86400000);
Good
const MILLISECOND_IN_A_DAY = 86400000;
setTimeout(blastOff, MILLISECOND_IN_A_DAY);
可描述
通過一個變數生成了一個新變數,也需要為這個新變數命名,也就是說每個變數當你看到他第一眼你就知道他是幹什麼的。
Bad:
const ADDRESS = `One Infinite Loop, Cupertino 95014`;
const CITY_ZIP_CODE_REGEX = /^[^,\]+[,\s]+(.+?)s*(d{5})?$/;
saveCityZipCode(ADDRESS.match(CITY_ZIP_CODE_REGEX)[1], address.match(CITY_ZIP_CODE_REGEX)[2]);
Good
const ADDRESS = `One Infinite Loop, Cupertino 95014`;
const CITY_ZIP_CODE_REGEX = /^[^,\]+[,\s]+(.+?)s*(d{5})?$/;
const [, city, zipCode] = ADDRESS.match(CITY_ZIP_CODE_REGEX) || [];
saveCityZipCode(city, zipCode);
直接了當
Bad:
const locations = [`Austin`, `New York`, `San Francisco`];
locations.forEach((l) => {
doStuff();
doSomeOtherStuff();
// ...
// ...
// ...
// 需要看其他程式碼才能確定 `l` 是幹什麼的。
dispatch(l);
});
Good
const locations = [`Austin`, `New York`, `San Francisco`];
locations.forEach((location) => {
doStuff();
doSomeOtherStuff();
// ...
// ...
// ...
dispatch(location);
});
避免無意義的字首
如果建立了一個物件 car,就沒有必要把它的顏色命名為 carColor。
Bad:
const car = {
carMake: `Honda`,
carModel: `Accord`,
carColor: `Blue`
};
function paintCar(car) {
car.carColor = `Red`;
}
Good
const car = {
make: `Honda`,
model: `Accord`,
color: `Blue`
};
function paintCar(car) {
car.color = `Red`;
}
使用預設值
Bad:
function createMicrobrewery(name) {
const breweryName = name || `Hipster Brew Co.`;
// ...
}
Good
function createMicrobrewery(name = `Hipster Brew Co.`) {
// ...
}
函式
引數越少越好
如果引數超過兩個,使用 ES2015/ES6
的解構語法,不用考慮引數的順序。
Bad:
function createMenu(title, body, buttonText, cancellable) {
// ...
}
Good
function createMenu({ title, body, buttonText, cancellable }) {
// ...
}
createMenu({
title: `Foo`,
body: `Bar`,
buttonText: `Baz`,
cancellable: true
});
只做一件事情
這是一條在軟體工程領域流傳久遠的規則。嚴格遵守這條規則會讓你的程式碼可讀性更好,也更容易重構。如果違反這個規則,那麼程式碼會很難被測試或者重用。
Bad:
function emailClients(clients) {
clients.forEach((client) => {
const clientRecord = database.lookup(client);
if (clientRecord.isActive()) {
email(client);
}
});
}
Good
function emailActiveClients(clients) {
clients
.filter(isActiveClient)
.forEach(email);
}
function isActiveClient() {
const clientRecord = database.lookup(client);
return clientRecord.isActive();
}
顧名思義
看函式名就應該知道它是幹啥的。
Bad:
function addToDate(date, month) {
// ...
}
const date = new Date();
// 很難知道是把什麼加到日期中
addToDate(date, 1);
Good
function addMonthToDate(month, date) {
// ...
}
const date = new Date();
addMonthToDate(1, date);
只需要一層抽象層
如果函式巢狀過多會導致很難複用以及測試。
Bad:
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...
});
}
Good
function parseBetterJSAlternative(code) {
const tokens = tokenize(code);
const ast = lexer(tokens);
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;
}
刪除重複程式碼
很多時候雖然是同一個功能,但由於一兩個不同點,讓你不得不寫兩個幾乎相同的函式。
要想優化重複程式碼需要有較強的抽象能力,錯誤的抽象還不如重複程式碼。所以在抽象過程中必須要遵循 SOLID
原則(SOLID
是什麼?稍後會詳細介紹)。
Bad:
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);
});
}
Good
function showEmployeeList(employees) {
employees.forEach(employee => {
const expectedSalary = employee.calculateExpectedSalary();
const experience = employee.getExperience();
const data = {
expectedSalary,
experience,
};
switch(employee.type) {
case `develop`:
data.githubLink = employee.getGithubLink();
break
case `manager`:
data.portfolio = employee.getMBAProjects();
break
}
render(data);
})
}
物件設定預設屬性
Bad:
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);
Good
const menuConfig = {
title: `Order`,
// `body` key 缺失
buttonText: `Send`,
cancellable: true
};
function createMenu(config) {
config = Object.assign({
title: `Foo`,
body: `Bar`,
buttonText: `Baz`,
cancellable: true
}, config);
// config 就變成了: {title: "Order", body: "Bar", buttonText: "Send", cancellable: true}
// ...
}
createMenu(menuConfig);
不要傳 flag 引數
通過 flag 的 true 或 false,來判斷執行邏輯,違反了一個函式幹一件事的原則。
Bad:
function createFile(name, temp) {
if (temp) {
fs.create(`./temp/${name}`);
} else {
fs.create(name);
}
}
Good
function createFile(name) {
fs.create(name);
}
function createFileTemplate(name) {
createFile(`./temp/${name}`)
}
避免副作用(第一部分)
函式接收一個值返回一個新值,除此之外的行為我們都稱之為副作用,比如修改全域性變數、對檔案進行 IO 操作等。
當函式確實需要副作用時,比如對檔案進行 IO 操作時,請不要用多個函式/類進行檔案操作,有且僅用一個函式/類來處理。也就是說副作用需要在唯一的地方處理。
副作用的三大天坑:隨意修改可變資料型別、隨意分享沒有資料結構的狀態、沒有在統一地方處理副作用。
Bad:
// 全域性變數被一個函式引用
// 現在這個變數從字串變成了陣列,如果有其他的函式引用,會發生無法預見的錯誤。
var name = `Ryan McDermott`;
function splitIntoFirstAndLastName() {
name = name.split(` `);
}
splitIntoFirstAndLastName();
console.log(name); // [`Ryan`, `McDermott`];
Good
var name = `Ryan McDermott`;
var newName = splitIntoFirstAndLastName(name)
function splitIntoFirstAndLastName(name) {
return name.split(` `);
}
console.log(name); // `Ryan McDermott`;
console.log(newName); // [`Ryan`, `McDermott`];
避免副作用(第二部分)
在 JavaScript 中,基本型別通過賦值傳遞,物件和陣列通過引用傳遞。以引用傳遞為例:
假如我們寫一個購物車,通過 addItemToCart()
方法新增商品到購物車,修改 購物車陣列
。此時呼叫 purchase()
方法購買,由於引用傳遞,獲取的 購物車陣列
正好是最新的資料。
看起來沒問題對不對?
如果當使用者點選購買時,網路出現故障, purchase()
方法一直在重複呼叫,與此同時使用者又新增了新的商品,這時網路又恢復了。那麼 purchase()
方法獲取到 購物車陣列
就是錯誤的。
為了避免這種問題,我們需要在每次新增商品時,克隆 購物車陣列
並返回新的陣列。
Bad:
const addItemToCart = (cart, item) => {
cart.push({ item, date: Date.now() });
};
Good
const addItemToCart = (cart, item) => {
return [...cart, {item, date: Date.now()}]
};
不要寫全域性方法
在 JavaScript 中,永遠不要汙染全域性,會在生產環境中產生難以預料的 bug。舉個例子,比如你在 Array.prototype
上新增一個 diff
方法來判斷兩個陣列的不同。而你同事也打算做類似的事情,不過他的 diff
方法是用來判斷兩個陣列首位元素的不同。很明顯你們方法會產生衝突,遇到這類問題我們可以用 ES2015/ES6 的語法來對 Array
進行擴充套件。
Bad:
Array.prototype.diff = function diff(comparisonArray) {
const hash = new Set(comparisonArray);
return this.filter(elem => !hash.has(elem));
};
Good
class SuperArray extends Array {
diff(comparisonArray) {
const hash = new Set(comparisonArray);
return this.filter(elem => !hash.has(elem));
}
}
比起命令式我更喜歡函數語言程式設計
函式式變程式設計可以讓程式碼的邏輯更清晰更優雅,方便測試。
Bad:
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;
}
Good
const programmerOutput = [
{
name: `Uncle Bobby`,
linesOfCode: 500
}, {
name: `Suzie Q`,
linesOfCode: 1500
}, {
name: `Jimmy Gosling`,
linesOfCode: 150
}, {
name: `Gracie Hopper`,
linesOfCode: 1000
}
];
let totalOutput = programmerOutput
.map(output => output.linesOfCode)
.reduce((totalLines, lines) => totalLines + lines, 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)) {
// ...
}
避免使用條件語句
Q:不用條件語句寫程式碼是不可能的。
A:絕大多數場景可以用多型替代。
Q:用多型可行,但為什麼就不能用條件語句了呢?
A:為了讓程式碼更簡潔易讀,如果你的函式中出現了條件判斷,那麼說明你的函式不止幹了一件事情,違反了函式單一原則。
Bad:
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();
}
}
}
Good
class Airplane {
// ...
}
// 波音777
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();
}
}
避免型別檢查(第一部分)
JavaScript 是無型別的,意味著你可以傳任意型別引數,這種自由度很容易讓人困擾,不自覺的就會去檢查型別。仔細想想是你真的需要檢查型別還是你的 API 設計有問題?
Bad:
function travelToTexas(vehicle) {
if (vehicle instanceof Bicycle) {
vehicle.pedal(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`));
}
避免型別檢查(第二部分)
如果你需要做靜態型別檢查,比如字串、整數等,推薦使用 TypeScript,不然你的程式碼會變得又臭又長。
Bad:
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`);
}
Good
function combine(val1, val2) {
return val1 + val2;
}
不要過度優化
現代瀏覽器已經在底層做了很多優化,過去的很多優化方案都是無效的,會浪費你的時間,想知道現代瀏覽器優化了哪些內容,請點這裡。
Bad:
// 在老的瀏覽器中,由於 `list.length` 沒有做快取,每次迭代都會去計算,造成不必要開銷。
// 現代瀏覽器已對此做了優化。
for (let i = 0, len = list.length; i < len; i++) {
// ...
}
Good
for (let i = 0; i < list.length; i++) {
// ...
}
刪除棄用程式碼
很多時候有些程式碼已經沒有用了,但擔心以後會用,捨不得刪。
如果你忘了這件事,這些程式碼就永遠存在那裡了。
放心刪吧,你可以在程式碼庫歷史版本中找他它。
Bad:
function oldRequestModule(url) {
// ...
}
function newRequestModule(url) {
// ...
}
const req = newRequestModule;
inventoryTracker(`apples`, req, `www.inventory-awesome.io`);
Good
function newRequestModule(url) {
// ...
}
const req = newRequestModule;
inventoryTracker(`apples`, req, `www.inventory-awesome.io`);
物件和資料結構
用 get、set 方法運算元據
這樣做可以帶來很多好處,比如在運算元據時打日誌,方便跟蹤錯誤;在 set
的時候很容易對資料進行校驗…
Bad:
function makeBankAccount() {
// ...
return {
balance: 0,
// ...
};
}
const account = makeBankAccount();
account.balance = 100;
Good
function makeBankAccount() {
// 私有變數
let balance = 0;
function getBalance() {
return balance;
}
function setBalance(amount) {
// ... 在更新 balance 前,對 amount 進行校驗
balance = amount;
}
return {
// ...
getBalance,
setBalance,
};
}
const account = makeBankAccount();
account.setBalance(100);
使用私有變數
可以用閉包來建立私有變數
Bad:
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
Good
function makeEmployee(name) {
return {
getName() {
return name;
},
};
}
const employee = makeEmployee(`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
類
使用 class
在 ES2015/ES6 之前,沒有類的語法,只能用建構函式的方式模擬類,可讀性非常差。
Bad:
// 動物
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() {};
Good
// 動物
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() {};
}
鏈式呼叫
這種模式相當有用,可以在很多庫中發現它的身影,比如 jQuery、Lodash 等。它讓你的程式碼簡潔優雅。實現起來也非常簡單,在類的方法最後返回 this 可以了。
Bad:
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();
Good
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;
}
}
const car = new Car(`Ford`,`F-150`,`red`)
.setColor(`pink`);
.save();
不要濫用繼承
很多時候繼承被濫用,導致可讀性很差,要搞清楚兩個類之間的關係,繼承表達的一個屬於關係,而不是包含關係,比如 Human->Animal vs. User->UserDetails
Bad:
class Employee {
constructor(name, email) {
this.name = name;
this.email = email;
}
// ...
}
// TaxData(稅收資訊)並不是屬於 Employee(僱員),而是包含關係。
class EmployeeTaxData extends Employee {
constructor(ssn, salary) {
super();
this.ssn = ssn;
this.salary = salary;
}
// ...
}
Good
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
SOLID 是幾個單詞首字母組合而來,分別表示 單一功能原則
、開閉原則
、里氏替換原則
、介面隔離原則
以及依賴反轉原則
。
單一功能原則
如果一個類乾的事情太多太雜,會導致後期很難維護。我們應該釐清職責,各司其職減少相互之間依賴。
Bad:
class UserSettings {
constructor(user) {
this.user = user;
}
changeSettings(settings) {
if (this.verifyCredentials()) {
// ...
}
}
verifyCredentials() {
// ...
}
}
Good
class UserAuth {
constructor(user) {
this.user = user;
}
verifyCredentials() {
// ...
}
}
class UserSetting {
constructor(user) {
this.user = user;
this.auth = new UserAuth(this.user);
}
changeSettings(settings) {
if (this.auth.verifyCredentials()) {
// ...
}
}
}
}
開閉原則
“開”指的就是類、模組、函式都應該具有可擴充套件性,“閉”指的是它們不應該被修改。也就是說你可以新增功能但不能去修改原始碼。
Bad:
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) => {
// 傳遞 response 並 return
});
} else if (this.adapter.name === `httpNodeAdapter`) {
return makeHttpCall(url).then((response) => {
// 傳遞 response 並 return
});
}
}
}
function makeAjaxCall(url) {
// 處理 request 並 return promise
}
function makeHttpCall(url) {
// 處理 request 並 return promise
}
Good
class AjaxAdapter extends Adapter {
constructor() {
super();
this.name = `ajaxAdapter`;
}
request(url) {
// 處理 request 並 return promise
}
}
class NodeAdapter extends Adapter {
constructor() {
super();
this.name = `nodeAdapter`;
}
request(url) {
// 處理 request 並 return promise
}
}
class HttpRequester {
constructor(adapter) {
this.adapter = adapter;
}
fetch(url) {
return this.adapter.request(url).then((response) => {
// 傳遞 response 並 return
});
}
}
里氏替換原則
名字很唬人,其實道理很簡單,就是子類不要去重寫父類的方法。
Bad:
// 長方形
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();
rectangle.render(area);
});
}
const rectangles = [new Rectangle(), new Rectangle(), new Square()];
renderLargeRectangles(rectangles);
Good
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);
介面隔離原則
JavaScript 幾乎沒有介面的概念,所以這條原則很少被使用。官方定義是“客戶端不應該依賴它不需要的介面”,也就是介面最小化,把介面解耦。
Bad:
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.
// ...
});
Good
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() {}
}
});
依賴反轉原則
說就兩點:
- 高層次模組不能依賴低層次模組,它們依賴於抽象介面。
- 抽象介面不能依賴具體實現,具體實現依賴抽象介面。
總結下來就兩個字,解耦。
Bad:
// 庫存查詢
class InventoryRequester {
constructor() {
this.REQ_METHODS = [`HTTP`];
}
requestItem(item) {
// ...
}
}
// 庫存跟蹤
class InventoryTracker {
constructor(items) {
this.items = items;
// 這裡依賴一個特殊的請求類,其實我們只是需要一個請求方法。
this.requester = new InventoryRequester();
}
requestItems() {
this.items.forEach((item) => {
this.requester.requestItem(item);
});
}
}
const inventoryTracker = new InventoryTracker([`apples`, `bananas`]);
inventoryTracker.requestItems();
Good
// 庫存跟蹤
class InventoryTracker {
constructor(items, requester) {
this.items = items;
this.requester = requester;
}
requestItems() {
this.items.forEach((item) => {
this.requester.requestItem(item);
});
}
}
// HTTP 請求
class InventoryRequesterHTTP {
constructor() {
this.REQ_METHODS = [`HTTP`];
}
requestItem(item) {
// ...
}
}
// webSocket 請求
class InventoryRequesterWS {
constructor() {
this.REQ_METHODS = [`WS`];
}
requestItem(item) {
// ...
}
}
// 通過依賴注入的方式將請求模組解耦,這樣我們就可以很輕易的替換成 webSocket 請求。
const inventoryTracker = new InventoryTracker([`apples`, `bananas`], new InventoryRequesterHTTP());
inventoryTracker.requestItems();
測試
隨著專案變得越來越龐大,時間線拉長,有的老程式碼可能半年都沒碰過,如果此時上線,你有信心這部分程式碼能正常工作嗎?測試的覆蓋率和你的信心是成正比的。
PS: 如果你發現你的程式碼很難被測試,那麼你應該優化你的程式碼了。
單一化
Bad:
import assert from `assert`;
describe(`MakeMomentJSGreatAgain`, () => {
it(`handles date boundaries`, () => {
let date;
date = new MakeMomentJSGreatAgain(`1/1/2015`);
date.addDays(30);
assert.equal(`1/31/2015`, date);
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);
});
});
Good
import assert from `assert`;
describe(`MakeMomentJSGreatAgain`, () => {
it(`handles 30-day months`, () => {
const date = new MakeMomentJSGreatAgain(`1/1/2015`);
date.addDays(30);
assert.equal(`1/31/2015`, date);
});
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 替代回撥吧。
Bad:
import { get } from `request`;
import { writeFile } from `fs`;
get(`https://en.wikipedia.org/wiki/Robert_Cecil_Martin`, (requestErr, response) => {
if (requestErr) {
console.error(requestErr);
} else {
writeFile(`article.html`, response.body, (writeErr) => {
if (writeErr) {
console.error(writeErr);
} else {
console.log(`File written`);
}
});
}
});
Good
get(`https://en.wikipedia.org/wiki/Robert_Cecil_Martin`)
.then((response) => {
return writeFile(`article.html`, response);
})
.then(() => {
console.log(`File written`);
})
.catch((err) => {
console.error(err);
});
Async/Await 比起 Promises 更簡潔
Bad:
import { get } from `request-promise`;
import { writeFile } from `fs-promise`;
get(`https://en.wikipedia.org/wiki/Robert_Cecil_Martin`)
.then((response) => {
return writeFile(`article.html`, response);
})
.then(() => {
console.log(`File written`);
})
.catch((err) => {
console.error(err);
});
Good
import { get } from `request-promise`;
import { writeFile } from `fs-promise`;
async function getCleanCodeArticle() {
try {
const response = await get(`https://en.wikipedia.org/wiki/Robert_Cecil_Martin`);
await writeFile(`article.html`, response);
console.log(`File written`);
} catch(err) {
console.error(err);
}
}
錯誤處理
不要忽略拋異常
Bad:
try {
functionThatMightThrow();
} catch (error) {
console.log(error);
}
Good
try {
functionThatMightThrow();
} catch (error) {
// 這一種選擇,比起 console.log 更直觀
console.error(error);
// 也可以在介面上提醒使用者
notifyUserOfError(error);
// 也可以把異常傳回伺服器
reportErrorToService(error);
// 其他的自定義方法
}
不要忘了在 Promises 拋異常
Bad:
getdata()
.then((data) => {
functionThatMightThrow(data);
})
.catch((error) => {
console.log(error);
});
Good
getdata()
.then((data) => {
functionThatMightThrow(data);
})
.catch((error) => {
// 這一種選擇,比起 console.log 更直觀
console.error(error);
// 也可以在介面上提醒使用者
notifyUserOfError(error);
// 也可以把異常傳回伺服器
reportErrorToService(error);
// 其他的自定義方法
});
程式碼風格
程式碼風格是主觀的,爭論哪種好哪種不好是在浪費生命。市面上有很多自動處理程式碼風格的工具,選一個喜歡就行了,我們來討論幾個非自動處理的部分。
常量大寫
Bad:
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 {}
Good
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 {}
先宣告後呼叫
就像我們看報紙文章一樣,從上到下看,所以為了方便閱讀把函式宣告寫在函式呼叫前面。
Bad:
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(employee);
review.perfReview();
Good
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();
註釋
只有業務邏輯需要註釋
程式碼註釋不是越多越好。
Bad:
function hashIt(data) {
// 這是初始值
let hash = 0;
// 陣列的長度
const length = data.length;
// 迴圈陣列
for (let i = 0; i < length; i++) {
// 獲取字元程式碼
const char = data.charCodeAt(i);
// 修改 hash
hash = ((hash << 5) - hash) + char;
// 轉換為32位整數
hash &= hash;
}
}
Good
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;
// 轉換為32位整數
hash &= hash;
}
}
刪掉註釋的程式碼
git 存在的意義就是儲存你的舊程式碼,所以註釋的程式碼趕緊刪掉吧。
Bad:
doStuff();
// doOtherStuff();
// doSomeMoreStuff();
// doSoMuchStuff();
Good
doStuff();
不要記日記
記住你有 git!,git log
可以幫你幹這事。
Bad:
/**
* 2016-12-20: 刪除了 xxx
* 2016-10-01: 改進了 xxx
* 2016-02-03: 刪除了第12行的型別檢查
* 2015-03-14: 增加了一個合併的方法
*/
function combine(a, b) {
return a + b;
}
Good
function combine(a, b) {
return a + b;
}
註釋不需要高亮
註釋高亮,並不能起到提示的作用,反而會干擾你閱讀程式碼。
Bad:
////////////////////////////////////////////////////////////////////////////////
// Scope Model Instantiation
////////////////////////////////////////////////////////////////////////////////
$scope.model = {
menu: `foo`,
nav: `bar`
};
////////////////////////////////////////////////////////////////////////////////
// Action setup
////////////////////////////////////////////////////////////////////////////////
const actions = function() {
// ...
};
Good
$scope.model = {
menu: `foo`,
nav: `bar`
};
const actions = function() {
// ...
};
ref
翻譯自ryanmcdermott的《clean-code-javascript》,本文對原文進行了一些修改。
相關文章
- JavaScript 程式碼簡潔之道JavaScript
- JavaScript 程式碼整潔之道JavaScript
- JS程式碼簡潔之道--函式JS函式
- Clean Code PHP 程式碼簡潔之道PHP
- PHP程式碼簡潔之道——函式部分PHP函式
- PHP程式碼簡潔之道——變數部分PHP變數
- JAVA基礎之程式碼簡潔之道Java
- 程式碼整潔之道
- Typescript 程式碼整潔之道TypeScript
- 聊聊程式碼整潔之道
- 簡潔Java之道Java
- 程式碼整潔之道 clean code
- 程式碼簡潔之道:編寫乾淨的 React Components & JSXReactJS
- JDK新特性——Stream程式碼簡潔之道的詳細用法JDK
- 白話文轉文言文——Kotlin程式碼簡潔之道Kotlin
- 程式碼整潔之道讀書記
- 程式碼整潔之道 – 有意義的命名
- 閱讀《程式碼整潔之道》總結
- 程式碼整潔之道Clean Code筆記筆記
- 程式碼整潔之道之做減法
- 程式碼整潔之道的 7 個方法
- 《程式碼整潔之道》總結和筆記筆記
- 程式碼整潔之道--讀書筆記(7)筆記
- 程式碼整潔之道--讀書筆記(1)筆記
- 程式碼整潔之道--讀書筆記(2)筆記
- 程式碼整潔之道--讀書筆記(13)筆記
- 程式碼整潔之道--讀書筆記(14)筆記
- 程式碼整潔之道--讀書筆記(12)筆記
- 程式碼整潔之道--讀書筆記(10)筆記
- 程式碼整潔之道--讀書筆記(6)筆記
- 程式碼整潔之道--讀書筆記(5)筆記
- 程式碼整潔之道--讀書筆記(9)筆記
- 程式碼整潔之道--讀書筆記(4)筆記
- 程式碼整潔之道--讀書筆記(3)筆記
- 程式碼整潔之道--讀書筆記(11)筆記
- 讀書筆記-程式碼整潔之道(一)筆記
- 9個JavaScript小技巧:寫出更簡潔,高效程式碼JavaScript
- Python程式碼整潔之道--使用裝飾器改進程式碼Python