摘要:代碼質量與其整潔度成正比。它讓你的代碼簡潔優雅。是幾個單詞首字母組合而來,分別表示單一功能原則開閉原則里氏替換原則接口隔離原則以及依賴反轉原則。開閉原則開指的就是類模塊函數都應該具有可擴展性,閉指的是它們不應該被修改。
測試代碼質量的唯一方式:別人看你代碼時說 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 l = ["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 的解構語法,不用考慮參數的順序。(注:不要超過3個參數,如果確實需要3個以上的參數,用對象包起來)
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(); } } // 利用對象使用分支判斷 var Airplane = { "777": function() { return this.getMaxAltitude() - this.getPassengerCount(); }, "Air Force One": function() { return this.getMaxAltitude(); }, "Cessna": function() { 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,不然你的代碼會變得又臭又長。
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: undefinedGood:
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 是幾個單詞首字母組合而來,分別表示 單一功能原則、開閉原則、里氏替換原則、接口隔離原則以及依賴反轉原則。
單一功能原則 The Single Responsibility Principle如果一個類干的事情太多太雜,會導致后期很難維護。我們應該厘清職責,各司其職減少相互之間依賴。
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()) { // ... } } }開閉原則 The Open Closed Principle
“開”指的就是類、模塊、函數都應該具有可擴展性,“閉”指的是它們不應該被修改。也就是說你可以新增功能但不能去修改源碼。
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 }); } }里氏替換原則 Liskov Substitution Principle
名字很唬人,其實道理很簡單,就是子類不要去重寫父類的方法。
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);接口隔離原則 The Interface Segregation Principle
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() {} } });依賴倒置原則 The Dependency Inversion Principle
說就兩點:
高層次模塊不能依賴低層次模塊,它們依賴于抽象接口。
抽象接口不能依賴具體實現,具體實現依賴抽象接口。
總結下來就兩個字,解耦。
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();
javascript
不要記日記
記住你有 git!,git log 可以幫你干這事。
/** * 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,本文對原文進行了一些修改。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/117198.html
摘要:考慮到函數表示某種行為,函數名稱應該是動詞或短語,用以說明其背后的意圖以及參數的意圖。不好的方式好的方式使用條件簡寫。這可能微不足道,但值得一提。 為了保證可讀性,本文采用的音譯而非直意。 簡介 如果你關注代碼本身和代碼的編寫方式,而不是只關心它是否能工作,那么你寫代碼是有一定的水準。專業開發人員將為未來的自己和其他人編寫代碼,而不僅僅只編寫當前能工作就行的代碼。 在此基礎上,簡潔代碼...
摘要:將代碼寫的簡潔并且易讀易懂是每一位優秀的所應該具備的基本功。前幾天在上看到這個項目,感覺很有收獲,于是在這里記錄一下。 將代碼寫的簡潔并且易讀易懂是每一位優秀的coder所應該具備的基本功。 前幾天在github上看到clean-code-php這個項目,感覺很有收獲,于是在這里記錄一下。 使用有意義并且可讀的變量名稱 Bad: $ymdstr = $moment->format(y-...
摘要:統一的編碼規范編碼規范往簡單說其實就是三個方面換行空格變量命名放在里面,還有一些附加的地方,比如關鍵字大小寫,語法糖的使用與等的問題。這些都是規范代碼的重要手段。推廣給你的隊友團隊項目中,隊友的配合對整個代碼的規范起著決定性的作用。 1. 統一的編碼規范 編碼規范往簡單說其實就是三個方面: 換行 空格 變量命名 放在 PHP 里面,還有一些附加的地方,比如關鍵字大小寫,語法糖的使用...
摘要:使用和在中,通過為屬性或方法設置和關鍵字可以實現對屬性或方法的可見性控制。你的繼承表達了一個對等比如人類是動物的關系,不是包含的關系比如用戶具有用戶詳情你能從基類中復用代碼你想通過修改全局類來對所有派生類進行修改。 使用getter和setter 在 PHP 中,通過為屬性或方法設置 public, protected 和 private 關鍵字可以實現對屬性或方法的可見性控制。不過,...
摘要:與此類似,理所當然的,我們程序員也會有自己的圣經。這便是程序員的圣經三個原則我認為做為一個程序員,最神圣的就是三個原則,它幾乎能完整無誤的定義做為一個程序員應該如何去編碼。 ...
摘要:的黑客與設計剖析設計之美的秘密,英文原版在這里,還有一套免費教程。的代碼整潔之道程序員的職業素養,英文原版的學習如何為他人寫文檔我們與同事或者其他人溝通的很大一部分都是通過文字來的。 作者:Artem Sapegin 編譯:胡子大哈 翻譯原文:http://huziketang.com/blog/posts/detail?postId=58aaa33bfc5b7f63e8c23f68...
閱讀 3729·2021-11-24 09:39
閱讀 3442·2019-08-30 15:56
閱讀 1370·2019-08-30 15:55
閱讀 1031·2019-08-30 15:53
閱讀 1918·2019-08-29 18:37
閱讀 3600·2019-08-29 18:32
閱讀 3128·2019-08-29 16:30
閱讀 2918·2019-08-29 15:14