JavaScript 程式設計風格指南

alivebao發表於2017-01-11

介紹

作者根據Robert C. Martin《程式碼整潔之道》總結了適用於JavaScript的軟體工程原則《Clean Code JavaScript》

本文是對其的翻譯。

不必嚴格遵守本文的所有原則,有時少遵守一些效果可能會更好,具體應根據實際情況決定。這是根據《程式碼整潔之道》作者多年經驗整理的程式碼優化建議,但也僅僅只是一份建議。

軟體工程已經發展了50多年,至今仍在不斷前進。現在,把這些原則當作試金石,嘗試將他們作為團隊程式碼質量考核的標準之一吧。

最後你需要知道的是,這些東西不會讓你立刻變成一個優秀的工程師,長期奉行他們也並不意味著你能夠高枕無憂不再犯錯。千里之行,始於足下。我們需要時常和同行們進行程式碼評審,不斷優化自己的程式碼。不要懼怕改善程式碼質量所需付出的努力,加油。

變數

使用有意義,可讀性好的變數名

反例:

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

正例:

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

使用ES6的const定義常量

反例中使用”var”定義的”常量”是可變的。

在宣告一個常量時,該常量在整個程式中都應該是不可變的。

反例:

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

正例:

// Declare them as capitalized `var` globals.
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個)

限制函式引數數量很有必要,這麼做使得在測試函式時更加輕鬆。過多的引數將導致難以採用有效的測試用例對函式的各個引數進行測試。

應避免三個以上引數的函式。通常情況下,引數超過兩個意味著函式功能過於複雜,這時需要重新優化你的函式。當確實需要多個引數時,大多情況下可以考慮這些引數封裝成一個物件。

JS定義物件非常方便,當需要多個引數時,可以使用一個物件進行替代。

反例:

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

移除重複的程式碼

永遠、永遠、永遠不要在任何迴圈下有重複的程式碼。

這種做法毫無意義且潛在危險極大。重複的程式碼意味著邏輯變化時需要對不止一處進行修改。JS弱型別的特點使得函式擁有更強的普適性。好好利用這一優點吧。

反例:

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

createMenu(menuConfig);

不要使用標記(Flag)作為函式引數

這通常意味著函式的功能的單一性已經被破壞。此時應考慮對函式進行再次劃分。

反例:

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

避免副作用

當函式產生了除了“接受一個值並返回一個結果”之外的行為時,稱該函式產生了副作用。比如寫檔案、修改全域性變數或將你的錢全轉給了一個陌生人等。

程式在某些情況下確實需要副作用這一行為,如先前例子中的寫檔案。這時應該將這些功能集中在一起,不要用多個函式/類修改某個檔案。用且只用一個service完成這一需求。

反例:

// Global variable referenced by following function.
// If we had another function that used this name, now it'd be an array and it could break it.
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'];

不要寫全域性函式

在JS中汙染全域性是一個非常不好的實踐,這麼做可能和其他庫起衝突,且呼叫你的API的使用者在實際環境中得到一個exception前對這一情況是一無所知的。

想象以下例子:如果你想擴充套件JS中的Array,為其新增一個diff函式顯示兩個陣列間的差異,此時應如何去做?你可以將diff寫入Array.prototype,但這麼做會和其他有類似需求的庫造成衝突。如果另一個庫對diff的需求為比較一個陣列中收尾元素間的差異呢?

使用ES6中的class對全域性的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;
  }
}

採用函數語言程式設計

函式式的程式設計具有更乾淨且便於測試的特點。儘可能的使用這種風格吧。

反例:

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完成其他功能呢?”許多情況下通過使用多型(polymorphism)可以達到同樣的目的。

第二個問題在於採用這種方式的原因是什麼。答案是我們之前提到過的:保持函式功能的單一性。

反例:

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

避免型別判斷(part 1)

JS是弱型別語言,這意味著函式可接受任意型別的引數。

有時這會對你帶來麻煩,你會對引數做一些型別判斷。有許多方法可以避免這些情況。

反例:

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

避免型別判斷(part 2)

如果需處理的資料為字串,整型,陣列等型別,無法使用多型並仍有必要對其進行型別檢測時,可以考慮使用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是因為在老式瀏覽器中,
// 直接使用正例中的方式會導致每次迴圈均重複計算list.length的值,
// 而在現代瀏覽器中會自動完成優化,這一行為是沒有必要的
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');

物件和資料結構

使用getters和setters

JS沒有介面或型別,因此實現這一模式是很困難的,因為我們並沒有類似publicprivate的關鍵詞。

