JavaScript代碼整潔之道

概述

一張幽默的圖片:軟件質量經過你在閱讀代碼的時候有多少報怨來進行評估

Robert C. Martin 在《代碼整潔之道》 中提到的軟件工程原則,一樣適用於 JavaScript。這不是一個風格參考。它指導如何用 JavaScript 編寫可讀、可複用、可重構的軟件。javascript

並非每個原則都必須嚴格遵循,甚至不多獲得你們的認同。它們僅用於參考,不過要知道這些原則都是_《代碼整潔之道》_的做者們累積多年的集體經驗。html

咱們在軟件工程方面的技術發展剛剛超過 50 年,咱們仍然在學習不少東西。當軟件架構和架構自己同樣古老的時候,咱們應該遵循更爲嚴格規則。如今,對於你和你的團隊編寫的 JavaScript 代碼,不妨依據這些準則來進行質量評估。java

還有一件事:知道這些不會立刻讓你成爲更好的軟件開發者,在工做中常年使用這些準則不能讓你避免錯誤。每一段代碼都從最初的草圖開始到最終成型,就像爲溼粘土塑形同樣。最後,當咱們與同行一塊兒審查的時候,再把不完美的地方消除掉。不要由於初稿須要改善而否認本身,須要要否認的只是那些代碼!node

變量

使用有準確意義的變量名

很差:git

var yyyymmdstr = moment().format('YYYY/MM/DD');

:程序員

var yearMonthDay = moment().format('YYYY/MM/DD');

在變量的值不會改變時使用 ES6 的常量

在很差的示例中,變量能夠被改變。若是你申明一個常量,它會在整個程序中始終保持不變。github

很差:編程

var FIRST_US_PRESIDENT = "George Washington";

:設計模式

const FIRST_US_PRESIDENT = "George Washington";

對同一類型的變量使用相同的詞彙

很差:數組

getUserInfo();
getClientData();
getCustomerRecord();

:

getUser();

使用可檢索的名稱

咱們閱讀的代碼永遠比寫的折。寫可讀性強、易於檢索的的代碼很是重要。在程序中使用_無_明確意義的變量名會難以理解,對讀者形成傷害。因此,把名稱定義成可檢索的。

很差:

// 見鬼,525600 是個啥?
for (var i = 0; i < 525600; i++) {
  runCronJob();
}

:

// 用 `var` 申明爲大寫的全局變量
var MINUTES_IN_A_YEAR = 525600;
for (var i = 0; i < MINUTES_IN_A_YEAR; i++) {
  runCronJob();
}

使用解釋性的變量

很差:

const cityStateRegex = /^(.+)[,\\s]+(.+?)\s*(\d{5})?$/;
saveCityState(cityStateRegex.match(cityStateRegex)[1], cityStateRegex.match(cityStateRegex)[2]);

:

const cityStateRegex = /^(.+)[,\\s]+(.+?)\s*(\d{5})?$/;
const match = cityStateRegex.match(cityStateRegex)
const city = match[1];
const state = match[2];
saveCityState(city, state);

避免暗示

顯式優於隱式。

很差:

var locations = ['Austin', 'New York', 'San Francisco'];
locations.forEach((l) => {
  doStuff();
  doSomeOtherStuff();
  ...
  ...
  ...
  // 等等,`l` 又是什麼?
  dispatch(l);
});

:

var locations = ['Austin', 'New York', 'San Francisco'];
locations.forEach((location) => {
  doStuff();
  doSomeOtherStuff();
  ...
  ...
  ...
  dispatch(location);
});

不要添加不必的上下文

若是你的類名稱/對象名稱已經說明了它們是什麼,不要在(屬性)變量名裏重複。

很差:

var Car = {
  carMake: 'Honda',
  carModel: 'Accord',
  carColor: 'Blue'
};

function paintCar(car) {
  car.carColor = 'Red';
}

:

