摘要:軟件工程已經走過了五十多個年頭,而我們也一直在前行,很難說有什么原則是永恒正確的。函數應當遵循單一職責原則這一條算是迄今為止軟件工程中最重要的原則之一了。
Introduction:簡介Clean JavaScript:寫出整潔的JavaScript代碼翻譯自clean-code-javascript。本文從屬于筆者的Web 前端入門與工程實踐。
很多開發者都會推崇Robert C. Martin的Clean Code一書中提及的軟件工程準則,本文就是對于這些準則在JavaScript開發領域中的實踐應用總結。本文并不僅僅是樣式指南,而是對于如何編寫出基于JavaScript實現的高可讀性、高可用性以及可重構的軟件系統。雖然本文對比的講了很多好壞的實踐,但并不是說本文就建議大家強制遵循所有的指南。實際上對于Clean Code的概念不同的團隊、不同的開發者都會有不同的見解與看法,本文的很多觀點也是充滿爭議。軟件工程已經走過了五十多個年頭,而我們也一直在前行,很難說有什么原則是永恒正確的。作者更希望這些指南與考量起到試金石的作用,成為評判團隊JavaScript代碼質量的考量標準之一。
最后還需要強調的,好的代碼、好的架構都是慢慢衍化而來,切不可操之過急。千里之行,始于足下,在前行的道路上勢必會走很多的彎路、錯誤,但是只要不斷調整方向總會回到正確的道路上。我們不能畏懼改變,也不能把他人的話完全奉為圭臬,無論多少年的老程序員也是會犯錯的、
Variables:變量 使用有意義的可發音的變量名Bad:
var yyyymmdstr = moment().format("YYYY/MM/DD");
Good:
var yearMonthDay = moment().format("YYYY/MM/DD");對相同類型的變量使用相同的關鍵字
Bad:
getUserInfo(); getClientData(); getCustomerRecord();
Good:
getUser();使用可搜索的命名
在開發過程中,我們閱讀代碼的時間會遠遠超過編寫代碼的時間,因此保證代碼的可讀性與可搜索會非常重要。切記,沒事不要坑自己。
Bad:
//525600到底啥意思? for (var i = 0; i < 525600; i++) { runCronJob(); }
Good:
// 聲明為全局變量 var MINUTES_IN_A_YEAR = 525600; for (var i = 0; i < MINUTES_IN_A_YEAR; i++) { runCronJob(); }使用說明性質的臨時變量
Bad:
let cityStateRegex = /^(.+)[,s]+(.+?)s*(d{5})?$/; saveCityState(cityStateRegex.match(cityStateRegex)[1], cityStateRegex.match(cityStateRegex)[2]);
Good:
let cityStateRegex = /^(.+)[,s]+(.+?)s*(d{5})?$/; let match = cityStateRegex.match(cityStateRegex) let city = match[1]; let state = match[2]; saveCityState(city, state);避免摸不著頭腦的臨時變量
在遍歷或者mapping過程中,需要避免短小無意義的變量命名。
Bad:
var locations = ["Austin", "New York", "San Francisco"]; locations.forEach((l) => { doStuff(); doSomeOtherStuff(); ... ... ... // Wait, what is `l` for again? dispatch(l); });
Good:
var locations = ["Austin", "New York", "San Francisco"]; locations.forEach((location) => { doStuff(); doSomeOtherStuff(); ... ... ... dispatch(location); });避免添加不需要的內容
如果你的類名/實例名已經能夠表述某些信息,那么在類/實例的屬性中就不需要重復命名。
Bad:
var Car = { carMake: "Honda", carModel: "Accord", carColor: "Blue" }; function paintCar(car) { car.carColor = "Red"; }
Good:
var Car = { make: "Honda", model: "Accord", color: "Blue" }; function paintCar(car) { car.color = "Red"; }Short-circuiting 優于條件選擇
Bad:
function createMicrobrewery(name) { var breweryName; if (name) { breweryName = name; } else { breweryName = "Hipster Brew Co."; } }
Good:
function createMicrobrewery(name) { var breweryName = name || "Hipster Brew Co." }函數 函數參數最好不超過兩個
限制函數的參數數目還是比較重要的,它能夠方便對于函數的測試,避免需要進行不同的Case測試時把代碼變得一團糟。我們應該盡可能地控制參數數目小于或等于兩個,如果你的參數數目多于兩個,那么建議使用高階對象進行適當封裝。
Bad:
function createMenu(title, body, buttonText, cancellable) { ... }
Good:
var menuConfig = { title: "Foo", body: "Bar", buttonText: "Baz", cancellable: true } function createMenu(menuConfig) { ... }函數應當遵循單一職責原則
這一條算是迄今為止軟件工程中最重要的原則之一了。如果我們給單一函數賦予了過多的職責,那么其很難被用于組合、測試等。而如果你保證函數的單一職責性質,那么相對其重構難度、代碼可讀性也會更好。
Bad:
function emailClients(clients) { clients.forEach(client => { let clientRecord = database.lookup(client); if (clientRecord.isActive()) { email(client); } }); }
Good:
function emailClients(clients) { clients.forEach(client => { emailClientIfNeeded(client); }); } function emailClientIfNeeded(client) { if (isClientActive(client)) { email(client); } } function isClientActive(client) { let clientRecord = database.lookup(client); return clientRecord.isActive(); }函數命名應該反映其功能
Bad:
function dateAdd(date, month) { // ... } let date = new Date(); // 很難從函數名中獲知該函數到底是誰加上誰 dateAdd(date, 1);
Good:
function dateAddMonth(date, month) { // ... } let date = new Date(); dateAddMonth(date, 1);函數應當只是一層抽象
這一條類似于單一職責原則,不過更傾向于關注函數的抽象程度,如果我們在單一函數中添加了過多的抽象層,同樣會降低的函數可讀性、增加重構難度。
Bad:
function parseBetterJSAlternative(code) { let REGEXES = [ // ... ]; let statements = code.split(" "); let tokens; REGEXES.forEach((REGEX) => { statements.forEach((statement) => { // ... }) }); let ast; tokens.forEach((token) => { // lex... }); ast.forEach((node) => { // parse... }) }
Good:
function tokenize(code) { let REGEXES = [ // ... ]; let statements = code.split(" "); let tokens; REGEXES.forEach((REGEX) => { statements.forEach((statement) => { // ... }) }); return tokens; } function lexer(tokens) { let ast; tokens.forEach((token) => { // lex... }); return ast; } function parseBetterJSAlternative(code) { let tokens = tokenize(code); let ast = lexer(tokens); ast.forEach((node) => { // parse... }) }移除重復代碼
在任何情況下都不要去容許重復代碼的存在。重復代碼指那些修改單一邏輯時需要修改多個代碼片的代碼交集,JavaScript本身是弱類型語言,相對而言編寫泛型函數會更加容易。
Bad:
function showDeveloperList(developers) { developers.forEach(developers => { var expectedSalary = developer.calculateExpectedSalary(); var experience = developer.getExperience(); var githubLink = developer.getGithubLink(); var data = { expectedSalary: expectedSalary, experience: experience, githubLink: githubLink }; render(data); }); } function showManagerList(managers) { managers.forEach(manager => { var expectedSalary = manager.calculateExpectedSalary(); var experience = manager.getExperience(); var portfolio = manager.getMBAProjects(); var data = { expectedSalary: expectedSalary, experience: experience, portfolio: portfolio }; render(data); }); }
Good:
function showList(employees) { employees.forEach(employee => { var expectedSalary = employee.calculateExpectedSalary(); var experience = employee.getExperience(); var portfolio; if (employee.type === "manager") { portfolio = employee.getMBAProjects(); } else { portfolio = employee.getGithubLink(); } var data = { expectedSalary: expectedSalary, experience: experience, portfolio: portfolio }; render(data); }); }使用默認參數代替或運算
Bad:
function writeForumComment(subject, body) { subject = subject || "No Subject"; body = body || "No text"; }
Good:
function writeForumComment(subject = "No subject", body = "No text") { ... }使用 Object.assign 設置默認值
Bad:
var menuConfig = { title: null, body: "Bar", buttonText: null, cancellable: true } function createMenu(config) { config.title = config.title || "Foo" config.body = config.body || "Bar" config.buttonText = config.buttonText || "Baz" config.cancellable = config.cancellable === undefined ? config.cancellable : true; } createMenu(menuConfig);
Good:
var menuConfig = { title: "Order", // User did not include "body" key buttonText: "Send", cancellable: true } function createMenu(config) { config = Object.assign({ title: "Foo", body: "Bar", buttonText: "Baz", cancellable: true }, config); // config now equals: {title: "Foo", body: "Bar", buttonText: "Baz", cancellable: true} // ... } createMenu(menuConfig);避免在參數中使用Flags
有的開發者會使用Flags來控制函數執行不同的邏輯流,不過就如我們在上文中提及的單一職責原則,我們應當將函數拆分為不同的部分,然后在外層調用上根據Flags調用不同的函數。
Bad:
function createFile(name, temp) { if (temp) { fs.create("./temp/" + name); } else { fs.create(name); } }
Good:
function createTempFile(name) { fs.create("./temp/" + name); } function createFile(name) { fs.create(name); }避免冗余副作用
如果某個函數除了接收輸入值與返回值之外還做了其他事,那么就稱其具有副作用。典型的副作用譬如寫文件、修改某些全局變量、修改內存參數等等。在編程中我們不可避免的需要產生副作用,譬如上面例子中我們需要寫入到某個外部文件。而你應當做的就是將所有的寫文件操作由某個服務統一處理,而不應該將寫文件的操作分散到數個類或者函數中。這一點最大的優勢在于避免了不同對象之間共享狀態,共享的可變狀態可是萬惡之源啊。
Bad:
// 定義全局變量 // 如果我們有其他的函數引用了該變量,那么我們就無法預測該變量類型 var name = "Ryan McDermott"; function splitIntoFirstAndLastName() { name = name.split(" "); } splitIntoFirstAndLastName(); console.log(name); // ["Ryan", "McDermott"];
Good:
function splitIntoFirstAndLastName(name) { return name.split(" "); } var name = "Ryan McDermott" var newName = splitIntoFirstAndLastName(name); console.log(name); // "Ryan McDermott"; console.log(newName); // ["Ryan", "McDermott"];避免污染全局函數
JavaScript中有個不太好的實踐就是修改某個全局函數,將其指向其他的庫或者自定義函數,不過這個會對某個懵懂的用戶造成困惱。如果你想給JavaScript原生的Array添加一個diff函數支持,來展示兩個數組的差異。你可以選擇將函數掛載到Array.prototype,不過很有可能跟其他打算占用這個位置的庫起沖突。我們更建議使用ES6的classes,并且使用繼承方式去添加新的功能函數。
Bad:
Array.prototype.diff = function(comparisonArray) { var values = []; var hash = {}; for (var i of comparisonArray) { hash[i] = true; } for (var i of this) { if (!hash[i]) { values.push(i); } } return values; }
Good:
class SuperArray extends Array { constructor(...args) { super(...args); } diff(comparisonArray) { var values = []; var hash = {}; for (var i of comparisonArray) { hash[i] = true; } for (var i of this) { if (!hash[i]) { values.push(i); } } return values; } }優先選擇函數式編程而不是命令式編程
JavaScript并不像Haskell這樣純粹的函數式編程語言,不過其對于實踐函數式編程的理念還是很推崇的。函數式編程可讀性更好,也更易于測試。
Bad:
const programmerOutput = [ { name: "Uncle Bobby", linesOfCode: 500 }, { name: "Suzie Q", linesOfCode: 1500 }, { name: "Jimmy Gosling", linesOfCode: 150 }, { name: "Gracie Hopper", linesOfCode: 1000 } ]; var totalOutput = 0; for (var i = 0; i < programmerOutput.length; i++) { totalOutput += programmerOutput[i].linesOfCode; }
Good:
const programmerOutput = [ { name: "Uncle Bobby", linesOfCode: 500 }, { name: "Suzie Q", linesOfCode: 1500 }, { name: "Jimmy Gosling", linesOfCode: 150 }, { name: "Gracie Hopper", linesOfCode: 1000 } ]; var totalOutput = programmerOutput .map((programmer) => programmer.linesOfCode) .reduce((acc, linesOfCode) => acc + linesOfCode, 0);封裝條件選擇
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)) { // ... }避免使用條件選擇
很多人第一次聽到這個概念都會覺得不可思議,沒有if條件選擇語句的話又該如何編程呢?在這里我們推薦使用多態性來達成這一目標,因為如果在函數或類中嵌入過多的if語句,會導致該函數或者類破壞單一職責原則。
Bad:
class Airplane { //... getCruisingAltitude() { switch (this.type) { case "777": return getMaxAltitude() - getPassengerCount(); case "Air Force One": return getMaxAltitude(); case "Cesna": return getMaxAltitude() - getFuelExpenditure(); } } }
Good:
class Airplane { //... } class Boeing777 extends Airplane { //... getCruisingAltitude() { return getMaxAltitude() - getPassengerCount(); } } class AirForceOne extends Airplane { //... getCruisingAltitude() { return getMaxAltitude(); } } class Cesna extends Airplane { //... getCruisingAltitude() { return getMaxAltitude() - getFuelExpenditure(); } }避免依賴于類型檢測
很多時候我們會依賴于JavaScript輸入的參數類型來進入不同的控制流,不過鑒于JavaScript本身是弱類型語言,我們還是應該避免這種實踐。第一個方法就是使用較為一致性的接口。
Bad:
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")); } }
Good:
function travelToTexas(vehicle) { vehicle.move(this.currentLocation, new Location("texas")); }避免依賴于類型檢測
如果你需要操作像字符串、數值、列表這樣的基礎數據類型,你就無法依賴于多態性來實現類型檢測。那么建議是使用TypeScript,它為普通的JavaScript添加了靜態類型支持。
Bad:
function combine(val1, val2) { if (typeof val1 == "number" && typeof val2 == "number" || typeof val1 == "string" && typeof val2 == "string") { return val1 + val2; } else { throw new Error("Must be of type String or Number"); } }
Good:
function combine(val1, val2) { return val1 + val2; }避免過度優化
現代瀏覽器已經在運行時做了很多的優化,因此很多時候如果我們要遵循那些流傳已久的優化策略不過是浪費時間。可以參考這個來獲取建議的優化要點。
Bad:
// On old browsers, each iteration would be costly because `len` would be // recomputed. In modern browsers, this is optimized. for (var i = 0, len = list.length; i < len; i++) { // ... }
Good:
for (var i = 0; i < list.length; i++) { // ... }移除棄用的代碼
棄用的代碼就和重復的代碼一樣,我們沒有任何理由保留他們。不過為防萬一建議不要徹底從Git的歷史記錄中刪除它們。
Bad:
function oldRequestModule(url) { // ... } function newRequestModule(url) { // ... } var req = newRequestModule; inventoryTracker("apples", req, "www.inventory-awesome.io");
Good:
function newRequestModule(url) { // ... } var req = newRequestModule; inventoryTracker("apples", req, "www.inventory-awesome.io");對象與數據結構 使用getters與setters
在JavaScript的對象屬性讀寫中,建議使用getter或者setter,而不是直接讀取或者賦值。不過JavaScript并沒有類似于public或者private這樣的關鍵字,因此很難通過接口方式進行強限制。不過鑒于以下優勢我們還是強烈建議使用getter或者setter:
1.如果你打算不僅僅是直接獲取原始值,使用getter能夠避免去修改每個取值的地方。
2.使用set能夠方便地添加校驗。
封裝內部表述。
便于添加日志與錯誤處理。
通過繼承能夠復寫默認功能。
支持屬性懶加載。
Bad:
class BankAccount { constructor() { this.balance = 1000; } } let bankAccount = new BankAccount(); // Buy shoes... bankAccount.balance = bankAccount.balance - 100;
Good:
class BankAccount { constructor() { this.balance = 1000; } // It doesn"t have to be prefixed with `get` or `set` to be a getter/setter withdraw(amount) { if (verifyAmountCanBeDeducted(amount)) { this.balance -= amount; } } } let bankAccount = new BankAccount(); // Buy shoes... bankAccount.withdraw(100);為對象添加私有屬性
可以通過閉包方式添加私有屬性:
Bad:
var Employee = function(name) { this.name = name; } Employee.prototype.getName = function() { return this.name; } var 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:
var Employee = (function() { function Employee(name) { this.getName = function() { return name; }; } return Employee; }()); var 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類 單一職責原則
便如Clean Code中所述,不應該為了多個理由去更改某個類的代碼,這樣會把某個類塞入過多的功能。最小化你需要去改變某個類的次數對于保證代碼的穩定性至關重要,過多的改變代碼會影響代碼庫中依賴于該類的其他模塊。
Bad:
class UserSettings { constructor(user) { this.user = user; } changeSettings(settings) { if (this.verifyCredentials(user)) { // ... } } verifyCredentials(user) { // ... } }
Good:
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()) { // ... } } }開放封閉原則
正如Bertrand Meyer所述,譬如類、模塊、函數這樣的實體應該面向擴展開放,而拒絕修改。換言之,我們推薦去繼承擴展某個函數或模塊,而不是每次都去修改源代碼。
Bad:
class AjaxRequester { constructor() { // What if we wanted another HTTP Method, like DELETE? We would have to // open this file up and modify this and put it in manually. this.HTTP_METHODS = ["POST", "PUT", "GET"]; } get(url) { // ... } }
Good:
class AjaxRequester { constructor() { this.HTTP_METHODS = ["POST", "PUT", "GET"]; } get(url) { // ... } addHTTPMethod(method) { this.HTTP_METHODS.push(method); } }里氏替換原則
這個原則聽起來有點拗口,不過概念卻很好理解。其形式化描述為如果S為T的子類型,那么類型T的實例可以被類型S的實例替換而不需要修改任何的代碼。形象而言,我們創建的父類與其子類應當可交換地使用而不會引起異常,譬如下文的Square-Rectangle這個例子。Square也是Rectangle:
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 { constructor() { super(); } 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); let area = rectangle.getArea(); // BAD: Will return 25 for Square. Should be 20. rectangle.render(area); }) } let rectangles = [new Rectangle(), new Rectangle(), new Square()]; renderLargeRectangles(rectangles);
Good:
class Shape { constructor() {} setColor(color) { // ... } render(area) { // ... } } class Rectangle extends Shape { constructor() { super(); this.width = 0; this.height = 0; } setWidth(width) { this.width = width; } setHeight(height) { this.height = height; } getArea() { return this.width * this.height; } } class Square extends Shape { constructor() { super(); this.length = 0; } setLength(length) { this.length = length; } getArea() { return this.length * this.length; } } function renderLargeShapes(shapes) { shapes.forEach((shape) => { switch (shape.constructor.name) { case "Square": shape.setLength(5); case "Rectangle": shape.setWidth(4); shape.setHeight(5); } let area = shape.getArea(); shape.render(area); }) } let shapes = [new Rectangle(), new Rectangle(), new Square()]; renderLargeShapes(shapes);接口隔離原則
JavaScript本身并不包含對于接口語法的支持,因此也無法像其他語言那樣達到嚴格限制的程度。不過鑒于JavaScript本身類型系統的缺失,遵循接口隔離原則還是蠻重要的。ISP的表述為不應該強制客戶端去依賴于他們不需要的接口,這一點在JavaScript中較為典型的例子就是那些需要大量配置信息的對象。其實使用者并不需要去關心每一個配置項,允許他們動態的設置能夠節省大量的時間,代碼的可讀性也會更好。
Bad:
class DOMTraverser { constructor(settings) { this.settings = settings; this.setup(); } setup() { this.rootNode = this.settings.rootNode; this.animationModule.setup(); } traverse() { // ... } } let $ = new DOMTraverser({ rootNode: document.getElementsByTagName("body"), animationModule: function() {} // 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() { // ... } } let $ = new DOMTraverser({ rootNode: document.getElementsByTagName("body"), options: { animationModule: function() {} } });依賴反轉原則
This principle states two essential things:
High-level modules should not depend on low-level modules. Both should
on abstractions.
Abstractions should not depend upon details. Details should depend on
abstractions.
This can be hard to understand at first, but if you"ve worked with Angular.js,
you"ve seen an implementation of this principle in the form of Dependency
Injection (DI). While they are not identical concepts, DIP keeps high-level
modules from knowing the details of its low-level modules and setting them up.
It can accomplish this through DI. A huge benefit of this is that it reduces
the coupling between modules. Coupling is a very bad development pattern because
it makes your code hard to refactor.
As stated previously, JavaScript doesn"t have interfaces so the abstractions
that are depended upon are implicit contracts. That is to say, the methods
and properties that an object/class exposes to another object/class. In the
example below, the implicit contract is that any Request module for an
InventoryTracker will have a requestItems method.
Bad:
class InventoryTracker { constructor(items) { this.items = items; // BAD: We have created a dependency on a specific request implementation. // We should just have requestItems depend on a request method: `request` this.requester = new InventoryRequester(); } requestItems() { this.items.forEach((item) => { this.requester.requestItem(item); }); } } class InventoryRequester { constructor() { this.REQ_METHODS = ["HTTP"]; } requestItem(item) { // ... } } let 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); }); } } class InventoryRequesterV1 { constructor() { this.REQ_METHODS = ["HTTP"]; } requestItem(item) { // ... } } class InventoryRequesterV2 { constructor() { this.REQ_METHODS = ["WS"]; } requestItem(item) { // ... } } // By constructing our dependencies externally and injecting them, we can easily // substitute our request module for a fancy new one that uses WebSockets. let inventoryTracker = new InventoryTracker(["apples", "bananas"], new InventoryRequesterV2()); inventoryTracker.requestItems();優先選擇ES6類而不是ES5的基本函數定義
傳統ES5的類實現語法對于類的繼承、構建以及方法定義的可讀性都不是很好。如果你考慮在類中實現繼承,那么建議優先考慮ES6的類語法糖。如果你只是需要構建簡單的對象,那么可以考慮使用ES5的基本函數定義來構造類對象。
Bad:
var Animal = function(age) { if (!(this instanceof Animal)) { throw new Error("Instantiate Animal with `new`"); } this.age = age; }; Animal.prototype.move = function() {}; var 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() {}; var 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() {};
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() {} }Use method chaining
Against the advice of Clean Code, this is one place where we will have to differ.
It has been argued that method chaining is unclean and violates the Law of Demeter.
Maybe it"s true, but this pattern is very useful in JavaScript and you see it in
many libraries such as jQuery and Lodash. It allows your code to be expressive,
and less verbose. For that reason, I say, use method chaining and take a look at
how clean your code will be. In your class functions, simply return this at
the end of every function, and you can chain further class methods onto it.
Bad:
class Car { constructor() { this.make = "Honda"; this.model = "Accord"; this.color = "white"; } setMake(make) { this.name = name; } setModel(model) { this.model = model; } setColor(color) { this.color = color; } save() { console.log(this.make, this.model, this.color); } } let car = new Car(); car.setColor("pink"); car.setMake("Ford"); car.setModel("F-150") car.save();
Good:
class Car { constructor() { this.make = "Honda"; this.model = "Accord"; this.color = "white"; } setMake(make) { this.name = name; // 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); } } let car = new Car() .setColor("pink") .setMake("Ford") .setModel("F-150") .save();Prefer composition over inheritance
As stated famously in the Gang of Four,
you should prefer composition over inheritance where you can. There are lots of
good reasons to use inheritance and lots of good reasons to use composition.
The main point for this maxim is that if your mind instinctively goes for
inheritance, try to think if composition could model your problem better. In some
cases it can.
You might be wondering then, "when should I use inheritance?" It
depends on your problem at hand, but this is a decent list of when inheritance
makes more sense than composition:
Your inheritance represents an "is-a" relationship and not a "has-a"
(Animal->Human vs. User->UserDetails).
You can reuse code from the base classes (Humans can move like all animals).
You want to make global changes to derived classes by changing a base class.
(Change the caloric expenditure of all animals when they move).
Bad:
class Employee { constructor(name, email) { this.name = name; this.email = email; } // ... } // Bad because Employees "have" tax data. EmployeeTaxData is not a type of Employee class EmployeeTaxData extends Employee { constructor(ssn, salary) { super(); this.ssn = ssn; this.salary = salary; } // ... }
Good:
class Employee { constructor(name, email) { this.name = name; this.email = email; } setTaxData(ssn, salary) { this.taxData = new EmployeeTaxData(ssn, salary); } // ... } class EmployeeTaxData { constructor(ssn, salary) { this.ssn = ssn; this.salary = salary; } // ... }測試
測試是代碼部署前不可避免的重要步驟,如果你沒有添加任何的測試,那么你在每次部署之前你壓根不敢確定是否會產生什么意外情況。不同的團隊對于測試覆蓋率的需求不太一致,不過保持100%的覆蓋率能夠讓你的團隊對于代碼保持較好的掌控與信賴。我們可以使用很多優秀的測試工具與測試覆蓋率檢測工具,建議是對于每個新的特征或者模塊都添加測試用例。如果更傾向于使用測試驅動開發,一定要注意在你打算添加新的特性或者重構當前代碼之前保證測試覆蓋率已經達到了預期。
每個測試用例單一目標Bad:
const assert = require("assert"); describe("MakeMomentJSGreatAgain", function() { it("handles date boundaries", function() { 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); }); });
Good:
const assert = require("assert"); describe("MakeMomentJSGreatAgain", function() { it("handles 30-day months", function() { let date = new MakeMomentJSGreatAgain("1/1/2015"); date.addDays(30); date.shouldEqual("1/31/2015"); }); it("handles leap year", function() { let date = new MakeMomentJSGreatAgain("2/1/2016"); date.addDays(28); assert.equal("02/29/2016", date); }); it("handles non-leap year", function() { let date = new MakeMomentJSGreatAgain("2/1/2015"); date.addDays(28); assert.equal("03/01/2015", date); }); });并發 使用Promise替代回調
回調含義不清晰,還會導致過深的代碼嵌套,就是所謂的回調地獄。在ES6中,Promises已經是內置的全局類型。
Bad:
require("request").get("https://en.wikipedia.org/wiki/Robert_Cecil_Martin", function(err, response) { if (err) { console.error(err); } else { require("fs").writeFile("article.html", response.body, function(err) { if (err) { console.error(err); } else { console.log("File written"); } }) } })
Good:
require("request-promise").get("https://en.wikipedia.org/wiki/Robert_Cecil_Martin") .then(function(response) { return require("fs-promise").writeFile("article.html", response); }) .then(function() { console.log("File written"); }) .catch(function(err) { console.log(err); })Async/Await 更為清晰
Promises本身已經是對于回調的不錯的替代,而ES7中的async與await則是更為清晰的解決方案,可以避免你編寫大量的then調用鏈。
Bad:
require("request-promise").get("https://en.wikipedia.org/wiki/Robert_Cecil_Martin") .then(function(response) { return require("fs-promise").writeFile("article.html", response); }) .then(function() { console.log("File written"); }) .catch(function(err) { console.log(err); })
Good:
async function getCleanCodeArticle() { try { var request = await require("request-promise") var response = await request.get("https://en.wikipedia.org/wiki/Robert_Cecil_Martin"); var fileHandle = await require("fs-promise"); await fileHandle.writeFile("article.html", response); console.log("File written"); } catch(err) { console.log(err); } }格式化
就像本文的很多建議一樣,格式化本身是非常主觀的原則。建議是使用工具 來自動完成格式化操作,而不是爭論具體的格式化的細節。
相似含義變量的大寫一致性JavaScript本身是無類型的,因此變量名大寫也能傳遞很多有用的信息。這個規則算是比較主觀的,建議團隊可以根據自己的內部規范將相同含義變量的大小寫保持一致性。
Bad:
var DAYS_IN_WEEK = 7; var daysInMonth = 30; var songs = ["Back In Black", "Stairway to Heaven", "Hey Jude"]; var Artists = ["ACDC", "Led Zeppelin", "The Beatles"]; function eraseDatabase() {} function restore_database() {} class animal {} class Alpaca {}
Good:
var DAYS_IN_WEEK = 7; var DAYS_IN_MONTH = 30; var songs = ["Back In Black", "Stairway to Heaven", "Hey Jude"]; var 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"); } lookupMananger() { return db.lookup(this.employee, "manager"); } getPeerReviews() { let peers = this.lookupPeers(); // ... } perfReview() { getPeerReviews(); getManagerReview(); getSelfReview(); } getManagerReview() { let manager = this.lookupManager(); } getSelfReview() { // ... } } let review = new PerformanceReview(user); review.perfReview();
Good:
class PerformanceReview { constructor(employee) { this.employee = employee; } perfReview() { getPeerReviews(); getManagerReview(); getSelfReview(); } getPeerReviews() { let peers = this.lookupPeers(); // ... } lookupPeers() { return db.lookup(this.employee, "peers"); } getManagerReview() { let manager = this.lookupManager(); } lookupMananger() { return db.lookup(this.employee, "manager"); } getSelfReview() { // ... } } let review = new PerformanceReview(employee); review.perfReview();注釋 僅僅對業務邏輯進行注釋
好的代碼應該是見名知義,注釋更多的是對于業務邏輯的描述說明。
Bad:
function hashIt(data) { // The hash var hash = 0; // Length of string var length = data.length; // Loop through every character in data for (var i = 0; i < length; i++) { // Get character code. var char = data.charCodeAt(i); // Make the hash hash = ((hash << 5) - hash) + char; // Convert to 32-bit integer hash = hash & hash; } }
Good:
function hashIt(data) { var hash = 0; var length = data.length; for (var i = 0; i < length; i++) { var char = data.charCodeAt(i); hash = ((hash << 5) - hash) + char; // Convert to 32-bit integer hash = hash & hash; } }避免保留被注釋的代碼
Bad:
doStuff(); // doOtherStuff(); // doSomeMoreStuff(); // doSoMuchStuff();
Good:
doStuff();不要使用日記形式的注釋
千萬記住,要使用版本控制工具,而不是在你的代碼前面添加日記形式的注釋,使用git log查看歷史記錄。
Bad:
/** * 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; }
Good:
function combine(a, b) { return a + b; }避免額外的代碼標記注釋
建議是讓函數與變量名來表述其功能,避免添加過多額外的注釋。
Bad:
//////////////////////////////////////////////////////////////////////////////// // Scope Model Instantiation //////////////////////////////////////////////////////////////////////////////// let $scope.model = { menu: "foo", nav: "bar" }; //////////////////////////////////////////////////////////////////////////////// // Action setup //////////////////////////////////////////////////////////////////////////////// let actions = function() { // ... }
Good:
let $scope.model = { menu: "foo", nav: "bar" }; let actions = function() { // ... }避免在源文件中添加法律聲明
Bad:
/* The MIT License (MIT) Copyright (c) 2016 Ryan McDermott Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE */ function calculateBill() { // ... }
Good:
function calculateBill() { // ... }錯誤處理
在JavaScript中拋出錯誤是個不錯的實踐,不僅可以幫助開發者即時感知程序中出現的錯誤,還能立刻終止程序執行并且打印出其調用棧。
不要忽略被捕獲的錯誤如果我們只是簡單地捕獲錯誤而沒有將其反饋給相對應的開發人員或者將該錯誤記錄下來,那么我們進行錯誤處理就沒有什么意義。
Bad:
try { functionThatMightThrow(); } catch (error) { console.log(error); }
Good:
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! }不要忽略被拒絕的Promises
Bad:
getdata() .then(data => { functionThatMightThrow(data); }) .catch(error => { console.log(error); });
Good:
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! });
延伸閱讀
知乎專欄:某熊的全棧之路
知乎專欄:前端當自強
知乎專欄:lotuc的編程之路
2016-我的技術之路:編程知識體系結構
2016-我的前端之路:工具化與工程化
某熊周刊系列:一周推薦外文技術資料(12.1)
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/81116.html
摘要:壞的命名方式好的命名方式避免使用過多參數。你只需要在每一個函數的末尾返回,之后的代碼會更加的簡潔。在很多情況下,你可能搞出重復代碼。自從年雙十一正式上線,累計處理了億錯誤事件,付費客戶有金山軟件百姓網等眾多品牌企業。 譯者按: 簡潔的代碼可以避免寫出過多的BUG。 原文: JavaScript Clean Code - Best Practices 譯者: Fundebug ...
摘要:設計模式是以面向對象編程為基礎的,的面向對象編程和傳統的的面向對象編程有些差別,這讓我一開始接觸的時候感到十分痛苦,但是這只能靠自己慢慢積累慢慢思考。想繼續了解設計模式必須要先搞懂面向對象編程,否則只會讓你自己更痛苦。 JavaScript 中的構造函數 學習總結。知識只有分享才有存在的意義。 是時候替換你的 for 循環大法了~ 《小分享》JavaScript中數組的那些迭代方法~ ...
摘要:循環的函數式改造翻譯自。循環的設計思想深受可變狀態與副作用的影響,不過函數式編程中認為可變狀態與副作用是導致潛在錯誤與不可預測性的罪魁禍首,是應該盡力避免的模式。 JavaScript For 循環的函數式改造翻譯自Rethinking JavaScript: Death of the For Loop。前兩天筆者整理了一篇JavaScript 函數式編程導論,筆者個人不是很喜歡徹底的...
摘要:代碼整潔之道整潔的代碼不僅僅是讓人看起來舒服,更重要的是遵循一些規范能夠讓你的代碼更容易維護,同時降低幾率。另外這不是強制的代碼規范,就像原文中說的,。里式替換原則父類和子類應該可以被交換使用而不會出錯。注釋好的代碼是自解釋的。 JavaScript代碼整潔之道 整潔的代碼不僅僅是讓人看起來舒服,更重要的是遵循一些規范能夠讓你的代碼更容易維護,同時降低bug幾率。 原文clean-c...
閱讀 2415·2021-09-01 10:41
閱讀 1439·2019-08-30 14:12
閱讀 507·2019-08-29 12:32
閱讀 2856·2019-08-29 12:25
閱讀 2934·2019-08-28 18:30
閱讀 1704·2019-08-26 11:47
閱讀 973·2019-08-26 10:35
閱讀 2586·2019-08-23 18:06