Clean JavaScript:寫出整潔的JavaScript代碼

Clean JavaScript:寫出整潔的JavaScript代碼翻譯自clean-code-javascript。本文從屬於筆者的Web 前端入門與工程實踐javascript

Introduction:簡介

不少開發者都會推崇Robert C. Martin的Clean Code一書中說起的軟件工程準則,本文就是對於這些準則在JavaScript開發領域中的實踐應用總結。本文並不只僅是樣式指南,而是對於如何編寫出基於JavaScript實現的高可讀性、高可用性以及可重構的軟件系統。雖然本文對比的講了不少好壞的實踐,但並非說本文就建議你們強制遵循全部的指南。實際上對於Clean Code的概念不一樣的團隊、不一樣的開發者都會有不一樣的看法與見解,本文的不少觀點也是充滿爭議。軟件工程已經走過了五十多個年頭,而咱們也一直在前行,很難說有什麼原則是永恆正確的。做者更但願這些指南與考量起到試金石的做用,成爲評判團隊JavaScript代碼質量的考量標準之一。html

最後還須要強調的,好的代碼、好的架構都是慢慢衍化而來,切不可操之過急。千里之行,始於足下,在前行的道路上勢必會走不少的彎路、錯誤,可是隻要不斷調整方向總會回到正確的道路上。咱們不能畏懼改變,也不能把他人的話徹底奉爲圭臬,不管多少年的老程序員也是會犯錯的、前端

Variables:變量

使用有意義的可發音的變量名

Bad:java

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

Good:node

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

對相同類型的變量使用相同的關鍵字

Bad:git

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

Good:程序員

getUser();

使用可搜索的命名

在開發過程當中,咱們閱讀代碼的時間會遠遠超過編寫代碼的時間,所以保證代碼的可讀性與可搜索會很是重要。切記,沒事不要坑本身。github

Bad:express

//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可以方便地添加校驗。

  1. 封裝內部表述。

  2. 便於添加日誌與錯誤處理。

  3. 經過繼承可以複寫默認功能。

  4. 支持屬性懶加載。

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:

  1. High-level modules should not depend on low-level modules. Both should

  2. on abstractions.

  3. 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:

  1. Your inheritance represents an "is-a" relationship and not a "has-a"

  2. (Animal->Human vs. User->UserDetails).

  3. You can reuse code from the base classes (Humans can move like all animals).

  4. 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!
});

延伸閱讀

相關文章
相關標籤/搜索