var Car = {
  make: 'Honda',
  model: 'Accord',
  color: 'Blue'
};

function paintCar(car) {
  car.color = 'Red';
}

短路語法比條件語句更清晰

很差:

function createMicrobrewery(name) {
  var breweryName;
  if (name) {
    breweryName = name;
  } else {
    breweryName = 'Hipster Brew Co.';
  }
}

:

function createMicrobrewery(name) {
  var breweryName = name || 'Hipster Brew Co.'
}

函數

函數參數 (理論上少於等於2個)

限制函數參數的數量極爲重要,它會讓你更容易測試函數。超過3個參數會致使組合膨脹,以至於你必須根據不一樣的參數對大量不一樣的狀況進行測試。

理想狀況下是沒有參數。有一個或者兩個參數也還好,三個就應該避免了。多於那個數量就應該考慮合併。一般狀況下,若是你有多於2個參數,你的函數會嘗試作太多事情。若是不是這樣,大多數時候可使用一個高階對象做爲參數使用。

既然 JavaScript 容許咱們在運行時隨意建立對象,而不須要預先定義樣板,那麼你在須要不少參數的時候就可使用一個對象來處理。

很差:

function createMenu(title, body, buttonText, cancellable) {
  ...
}

:

var menuConfig = {
  title: 'Foo',
  body: 'Bar',
  buttonText: 'Baz',
  cancellable: true
}

function createMenu(menuConfig) {
  ...
}

一個函數只作一件事

目前這是軟件工程中最重要的原則。若是函數作了較多的事情,它就難以組合、測試和推測。當你讓函數只作一件事情的時候,它們就很容易重構,並且代碼讀起來也會清晰得多。你只須要遵循本指南的這一條,就能領先於其餘不少開發者。

很差:

function emailClients(clients) {
  clients.forEach(client => {
    let clientRecord = database.lookup(client);
    if (clientRecord.isActive()) {
      email(client);
    }
  });
}

:

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

函數名稱要說明它作的事

很差:

function dateAdd(date, month) {
  // ...
}

let date = new Date();

// 很難從函數名瞭解到加了什麼
dateAdd(date, 1);

:

function dateAddMonth(date, month) {
  // ...
}

let date = new Date();
dateAddMonth(date, 1);

函數應該只抽象一個層次

若是你有多個層次的抽象,那麼你的函數一般作了太多事情,此時應該拆分函數使其易於複用和易於測試。

很差:

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...
  })
}

:

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 是弱類型語句,因此它很容易寫通用性強的函數。記得利用這一點!

很差:

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);
  });
}

:

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);
  });
}

使用默認參數代替短路表達式

很差:

function writeForumComment(subject, body) {
  subject = subject || 'No Subject';
  body = body || 'No text';
}

:

function writeForumComment(subject = 'No subject', body = 'No text') {
  ...
}

用 Object.assign 設置默認對象

很差:

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);

:

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 等於: {title: "Foo", body: "Bar", buttonText: "Baz", cancellable: true}
  // ...
}

createMenu(menuConfig);

不要把標記用做函數參數

標記告訴你的用戶這個函數作的事情不止一件。可是函數應該只作一件事。若是你的函數中會根據某個布爾參數產生不一樣的分支,那就拆分這個函數。

很差:

function createFile(name, temp) {
  if (temp) {
    fs.create('./temp/' + name);
  } else {
    fs.create(name);
  }
}

:

function createTempFile(name) {
  fs.create('./temp/' + name);
}

function createFile(name) {
  fs.create(name);
}

避免反作用

若是一個函數不是獲取一個輸入的值並返回其它值,它就有可能產生反作用。這些反作用多是寫入文件、修改一些全局變量,或者意外地把你全部錢轉給一個陌生人。

如今你確實須要在程序中有反作用。像前面提到的那樣,你可能須要寫入文件。如今你須要作的事情是搞清楚在哪裏集中完成這件事情。不要使用幾個函數或類來完成寫入某個特定文件的工做。採用一個,就一個服務來完成。