然而,使用getters和setters獲取物件的資料遠比直接使用點操作符具有優勢。為什麼呢?

  1. 當需要對獲取的物件屬性執行額外操作時。
  2. 執行set時可以增加規則對要變數的合法性進行判斷。
  3. 封裝了內部邏輯。
  4. 在存取時可以方便的增加日誌和錯誤處理。
  5. 繼承該類時可以過載預設行為。
  6. 從伺服器獲取資料時可以進行懶載入。

反例:

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

let bankAccount = new BankAccount();

// Buy shoes...
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();

// Buy shoes...
bankAccount.withdraw(100);

讓物件擁有私有成員

可以通過閉包完成

反例:

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)

“程式碼實體(類,模組,函式等)應該易於擴充套件,難於修改。”

這一原則指的是我們應允許使用者方便的擴充套件我們程式碼模組的功能,而不需要開啟js檔案原始碼手動對其進行修改。

反例:

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

}

正例:

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

  get(url) {
    // ...
  }

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

利斯科夫替代原則 (LSP)

“子類物件應該能夠替換其超類物件被使用”。

也就是說,如果有一個父類和一個子類,當採用子類替換父類時不應該產生錯誤的結果。

反例:

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

正例:

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)

“客戶端不應該依賴它不需要的介面;一個類對另一個類的依賴應該建立在最小的介面上。”

在JS中,當一個類需要許多引數設定才能生成一個物件時,或許大多時候不需要設定這麼多的引數。此時減少對配置引數數量的需求是有益的。

反例:

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

正例:

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. 抽象介面應該脫離具體實現,具體實現應該依賴於抽象介面。

反例:

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

正例:

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的classes而不是ES5的Function

典型的ES5的類(function)在繼承、構造和方法定義方面可讀性較差。

當需要繼承時,優先選用classes。

但是,當在需要更大更復雜的物件時,最好優先選擇更小的function而非classes。

反例:

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

使用方法鏈

這裡我們的理解與《程式碼整潔之道》的建議有些不同。

有爭論說方法鏈不夠乾淨且違反了德米特法則,也許這是對的,但這種方法在JS及許多庫(如JQuery)中顯得非常實用。

因此,我認為在JS中使用方法鏈是非常合適的。在class的函式中返回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: 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();

優先使用組合模式而非繼承

在著名的設計模式一書中提到,應多使用組合模式而非繼承。

這麼做有許多優點,在想要使用繼承前,多想想能否通過組合模式滿足需求吧。

那麼,在什麼時候繼承具有更大的優勢呢?這取決於你的具體需求,但大多情況下,可以遵守以下三點:

  1. 繼承關係表現為”是一個”而非”有一個”(如動物->人 和 使用者->使用者細節)
  2. 可以複用基類的程式碼(“Human”可以看成是”All animal”的一種)
  3. 希望當基類改變時所有派生類都受到影響(如修改”all animals”移動時的卡路里消耗量)

反例:

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

  // ...
}

正例:

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

  // ...
}

測試

一些好的覆蓋工具.

一些好的JS測試框架

單一的測試每個概念

反例:

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

併發

用Promises替代回撥

回撥不夠整潔並會造成大量的巢狀。ES6內嵌了Promises,使用它吧。

反例:

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是較Promises更好的選擇

Promises是較回撥而言更好的一種選擇,但ES7中的async和await更勝過Promises。

在能使用ES7特性的情況下可以儘量使用他們替代Promises。

反例:

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

錯誤處理

錯誤丟擲是個好東西!這使得你能夠成功定位執行狀態中的程式產生錯誤的位置。

別忘了捕獲錯誤

對捕獲的錯誤不做任何處理是沒有意義的。

程式碼中try/catch的意味著你認為這裡可能出現一些錯誤,你應該對這些可能的錯誤存在相應的處理方案。

反例:

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

正例:

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

理由同try/catch.

反例:

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

正例:

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

格式化

格式化是一件主觀的事。如同這裡的許多規則一樣,這裡並沒有一定/立刻需要遵守的規則。可以在這裡完成格式的自動化。

大小寫一致

JS是弱型別語言,合理的採用大小寫可以告訴你關於變數/函式等的許多訊息。

這些規則是主觀定義的,團隊可以根據喜歡進行選擇。重點在於無論選擇何種風格,都需要注意保持一致性。

反例:

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

註釋

只對存在一定業務邏輯複製性的程式碼進行註釋

註釋並不是必須的,好的程式碼是能夠讓人一目瞭然,不用過多無謂的註釋。

反例:

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

正例:

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

不要在程式碼庫中遺留被註釋掉的程式碼

版本控制的存在是有原因的。讓舊程式碼存在於你的history裡吧。

反例:

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

相關文章