淺談JavaScript 代碼簡潔之道

這篇文章主要介紹了淺談JavaScript 代碼簡潔之道,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨着小編來一起學習學習吧

測試代碼質量的唯一方式:別人看你代碼時說 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(client) {
 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() {}
 }
});

依賴反轉原則

說就兩點:

  1. 高層次模塊不能依賴低層次模塊,它們依賴於抽象接口。
  2. 抽象接口不能依賴具體實現,具體實現依賴抽象接口。

總結下來就兩個字,解耦。

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() {
 // ...
};

翻譯自 ryanmcdermott 的 《clean-code-javascript》,本文對原文進行了一些修改。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章