關鍵點是避免覺的陷阱,好比在沒有結構的對象間共享狀態,使用能夠被任意修改的易變的數據類型,沒有集中處理髮生的反作用等。若是你能作到,你就能比其餘大多數程序員更愉快。

很差:

// 下面的函數使用了全局變量。
// 若是有另外一個函數在使用 name,如今可能會由於 name 變成了數組而不能正常運行。
var name = 'Ryan McDermott';

function splitIntoFirstAndLastName() {
  name = name.split(' ');
}

splitIntoFirstAndLastName();

console.log(name); // ['Ryan', 'McDermott'];

:

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 中全局污染是一件糟糕的事情,由於它可能和另外庫發生衝突,然而使用你 API 的用戶卻不會知道——直到他們在生產中遇到一個異常。來思考一個例子:你想擴展 JavaScript 的原生 Array,使之擁有一個 diff 方法,用來展現兩數據以前的區別,這時你會怎麼作?你能夠給 Array.prototype 添加一個新的函數,但它可能會與其它想作一樣事情的庫發生衝突。若是那個庫實現的 diff 只是好比數組中第一個元素和最後一個元素的異同會發生什麼事情呢?這就是爲何最好是使用 ES6 的類語法從全局的 Array 派生一個類來作這件事。

很差:

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;
}

好:

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;
  }
}

喜歡上命令式編程之上的函數式編程

若是 Haskell 是 IPA 那麼 JavaScript 就是 O'Douls。就是說,與 Haskell 不一樣,JavaScript 不是函數式編程語言,不過它仍然有一點函數式的意味。函數式語言更整潔也更容易測試,因此你最好能喜歡上這種編程風格。

很差:

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;
}

:

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);

封裝條件

很差:

if (fsm.state === 'fetching' && isEmpty(listNode)) {
  /// ...
}

:

function shouldShowSpinner(fsm, listNode) {
  return fsm.state === 'fetching' && isEmpty(listNode);
}

if (shouldShowSpinner(fsmInstance, listNodeInstance)) {
  // ...
}

避免否認條件

很差:

function isDOMNodeNotPresent(node) {
  // ...
}

if (!isDOMNodeNotPresent(node)) {
  // ...
}

:

function isDOMNodePresent(node) {
  // ...
}

if (isDOMNodePresent(node)) {
  // ...
}

避免條件

這彷佛是個不可能完成的任務。大多數人第一次聽到這個的時候會說,「沒有 if 語句我該怎麼辦?」回答是在多數狀況下均可以使用多態來實現相同的任務。第二個問題一般是,「那太好了,不過我爲何要這麼作呢?」答案在於咱們以前瞭解過整潔的概念:一個函數應該只作一件事情。若是你的類和函數有 if 語句,就意味着你的函數作了更多的事。記住,只作一件事。

很差:

class Airplane {
  //...
  getCruisingAltitude() {
    switch (this.type) {
      case '777':
        return getMaxAltitude() - getPassengerCount();
      case 'Air Force One':
        return getMaxAltitude();
      case 'Cessna':
        return getMaxAltitude() - getFuelExpenditure();
    }
  }
}

:

class Airplane {
  //...
}

class Boeing777 extends Airplane {
  //...
  getCruisingAltitude() {
    return getMaxAltitude() - getPassengerCount();
  }
}

class AirForceOne extends Airplane {
  //...
  getCruisingAltitude() {
    return getMaxAltitude();
  }
}

class Cessna extends Airplane {
  //...
  getCruisingAltitude() {
    return getMaxAltitude() - getFuelExpenditure();
  }
}

避免類型檢查(第1部分)

JavaScript 是無類型的,也就是說函數能夠獲取任意類型的參數。有時候你會以爲這種自由是種折磨,於是會情不自禁地在函數中使用類型檢查。有不少種方法能夠避免類型檢查。首先要考慮的就是 API 的一致性。

很差:

function travelToTexas(vehicle) {
  if (vehicle instanceof Bicycle) {
    vehicle.peddle(this.currentLocation, new Location('texas'));
  } else if (vehicle instanceof Car) {
    vehicle.drive(this.currentLocation, new Location('texas'));
  }
}

:

function travelToTexas(vehicle) {
  vehicle.move(this.currentLocation, new Location('texas'));
}

避免類型檢查(第2部分)

若是你在處理基本類型的數據,好比字符串,整數和數組,又不能使用多態,這時你會以爲須要使用類型檢查,那麼能夠考慮 TypeScript。這是普通 JavaScript 的完美替代品,它在標準的 JavaScript 語法之上提供了靜態類型。普通 JavaScript 手工檢查類型的問題在於這樣會寫不少廢話,而人爲的「類型安全」並不能彌補損失的可讀性。讓你的 JavaScript 保持整潔,寫很好的測試,並保持良好的代碼審查。不然讓 TypeScript (我說過,這是很好的替代品)來作全部事情。

很差:

function combine(val1, val2) {
  if (typeof val1 == "number" && typeof val2 == "number" ||
      typeof val1 == "string" && typeof val2 == "string") {
    return val1 + val2;
  } else {
    throw new Error('Must be of type String or Number');
  }
}

:

function combine(val1, val2) {
  return val1 + val2;
}

不要過分優化

如今瀏覽器在運行時悄悄地作了不少優化工做。不少時候你的優化都是在浪費時間。這裏有很好的資源 能夠看看哪些優化比較缺少。把它們做爲目標,直到他們能固定下來的時候。

很差:

// 在舊瀏覽器中,每次循環的成本都比較高,由於每次都會重算 `len`。
// 如今瀏覽器中,這已經被優化了。
for (var i = 0, len = list.length; i < len; i++) {
  // ...
}

:

for (var i = 0; i < list.length; i++) {
  // ...
}

刪除不用的代碼

不用的代碼和重複的代碼同樣糟糕。在代碼庫中保留無用的代碼是毫無道理的事情。若是某段代碼用不到,那就刪掉它!若是你之後須要它,仍然能夠從代碼庫的歷史版本中找出來。

很差:

function oldRequestModule(url) {
  // ...
}

function newRequestModule(url) {
  // ...
}

var req = newRequestModule;
inventoryTracker('apples', req, 'www.inventory-awesome.io');

:

function newRequestModule(url) {
  // ...
}

var req = newRequestModule;
inventoryTracker('apples', req, 'www.inventory-awesome.io');

對象和數據結構

使用 getter 和 setter

JavaScript 沒有接口或者類型,也沒有像 publicprivate 這樣的關鍵字,因此很難應用設計模式。實事上,在對象上使用 getter 和 setter 訪問數據遠好於直接查找對象屬性。「爲何?」你可能會這樣問。那好,下面列出了緣由:

  1. 你想在獲取對象屬性的時候作更多的事,沒必要在代碼中尋找全部訪問的代碼來逐個修改。

  2. 在進行 set 的時候能夠進行額外的數據檢驗。

  3. 封裝內部表現。

  4. 在獲取或設置的時候易於添加日誌和錯誤處理。

  5. 繼承當前類,能夠重寫默認功能。

  6. 能夠對對象屬性進行懶加載,好比說從服務器獲取屬性的數據。

很差:

class BankAccount {
  constructor() {
       this.balance = 1000;
  }
}

let bankAccount = new BankAccount();

// 買鞋...
bankAccount.balance = bankAccount.balance - 100;

:

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();

// 買鞋...
bankAccount.withdraw(100);

讓對象擁有私有成員

這能夠經過閉包實現(ES5以以前的版本)。

很差:

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

:

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

單一職責原則 (SRP)

正如《代碼整潔之道》所說,「不該該有超過一個緣由來改變類」。往一個類裏塞進許多功能是件誘人的事情,就像在坐飛機的時候只帶一個手提箱同樣。這帶來的問題是,你的類不會在概念上有凝聚力,會有不少因素形成對它的改變。讓你的類須要改變的次數最少是件很是重要的事情。這是由於若是一個類裏塞入了太多功能,你只修改它的一部分,可能會讓人難以理解它爲什麼會影響代碼庫中其它相關模塊。

很差:

class UserSettings {
  constructor(user) {
    this.user = user;
  }

  changeSettings(settings) {
    if (this.verifyCredentials(user)) {
      // ...
    }
  }

  verifyCredentials(user) {
    // ...
  }
}

:

class UserAuth {
  constructor(user) {
    this.user = user;
  }

  verifyCredentials() {
    // ...
  }
}

class UserSettings {
  constructor(user) {
    this.user = user;
    this.auth = new UserAuth(user)
  }

  changeSettings(settings) {
    if (this.auth.verifyCredentials()) {
      // ...
    }
  }
}

開放封裝原則(OCP)

正如 Bertrand Meyer 所說,「軟件實體(類、模塊、函數等)應該對擴展開放,對修改封閉。」這是什麼意思呢?這個原則基本上規定了你應該容許用戶擴展你的模塊,但不須要打開 .js 源代碼文件來進行編輯。

很差:

class AjaxRequester {
  constructor() {
    // 若是咱們須要另外一個 HTTP 方法,好比 DELETE,該怎麼辦?
    // 咱們必須打開這個文件而後手工把它加進去
    this.HTTP_METHODS = ['POST', 'PUT', 'GET'];
  }

  get(url) {
    // ...
  }

}

:

class AjaxRequester {
  constructor() {
    this.HTTP_METHODS = ['POST', 'PUT', 'GET'];
  }

  get(url) {
    // ...
  }

  addHTTPMethod(method) {
    this.HTTP_METHODS.push(method);
  }
}

里氏替換原則(LSP)

這是一個嚇人的術語,但描述的倒是個簡單的概念。它的正式定義爲「若是 S 是 T 的子類,那全部 T 類型的對象均可以替換爲 S 類型的對象(即 S 類型的對象能夠替代 T 類型的對象),這個替換不會改變程序的任何性質(正確性、任務執行等)。」這確實是個嚇人的定義。

對此最好的解釋是,若是你有父類和子類,那麼父類和子類能夠交替使用而不會形成不正確的結果。這可能仍然讓人感到疑惑,那麼讓咱們看看經典的正方形和矩形的例子。在數學上,正方形也是矩形,可是若是你在模型中經過繼承使用 「is-a」 關係,你很快就會陷入困境。

很差:

class Rectangle {
  constructor() {
    this.width = 0;
    this.height = 0;
  }

  setColor(color) {
    // ...
  }

  render(area) {
    // ...
  }

  setWidth(width) {
    this.width = width;
  }

  setHeight(height) {
    this.height = height;
  }

  getArea() {
    return this.width * this.height;
  }
}

class Square extends Rectangle {
  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(); // 很差:這裏對正方形會返回 25,但應該是 20.
    rectangle.render(area);
  })
}

let rectangles = [new Rectangle(), new Rectangle(), new Square()];
renderLargeRectangles(rectangles);

:

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);

接口隔離原則(ISP)

JavaScript 中沒有接口,因此實行這個原則不能像其它語言那樣嚴格。然而即便對 JavaScript 的弱類型系統來講,它仍然是重要的相關。

ISP 指出,「客戶不該該依賴於那些他們不使用的接口。」 因爲 Duck Typing 理論,接口在 JavaScript 中是個隱性契約。

在 JavaScript 中有一個很好的例子來演示這個原則,即一個擁有巨大設置對象的類。比較好的作法是不要求客戶設置大量的選項,由於多數時候他們不須要全部設置。讓這些選項成爲可選的有助於防止「胖接口」。

很差:

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() {} // 多數時候咱們不須要動畫
  // ...
});

:

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

依賴倒置原則(DIP)

這個原則說明了兩個基本問題:

1. 上層模塊不該該依賴下層模塊,二者都應該依賴抽象。

2. 抽象不該該依賴於具體實現,具體實現應該依賴於抽象。

這一開始可能很難理解,可是若是你使用 Angular.js,你已經看到了對這個原則的一種實現形式:依賴注入(DI)。雖然它們不是徹底相同的概念,DIP 阻止上層模塊去了解下層模塊的細節並設置它們。它能夠經過 DI 來實現。這帶來的巨大好處下降了模塊間的耦合。耦合是種很是很差的開發模式,由於它讓代碼難以重構。

前提已經提到,JavaScript 沒有接口,所以抽象依賴於隱性契約。也就是說,一個對象/類會把方法和屬性暴露給另外一個對象/類。在下面的例子中,隱性契約是任何用於 InventoryTracker 的 Request 模塊都應該擁有 requestItems 方法。

很差:

class InventoryTracker {
  constructor(items) {
    this.items = items;

    // 很差:咱們建立了一個依賴於特定請求的實現。
    // 咱們應該只依賴請求方法:`request` 的 requestItems
    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();

:

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

// 經過構建外部依賴並注入它們,咱們很容易把請求模塊替換成
// 一個使用 WebSocket 的新模塊。
let inventoryTracker = new InventoryTracker(['apples', 'bananas'], new InventoryRequesterV2());
inventoryTracker.requestItems();

多用 ES6 類語法,少用 ES5 構造函數語法

在經典的 ES5 的類定義中,很難找到易讀的繼承、構造、方法定義等。若是你須要繼承(你會發現作不到),那就應該使用類語法。不過,應該儘量使用小函數而不是類,直到你須要更大更復雜的對象。

很差:

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

好:

class Animal {
    constructor(age) {
        this.age = age;
    }

    move() {}
}

class Mammal extends Animal {
    constructor(age, furColor) {
        super(age);
        this.furColor = furColor;
    }

    liveBirth() {}
}

class Human extends Mammal {
    constructor(age, furColor, languageSpoken) {
        super(age, furColor);
        this.languageSpoken = languageSpoken;
    }

    speak() {}
}

使用方法鏈

在這裏個人意見與《代碼整潔之道》的觀點不一樣。有人認爲方法鏈不整潔,並且違反了得墨忒耳定律。也許他們是對的,但這個模式在 JavaScript 中很是有用,你能夠不少庫中看到,好比 jQuery 和 Lodash。它讓代碼變得既簡潔又有表現力。在類中,只須要在每一個函數結束前返回 this,就實現了鏈式調用的類方法。

很差:

class Car {
  constructor() {
    this.make = 'Honda';
    this.model = 'Accord';
    this.color = 'white';
  }

  setMake(make) {
    this.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();

:

class Car {
  constructor() {
    this.make = 'Honda';
    this.model = 'Accord';
    this.color = 'white';
  }

  setMake(make) {
    this.name = name;
    // NOTE: 返回 this 以實現鏈式調用
    return this;
  }

  setModel(model) {
    this.model = model;
    // NOTE: 返回 this 以實現鏈式調用
    return this;
  }

  setColor(color) {
    this.color = color;
    // NOTE: 返回 this 以實現鏈式調用
    return this;
  }

  save() {
    console.log(this.make, this.model, this.color);
  }
}

let car = new Car()
  .setColor('pink')
  .setMake('Ford')
  .setModel('F-150')
  .save();

多用組合,少用繼承

你們都知道 GoF 的設計模式,其中提到應該多用組合而不是繼承。對於繼承和組合,都有大量的理由在支撐,但這個準則的要點在於,你的想法本能地會想到繼承,但這時候不防多思考一下用組合是否能更好的處理問題——某些時候,的確能。

你可能會考慮:「我何時該用繼承?」這取決於你遇到的問題。這裏有一個不錯的清單說明了何時用繼承比用組合更合適:

  1. 你的繼承是一個「is-a」關係,而不是「has-a」關係(Animal->Human 對比 User->UserDetails)。

  2. 能夠從基礎複用代碼 (人能夠像全部動物同樣移動)。

  3. 你想經過修改基礎來實現對全部子類的全局性更改。(改變更物移動時的熱量消耗)。

很差:

class Employee {
  constructor(name, email) {
    this.name = name;
    this.email = email;
  }

  // ...
}

// 這樣很差,由於 Employees "擁有" 稅務數據。EmployeeTaxData 不是屬於 Employee 的一個類型
class EmployeeTaxData extends Employee {
  constructor(ssn, salary) {
    super();
    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);
  }
  // ...
}

class EmployeeTaxData {
  constructor(ssn, salary) {
    this.ssn = ssn;
    this.salary = salary;
  }

  // ...
}

測試

測試比生產更重要。若是你不進行測試,或者測試的量不夠,那你就不能確定你寫的代碼不會形成破壞。測試數量依靠你的開發團隊來決定,但 100% 覆蓋率(全部語句和分支)能讓你擁有巨大的信心,也能使程序員們安心。也就是說,你須要一個不錯的測試框架,還須要一個好的覆蓋檢查工具.

沒有什麼理由可讓你不寫測試。這裏有 大量不錯的 JS 測試框架,能夠去找個大家團隊喜歡的來用。若是你找一個適合在你的團隊中使用的工做,就把爲每一個新產生的特性/方法添加測試做爲目標。若是你喜歡測試驅動開發(TDD)的方法,很是好,但要注意在讓你的測試覆蓋全部特性,或者重構過的代碼。

每次測試一個概念

很差:

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);
  });
});

:

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);
  });
});

Concurrency

使用 Promise 而不是回調

回調並不整潔,它會致使過多的嵌套。ES6 的 Promise 是個內置的全局類型。使用它!

很差:

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');
      }
    })
  }
})

:

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.error(err);
  })

async/await 比 Promise 還整潔

與回調至關,Promise 已經至關整潔了,但 ES7 帶來了更整潔的解決方案 —— async 和 await。你要作的事情就是在一個函數前加上 async 關鍵字,而後寫下命令形式的邏輯,而再也不須要 then 鏈。如今可使用這個 ES7 特性帶來的便利!

很差:

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.error(err);
  })

:

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);
    }
  }

錯誤處理

拋出錯誤是件好事!這表示運行時已經成功檢測到程序出錯了,它中止當前調用框上的函數執行,並停止進程(在 Node 中),最後在控制檯通知你,並輸出棧跟蹤信息。

不要忽略捕捉到的錯誤

捕捉到錯誤卻什麼也不錯,你就失去了糾正錯誤的機會。多數狀況下把錯誤記錄到控制檯(console.log)也不比忽略它好多少,由於在少許的控制檯信息中很難發現這一條。若是嘗試在 try/catch 中封裝代碼,就意味着你知道這裏可能發生錯,你應該在錯誤發生的時候有應對的計劃、或者處理辦法。

很差:

try {
  functionThatMightThrow();
} catch (error) {
  console.log(error);
}

好:

try {
  functionThatMightThrow();
} catch (error) {
  // 選擇之一(比 console.log 更鬧心):
  console.error(error);
  // 另外一個選擇:
  notifyUserOfError(error);
  // 另外一個選擇:
  reportErrorToService(error);
  // 或者全部上述三種選擇!
}

不要忽視被拒絕的Promise

這一條與不要忽略從 try/catch 捕捉到的錯誤有相同的緣由。

很差:

getdata()
.then(data => {
  functionThatMightThrow(data);
})
.catch(error => {
  console.log(error);
});

好:

getdata()
.then(data => {
  functionThatMightThrow(data);
})
.catch(error => {
  // 選擇之一(比 console.log 更鬧心):
  console.error(error);
  // 另外一個選擇:
  notifyUserOfError(error);
  // 另外一個選擇:
  reportErrorToService(error);
  // 或者全部上述三種選擇!
});

格式

格式是個很主觀的東西,像這裏提到的許多規則一,你沒必要徹底遵循。要點不在於爭論格式。大量工具 能夠自動處理優化格式。用一個!讓工程師爭論格式問題簡直就是在浪費時間和金錢。

對於那些不能自動處理的格式(能夠自動處理的包括縮進、Tab或空格、雙引號或單引用等),就看看這裏的指導。

使用一致的大小寫

JavaScript 是無類型的,因此大小寫能夠幫助你瞭解變量、函數等。這些規則具備較強的主觀性,因此你的團隊應該選擇須要的。重點不在於你選擇了什麼,而在於要始終保持一致。

很差:

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 {}

:

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 {}

函數調用者和被調用者應該儘量放在一塊兒

若是一個函數調用另外一個函數,那應該讓他們在源文件中的位置很是接近。理想狀況下應該把調用者放在被調用者的正上方,這會讓你的代碼更易讀,由於咱們都習慣從上往下讀代碼,就像讀報紙那樣。

很差:

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();

:

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();

Comments

只註釋業務邏輯複雜的內容

註釋是用來解釋代碼的,而不是必須的。好的代碼應該 _自注釋_。

很差:

function hashIt(data) {
  // Hash 碼
  var hash = 0;

  // 字符串長度
  var length = data.length;

  // 遍歷數據中全部字符
  for (var i = 0; i < length; i++) {
    // 獲取字符編碼
    var char = data.charCodeAt(i);
    // 生成 Hash
    hash = ((hash << 5) - hash) + char;
    // 轉換爲32位整數
    hash = hash & hash;
  }
}

:

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;

    // 轉換爲32位整數
    hash = hash & hash;
  }
}

不要把註釋掉的代碼留在代碼庫中

版本控制存在的緣由就是保存你的歷史代碼。

很差:

doStuff();
// doOtherStuff();
// doSomeMoreStuff();
// doSoMuchStuff();

:

doStuff();

不須要日誌式的註釋

記住,使用版本控制!沒用的代碼、註釋掉的代碼,尤爲是日誌式的註釋。用 git log 來獲取歷史信息!

很差:

/**
 * 2016-12-20: Removed monads, didn't understand them (RM)
 * 2016-10-01: Improved using special monads (JP)
 * 2016-02-03: Removed type-checking (LI)
 * 2015-03-14: Added combine with type-checking (JR)
 */
function combine(a, b) {
  return a + b;
}

:

function combine(a, b) {
  return a + b;
}

避免位置標記

位置標記一般只會添加垃圾信息。經過對函數或變量名以及適當的縮進就能爲代碼帶來良好的可視化結構。

很差:

////////////////////////////////////////////////////////////////////////////////
// Scope Model Instantiation
////////////////////////////////////////////////////////////////////////////////
let $scope.model = {
  menu: 'foo',
  nav: 'bar'
};

////////////////////////////////////////////////////////////////////////////////
// Action setup
////////////////////////////////////////////////////////////////////////////////
let actions = function() {
  // ...
}

:

let $scope.model = {
  menu: 'foo',
  nav: 'bar'
};

let actions = function() {
  // ...
}

避免在源文件中添加版權註釋

這是代碼文件樹頂層的 LICENSE 文件應該乾的事情。

很差:

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

:

function calculateBill() {
  // ...
}

本文轉載自:衆成翻譯
譯者:邊城
連接:http://www.zcfy.cc/article/2273
原文:https://github.com/ryanmcdermott/clean-code-javascript/blob/master/README.md

相關文章
相關標籤/搜索