JavaScript程式碼簡潔之道

Fundebug發表於2019-01-09

摘要: 可以說是《Clean Code》的JS程式碼示例了,值得參考。

Fundebug經授權轉載,版權歸原作者所有。

測試程式碼質量的唯一方式:別人看你程式碼時說 f * k 的次數。

程式碼質量與其整潔度成正比。乾淨的程式碼,既在質量上較為可靠,也為後期維護、升級奠定了良好基礎。

本文並不是程式碼風格指南,而是關於程式碼的可讀性複用性擴充套件性探討。

我們將從幾個方面展開討論:

  • 變數
  • 函式
  • 物件和資料結構
  • SOLID
  • 測試
  • 非同步
  • 錯誤處理
  • 程式碼風格
  • 註釋

變數

用有意義且常用的單詞命名變數

Bad:

 const yyyymmdstr = moment().format(`YYYY/MM/DD`);

Good:

 const currentDate = moment().format(`YYYY/MM/DD`);

保持統一

可能同一個專案對於獲取使用者資訊,會有三個不一樣的命名。應該保持統一,如果你不知道該如何取名,可以去 codelf 搜尋,看別人是怎麼取名的。

Bad:

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

Good

getUser()

每個常量都該命名

可以用 buddy.js 或者 ESLint 檢測程式碼中未命名的常量。

Bad:

// 三個月之後你還能知道 86400000 是什麼嗎?
setTimeout(blastOff, 86400000);

Good

const MILLISECOND_IN_A_DAY = 86400000;

setTimeout(blastOff, MILLISECOND_IN_A_DAY);

可描述

通過一個變數生成了一個新變數,也需要為這個新變數命名,也就是說每個變數當你看到他第一眼你就知道他是幹什麼的。

Bad:

const ADDRESS = `One Infinite Loop, Cupertino 95014`;
const CITY_ZIP_CODE_REGEX = /^[^,\]+[,\s]+(.+?)s*(d{5})?$/;
saveCityZipCode(ADDRESS.match(CITY_ZIP_CODE_REGEX)[1], address.match(CITY_ZIP_CODE_REGEX)[2]);

Good

const ADDRESS = `One Infinite Loop, Cupertino 95014`;
const CITY_ZIP_CODE_REGEX = /^[^,\]+[,\s]+(.+?)s*(d{5})?$/;
const [, city, zipCode] = ADDRESS.match(CITY_ZIP_CODE_REGEX) || [];
saveCityZipCode(city, zipCode);

直接了當

Bad:

const locations = [`Austin`, `New York`, `San Francisco`];
locations.forEach((l) => {
  doStuff();
  doSomeOtherStuff();
  // ...
  // ...
  // ...
  // 需要看其他程式碼才能確定 `l` 是幹什麼的。
  dispatch(l);
});

Good

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

避免無意義的字首

如果建立了一個物件 car,就沒有必要把它的顏色命名為 carColor。

Bad:

const car = {
  carMake: `Honda`,
  carModel: `Accord`,
  carColor: `Blue`
};

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

Good

const car = {
  make: `Honda`,
  model: `Accord`,
  color: `Blue`
};

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

使用預設值

Bad:

function createMicrobrewery(name) {
  const breweryName = name || `Hipster Brew Co.`;
  // ...
}

Good

function createMicrobrewery(name = `Hipster Brew Co.`) {
  // ...
}

函式

引數越少越好

如果引數超過兩個,使用 ES2015/ES6 的解構語法,不用考慮引數的順序。

Bad:

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

Good

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

createMenu({
  title: `Foo`,
  body: `Bar`,
  buttonText: `Baz`,
  cancellable: true
});

只做一件事情

這是一條在軟體工程領域流傳久遠的規則。嚴格遵守這條規則會讓你的程式碼可讀性更好,也更容易重構。如果違反這個規則,那麼程式碼會很難被測試或者重用。

Bad:

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

Good

function emailActiveClients(clients) {
  clients
    .filter(isActiveClient)
    .forEach(email);
}
function isActiveClient() {
  const clientRecord = database.lookup(client);    
  return clientRecord.isActive();
}

顧名思義

看函式名就應該知道它是幹啥的。

Bad:

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

const date = new Date();

// 很難知道是把什麼加到日期中
addToDate(date, 1);

Good

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

const date = new Date();
addMonthToDate(1, date);

只需要一層抽象層

如果函式巢狀過多會導致很難複用以及測試。

Bad:

function parseBetterJSAlternative(code) {
  const REGEXES = [
    // ...
  ];

  const statements = code.split(` `);
  const tokens = [];
  REGEXES.forEach((REGEX) => {
    statements.forEach((statement) => {
      // ...
    });
  });

  const ast = [];
  tokens.forEach((token) => {
    // lex...
  });

  ast.forEach((node) => {
    // parse...
  });
}

Good

function parseBetterJSAlternative(code) {
  const tokens = tokenize(code);
  const ast = lexer(tokens);
  ast.forEach((node) => {
    // parse...
  });
}

function tokenize(code) {
  const REGEXES = [
    // ...
  ];

  const statements = code.split(` `);
  const tokens = [];
  REGEXES.forEach((REGEX) => {
    statements.forEach((statement) => {
      tokens.push( /* ... */ );
    });
  });

  return tokens;
}

function lexer(tokens) {
  const ast = [];
  tokens.forEach((token) => {
    ast.push( /* ... */ );
  });

  return ast;
}

刪除重複程式碼

很多時候雖然是同一個功能,但由於一兩個不同點,讓你不得不寫兩個幾乎相同的函式。

要想優化重複程式碼需要有較強的抽象能力,錯誤的抽象還不如重複程式碼。所以在抽象過程中必須要遵循 SOLID 原則(SOLID 是什麼?稍後會詳細介紹)。

Bad:

function showDeveloperList(developers) {
  developers.forEach((developer) => {
    const expectedSalary = developer.calculateExpectedSalary();
    const experience = developer.getExperience();
    const githubLink = developer.getGithubLink();
    const data = {
      expectedSalary,
      experience,
      githubLink
    };

    render(data);
  });
}

function showManagerList(managers) {
  managers.forEach((manager) => {
    const expectedSalary = manager.calculateExpectedSalary();
    const experience = manager.getExperience();
    const portfolio = manager.getMBAProjects();
    const data = {
      expectedSalary,
      experience,
      portfolio
    };

    render(data);
  });
}

Good

function showEmployeeList(employees) {
  employees.forEach(employee => {
    const expectedSalary = employee.calculateExpectedSalary();
    const experience = employee.getExperience();
    const data = {
      expectedSalary,
      experience,
    };
    
    switch(employee.type) {
      case `develop`:
        data.githubLink = employee.getGithubLink();
        break
      case `manager`:
        data.portfolio = employee.getMBAProjects();
        break
    }
    render(data);
  })
}

物件設定預設屬性

Bad:

const menuConfig = {
  title: null,
  body: `Bar`,
  buttonText: null,
  cancellable: true
};

function createMenu(config) {
  config.title = config.title || `Foo`;
  config.body = config.body || `Bar`;
  config.buttonText = config.buttonText || `Baz`;
  config.cancellable = config.cancellable !== undefined ? config.cancellable : true;
}

createMenu(menuConfig);

Good

const menuConfig = {
  title: `Order`,
  // `body` key 缺失
  buttonText: `Send`,
  cancellable: true
};

function createMenu(config) {
  config = Object.assign({
    title: `Foo`,
    body: `Bar`,
    buttonText: `Baz`,
    cancellable: true
  }, config);

  // config 就變成了: {title: "Order", body: "Bar", buttonText: "Send", cancellable: true}
  // ...
}

createMenu(menuConfig);

不要傳 flag 引數

通過 flag 的 true 或 false,來判斷執行邏輯,違反了一個函式幹一件事的原則。

Bad:

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

Good

function createFile(name) {
  fs.create(name);
}
function createFileTemplate(name) {
  createFile(`./temp/${name}`)
}

避免副作用(第一部分)

函式接收一個值返回一個新值,除此之外的行為我們都稱之為副作用,比如修改全域性變數、對檔案進行 IO 操作等。
當函式確實需要副作用時,比如對檔案進行 IO 操作時,請不要用多個函式/類進行檔案操作,有且僅用一個函式/類來處理。也就是說副作用需要在唯一的地方處理。
副作用的三大天坑:隨意修改可變資料型別、隨意分享沒有資料結構的狀態、沒有在統一地方處理副作用。

Bad:

// 全域性變數被一個函式引用
// 現在這個變數從字串變成了陣列,如果有其他的函式引用,會發生無法預見的錯誤。
var name = `Ryan McDermott`;

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

splitIntoFirstAndLastName();

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

Good

var name = `Ryan McDermott`;
var newName = splitIntoFirstAndLastName(name)

function splitIntoFirstAndLastName(name) {
  return name.split(` `);
}

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

避免副作用(第二部分)

在 JavaScript 中,基本型別通過賦值傳遞,物件和陣列通過引用傳遞。以引用傳遞為例:
假如我們寫一個購物車,通過 addItemToCart() 方法新增商品到購物車,修改 購物車陣列。此時呼叫 purchase() 方法購買,由於引用傳遞,獲取的 購物車陣列 正好是最新的資料。

看起來沒問題對不對?

如果當使用者點選購買時,網路出現故障, purchase() 方法一直在重複呼叫,與此同時使用者又新增了新的商品,這時網路又恢復了。那麼 purchase() 方法獲取到 購物車陣列 就是錯誤的。

為了避免這種問題,我們需要在每次新增商品時,克隆 購物車陣列 並返回新的陣列。

Bad:

const addItemToCart = (cart, item) => {
  cart.push({ item, date: Date.now() });
};

Good

const addItemToCart = (cart, item) => {
  return [...cart, {item, date: Date.now()}]
};

不要寫全域性方法

在 JavaScript 中,永遠不要汙染全域性,會在生產環境中產生難以預料的 bug。舉個例子,比如你在 Array.prototype 上新增一個 diff 方法來判斷兩個陣列的不同。而你同事也打算做類似的事情,不過他的 diff 方法是用來判斷兩個陣列首位元素的不同。很明顯你們方法會產生衝突,遇到這類問題我們可以用 ES2015/ES6 的語法來對 Array 進行擴充套件。
Bad:

Array.prototype.diff = function diff(comparisonArray) {
  const hash = new Set(comparisonArray);
  return this.filter(elem => !hash.has(elem));
};

Good

class SuperArray extends Array {
  diff(comparisonArray) {
    const hash = new Set(comparisonArray);
    return this.filter(elem => !hash.has(elem));        
  }
}

比起命令式我更喜歡函數語言程式設計

函式式變程式設計可以讓程式碼的邏輯更清晰更優雅,方便測試。

Bad:

const programmerOutput = [
  {
    name: `Uncle Bobby`,
    linesOfCode: 500
  }, {
    name: `Suzie Q`,
    linesOfCode: 1500
  }, {
    name: `Jimmy Gosling`,
    linesOfCode: 150
  }, {
    name: `Gracie Hopper`,
    linesOfCode: 1000
  }
];

let totalOutput = 0;

for (let i = 0; i < programmerOutput.length; i++) {
  totalOutput += programmerOutput[i].linesOfCode;
}

Good

const programmerOutput = [
  {
    name: `Uncle Bobby`,
    linesOfCode: 500
  }, {
    name: `Suzie Q`,
    linesOfCode: 1500
  }, {
    name: `Jimmy Gosling`,
    linesOfCode: 150
  }, {
    name: `Gracie Hopper`,
    linesOfCode: 1000
  }
];
let totalOutput = programmerOutput
  .map(output => output.linesOfCode)
  .reduce((totalLines, lines) => totalLines + lines, 0)

封裝條件語句

Bad:

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

Good

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

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

儘量別用“非”條件句

Bad:

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

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

Good

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

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

避免使用條件語句

Q:不用條件語句寫程式碼是不可能的。
A:絕大多數場景可以用多型替代。
Q:用多型可行,但為什麼就不能用條件語句了呢?
A:為了讓程式碼更簡潔易讀,如果你的函式中出現了條件判斷,那麼說明你的函式不止幹了一件事情,違反了函式單一原則。

Bad:

class Airplane {
  // ...
  
  // 獲取巡航高度
  getCruisingAltitude() {
    switch (this.type) {
      case `777`:
        return this.getMaxAltitude() - this.getPassengerCount();
      case `Air Force One`:
        return this.getMaxAltitude();
      case `Cessna`:
        return this.getMaxAltitude() - this.getFuelExpenditure();
    }
  }
}

Good

class Airplane {
  // ...
}
// 波音777
class Boeing777 extends Airplane {
  // ...
  getCruisingAltitude() {
    return this.getMaxAltitude() - this.getPassengerCount();
  }
}
// 空軍一號
class AirForceOne extends Airplane {
  // ...
  getCruisingAltitude() {
    return this.getMaxAltitude();
  }
}
// 賽納斯飛機
class Cessna extends Airplane {
  // ...
  getCruisingAltitude() {
    return this.getMaxAltitude() - this.getFuelExpenditure();
  }
}

避免型別檢查(第一部分)

JavaScript 是無型別的,意味著你可以傳任意型別引數,這種自由度很容易讓人困擾,不自覺的就會去檢查型別。仔細想想是你真的需要檢查型別還是你的 API 設計有問題?

Bad:

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

Good

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

避免型別檢查(第二部分)

如果你需要做靜態型別檢查,比如字串、整數等,推薦使用 TypeScript,不然你的程式碼會變得又臭又長。

Bad:

function combine(val1, val2) {
  if (typeof val1 === `number` && typeof val2 === `number` ||
      typeof val1 === `string` && typeof val2 === `string`) {
    return val1 + val2;
  }

  throw new Error(`Must be of type String or Number`);
}

Good

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

不要過度優化

現代瀏覽器已經在底層做了很多優化,過去的很多優化方案都是無效的,會浪費你的時間,想知道現代瀏覽器優化了哪些內容,請點這裡

Bad:

// 在老的瀏覽器中,由於 `list.length` 沒有做快取,每次迭代都會去計算,造成不必要開銷。
// 現代瀏覽器已對此做了優化。
for (let i = 0, len = list.length; i < len; i++) {
  // ...
}

Good

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

刪除棄用程式碼

很多時候有些程式碼已經沒有用了,但擔心以後會用,捨不得刪。

如果你忘了這件事,這些程式碼就永遠存在那裡了。

放心刪吧,你可以在程式碼庫歷史版本中找他它。

Bad:

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

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

const req = newRequestModule;
inventoryTracker(`apples`, req, `www.inventory-awesome.io`);

Good

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

const req = newRequestModule;
inventoryTracker(`apples`, req, `www.inventory-awesome.io`);

物件和資料結構

用 get、set 方法運算元據

這樣做可以帶來很多好處,比如在運算元據時打日誌,方便跟蹤錯誤;在 set 的時候很容易對資料進行校驗…

Bad:

function makeBankAccount() {
  // ...

  return {
    balance: 0,
    // ...
  };
}

const account = makeBankAccount();
account.balance = 100;

Good

function makeBankAccount() {
  // 私有變數
  let balance = 0;

  function getBalance() {
    return balance;
  }
  
  function setBalance(amount) {
    // ... 在更新 balance 前,對 amount 進行校驗
    balance = amount;
  }

  return {
    // ...
    getBalance,
    setBalance,
  };
}

const account = makeBankAccount();
account.setBalance(100);

使用私有變數

可以用閉包來建立私有變數

Bad:

const Employee = function(name) {
  this.name = name;
};

Employee.prototype.getName = function getName() {
  return this.name;
};

const employee = new Employee(`John Doe`);
console.log(`Employee name: ${employee.getName()}`); // Employee name: John Doe
delete employee.name;
console.log(`Employee name: ${employee.getName()}`); // Employee name: undefined

Good

function makeEmployee(name) {
  return {
    getName() {
      return name;
    },
  };
}

const employee = makeEmployee(`John Doe`);
console.log(`Employee name: ${employee.getName()}`); // Employee name: John Doe
delete employee.name;
console.log(`Employee name: ${employee.getName()}`); // Employee name: John Doe

使用 class

在 ES2015/ES6 之前,沒有類的語法,只能用建構函式的方式模擬類,可讀性非常差。

Bad:

// 動物
const Animal = function(age) {
  if (!(this instanceof Animal)) {
    throw new Error(`Instantiate Animal with `new``);
  }

  this.age = age;
};

Animal.prototype.move = function move() {};

// 哺乳動物
const Mammal = function(age, furColor) {
  if (!(this instanceof Mammal)) {
    throw new Error(`Instantiate Mammal with `new``);
  }

  Animal.call(this, age);
  this.furColor = furColor;
};

Mammal.prototype = Object.create(Animal.prototype);
Mammal.prototype.constructor = Mammal;
Mammal.prototype.liveBirth = function liveBirth() {};

// 人類
const Human = function(age, furColor, languageSpoken) {
  if (!(this instanceof Human)) {
    throw new Error(`Instantiate Human with `new``);
  }

  Mammal.call(this, age, furColor);
  this.languageSpoken = languageSpoken;
};

Human.prototype = Object.create(Mammal.prototype);
Human.prototype.constructor = Human;
Human.prototype.speak = function speak() {};

Good

// 動物
class Animal {
  constructor(age) {
    this.age = age
  };
  move() {};
}

// 哺乳動物
class Mammal extends Animal{
  constructor(age, furColor) {
    super(age);
    this.furColor = furColor;
  };
  liveBirth() {};
}

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

鏈式呼叫

這種模式相當有用,可以在很多庫中發現它的身影,比如 jQuery、Lodash 等。它讓你的程式碼簡潔優雅。實現起來也非常簡單,在類的方法最後返回 this 可以了。

Bad:

class Car {
  constructor(make, model, color) {
    this.make = make;
    this.model = model;
    this.color = color;
  }

  setMake(make) {
    this.make = make;
  }

  setModel(model) {
    this.model = model;
  }

  setColor(color) {
    this.color = color;
  }

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

const car = new Car(`Ford`,`F-150`,`red`);
car.setColor(`pink`);
car.save();

Good

class Car {
  constructor(make, model, color) {
    this.make = make;
    this.model = model;
    this.color = color;
  }

  setMake(make) {
    this.make = make;
    return this;
  }

  setModel(model) {
    this.model = model;
    return this;
  }

  setColor(color) {
    this.color = color;
    return this;
  }

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

const car = new Car(`Ford`,`F-150`,`red`)
  .setColor(`pink`);
  .save();

不要濫用繼承

很多時候繼承被濫用,導致可讀性很差,要搞清楚兩個類之間的關係,繼承表達的一個屬於關係,而不是包含關係,比如 Human->Animal vs. User->UserDetails

Bad:

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

  // ...
}

// TaxData(稅收資訊)並不是屬於 Employee(僱員),而是包含關係。
class EmployeeTaxData extends Employee {
  constructor(ssn, salary) {
    super();
    this.ssn = ssn;
    this.salary = salary;
  }

  // ...
}

Good

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

  // ...
}

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

  setTaxData(ssn, salary) {
    this.taxData = new EmployeeTaxData(ssn, salary);
  }
  // ...
}

SOLID

SOLID 是幾個單詞首字母組合而來,分別表示 單一功能原則開閉原則里氏替換原則介面隔離原則以及依賴反轉原則

單一功能原則

如果一個類乾的事情太多太雜,會導致後期很難維護。我們應該釐清職責,各司其職減少相互之間依賴。

Bad:

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

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

  verifyCredentials() {
    // ...
  }
}

Good

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

class UserSetting {
  constructor(user) {
    this.user = user;
    this.auth = new UserAuth(this.user);
  }
  changeSettings(settings) {
    if (this.auth.verifyCredentials()) {
      // ...
    }
  }
}
}

開閉原則

“開”指的就是類、模組、函式都應該具有可擴充套件性,“閉”指的是它們不應該被修改。也就是說你可以新增功能但不能去修改原始碼。

Bad:

class AjaxAdapter extends Adapter {
  constructor() {
    super();
    this.name = `ajaxAdapter`;
  }
}

class NodeAdapter extends Adapter {
  constructor() {
    super();
    this.name = `nodeAdapter`;
  }
}

class HttpRequester {
  constructor(adapter) {
    this.adapter = adapter;
  }

  fetch(url) {
    if (this.adapter.name === `ajaxAdapter`) {
      return makeAjaxCall(url).then((response) => {
        // 傳遞 response 並 return
      });
    } else if (this.adapter.name === `httpNodeAdapter`) {
      return makeHttpCall(url).then((response) => {
        // 傳遞 response 並 return
      });
    }
  }
}

function makeAjaxCall(url) {
  // 處理 request 並 return promise
}

function makeHttpCall(url) {
  // 處理 request 並 return promise
}

Good

class AjaxAdapter extends Adapter {
  constructor() {
    super();
    this.name = `ajaxAdapter`;
  }

  request(url) {
    // 處理 request 並 return promise
  }
}

class NodeAdapter extends Adapter {
  constructor() {
    super();
    this.name = `nodeAdapter`;
  }

  request(url) {
    // 處理 request 並 return promise
  }
}

class HttpRequester {
  constructor(adapter) {
    this.adapter = adapter;
  }

  fetch(url) {
    return this.adapter.request(url).then((response) => {
      // 傳遞 response 並 return
    });
  }
}

里氏替換原則

名字很唬人,其實道理很簡單,就是子類不要去重寫父類的方法。

Bad:

// 長方形
class Rectangle {
  constructor() {
    this.width = 0;
    this.height = 0;
  }

  setColor(color) {
    // ...
  }

  render(area) {
    // ...
  }

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

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

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

// 正方形
class Square extends Rectangle {
  setWidth(width) {
    this.width = width;
    this.height = width;
  }

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

function renderLargeRectangles(rectangles) {
  rectangles.forEach((rectangle) => {
    rectangle.setWidth(4);
    rectangle.setHeight(5);
    const area = rectangle.getArea(); 
    rectangle.render(area);
  });
}

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

Good

class Shape {
  setColor(color) {
    // ...
  }

  render(area) {
    // ...
  }
}

class Rectangle extends Shape {
  constructor(width, height) {
    super();
    this.width = width;
    this.height = height;
  }

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

class Square extends Shape {
  constructor(length) {
    super();
    this.length = length;
  }

  getArea() {
    return this.length * this.length;
  }
}

function renderLargeShapes(shapes) {
  shapes.forEach((shape) => {
    const area = shape.getArea();
    shape.render(area);
  });
}

const shapes = [new Rectangle(4, 5), new Rectangle(4, 5), new Square(5)];
renderLargeShapes(shapes);

介面隔離原則

JavaScript 幾乎沒有介面的概念,所以這條原則很少被使用。官方定義是“客戶端不應該依賴它不需要的介面”,也就是介面最小化,把介面解耦。

Bad:

class DOMTraverser {
  constructor(settings) {
    this.settings = settings;
    this.setup();
  }

  setup() {
    this.rootNode = this.settings.rootNode;
    this.animationModule.setup();
  }

  traverse() {
    // ...
  }
}

const $ = new DOMTraverser({
  rootNode: document.getElementsByTagName(`body`),
  animationModule() {} // Most of the time, we won`t need to animate when traversing.
  // ...
});

Good

class DOMTraverser {
  constructor(settings) {
    this.settings = settings;
    this.options = settings.options;
    this.setup();
  }

  setup() {
    this.rootNode = this.settings.rootNode;
    this.setupOptions();
  }

  setupOptions() {
    if (this.options.animationModule) {
      // ...
    }
  }

  traverse() {
    // ...
  }
}

const $ = new DOMTraverser({
  rootNode: document.getElementsByTagName(`body`),
  options: {
    animationModule() {}
  }
});

依賴反轉原則

說就兩點:

  1. 高層次模組不能依賴低層次模組,它們依賴於抽象介面。
  2. 抽象介面不能依賴具體實現,具體實現依賴抽象介面。

總結下來就兩個字,解耦。

Bad:

// 庫存查詢
class InventoryRequester {
  constructor() {
    this.REQ_METHODS = [`HTTP`];
  }

  requestItem(item) {
    // ...
  }
}

// 庫存跟蹤
class InventoryTracker {
  constructor(items) {
    this.items = items;

    // 這裡依賴一個特殊的請求類,其實我們只是需要一個請求方法。
    this.requester = new InventoryRequester();
  }

  requestItems() {
    this.items.forEach((item) => {
      this.requester.requestItem(item);
    });
  }
}

const inventoryTracker = new InventoryTracker([`apples`, `bananas`]);
inventoryTracker.requestItems();

Good

// 庫存跟蹤
class InventoryTracker {
  constructor(items, requester) {
    this.items = items;
    this.requester = requester;
  }

  requestItems() {
    this.items.forEach((item) => {
      this.requester.requestItem(item);
    });
  }
}

// HTTP 請求
class InventoryRequesterHTTP {
  constructor() {
    this.REQ_METHODS = [`HTTP`];
  }

  requestItem(item) {
    // ...
  }
}

// webSocket 請求
class InventoryRequesterWS {
  constructor() {
    this.REQ_METHODS = [`WS`];
  }

  requestItem(item) {
    // ...
  }
}

// 通過依賴注入的方式將請求模組解耦,這樣我們就可以很輕易的替換成 webSocket 請求。
const inventoryTracker = new InventoryTracker([`apples`, `bananas`], new InventoryRequesterHTTP());
inventoryTracker.requestItems();

測試

隨著專案變得越來越龐大,時間線拉長,有的老程式碼可能半年都沒碰過,如果此時上線,你有信心這部分程式碼能正常工作嗎?測試的覆蓋率和你的信心是成正比的。

PS: 如果你發現你的程式碼很難被測試,那麼你應該優化你的程式碼了。

單一化

Bad:

import assert from `assert`;

describe(`MakeMomentJSGreatAgain`, () => {
  it(`handles date boundaries`, () => {
    let date;

    date = new MakeMomentJSGreatAgain(`1/1/2015`);
    date.addDays(30);
    assert.equal(`1/31/2015`, date);

    date = new MakeMomentJSGreatAgain(`2/1/2016`);
    date.addDays(28);
    assert.equal(`02/29/2016`, date);

    date = new MakeMomentJSGreatAgain(`2/1/2015`);
    date.addDays(28);
    assert.equal(`03/01/2015`, date);
  });
});

Good

import assert from `assert`;

describe(`MakeMomentJSGreatAgain`, () => {
  it(`handles 30-day months`, () => {
    const date = new MakeMomentJSGreatAgain(`1/1/2015`);
    date.addDays(30);
    assert.equal(`1/31/2015`, date);
  });

  it(`handles leap year`, () => {
    const date = new MakeMomentJSGreatAgain(`2/1/2016`);
    date.addDays(28);
    assert.equal(`02/29/2016`, date);
  });

  it(`handles non-leap year`, () => {
    const date = new MakeMomentJSGreatAgain(`2/1/2015`);
    date.addDays(28);
    assert.equal(`03/01/2015`, date);
  });
});

非同步

不再使用回撥

不會有人願意去看巢狀回撥的程式碼,用 Promises 替代回撥吧。

Bad:

import { get } from `request`;
import { writeFile } from `fs`;

get(`https://en.wikipedia.org/wiki/Robert_Cecil_Martin`, (requestErr, response) => {
  if (requestErr) {
    console.error(requestErr);
  } else {
    writeFile(`article.html`, response.body, (writeErr) => {
      if (writeErr) {
        console.error(writeErr);
      } else {
        console.log(`File written`);
      }
    });
  }
});

Good

get(`https://en.wikipedia.org/wiki/Robert_Cecil_Martin`)
  .then((response) => {
    return writeFile(`article.html`, response);
  })
  .then(() => {
    console.log(`File written`);
  })
  .catch((err) => {
    console.error(err);
  });

Async/Await 比起 Promises 更簡潔

Bad:

import { get } from `request-promise`;
import { writeFile } from `fs-promise`;

get(`https://en.wikipedia.org/wiki/Robert_Cecil_Martin`)
  .then((response) => {
    return writeFile(`article.html`, response);
  })
  .then(() => {
    console.log(`File written`);
  })
  .catch((err) => {
    console.error(err);
  });

Good

import { get } from `request-promise`;
import { writeFile } from `fs-promise`;

async function getCleanCodeArticle() {
  try {
    const response = await get(`https://en.wikipedia.org/wiki/Robert_Cecil_Martin`);
    await writeFile(`article.html`, response);
    console.log(`File written`);
  } catch(err) {
    console.error(err);
  }
}

錯誤處理

不要忽略拋異常

Bad:

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

Good

try {
  functionThatMightThrow();
} catch (error) {
  // 這一種選擇,比起 console.log 更直觀
  console.error(error);
  // 也可以在介面上提醒使用者
  notifyUserOfError(error);
  // 也可以把異常傳回伺服器
  reportErrorToService(error);
  // 其他的自定義方法
}

不要忘了在 Promises 拋異常

Bad:

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

Good

getdata()
  .then((data) => {
    functionThatMightThrow(data);
  })
  .catch((error) => {
    // 這一種選擇,比起 console.log 更直觀
    console.error(error);
    // 也可以在介面上提醒使用者
    notifyUserOfError(error);
    // 也可以把異常傳回伺服器
    reportErrorToService(error);
    // 其他的自定義方法
  });

程式碼風格

程式碼風格是主觀的,爭論哪種好哪種不好是在浪費生命。市面上有很多自動處理程式碼風格的工具,選一個喜歡就行了,我們來討論幾個非自動處理的部分。

常量大寫

Bad:

const DAYS_IN_WEEK = 7;
const daysInMonth = 30;

const songs = [`Back In Black`, `Stairway to Heaven`, `Hey Jude`];
const Artists = [`ACDC`, `Led Zeppelin`, `The Beatles`];

function eraseDatabase() {}
function restore_database() {}

class animal {}
class Alpaca {}

Good

const DAYS_IN_WEEK = 7;
const DAYS_IN_MONTH = 30;

const SONGS = [`Back In Black`, `Stairway to Heaven`, `Hey Jude`];
const ARTISTS = [`ACDC`, `Led Zeppelin`, `The Beatles`];

function eraseDatabase() {}
function restoreDatabase() {}

class Animal {}
class Alpaca {}

先宣告後呼叫

就像我們看報紙文章一樣,從上到下看,所以為了方便閱讀把函式宣告寫在函式呼叫前面。

Bad:

class PerformanceReview {
  constructor(employee) {
    this.employee = employee;
  }

  lookupPeers() {
    return db.lookup(this.employee, `peers`);
  }

  lookupManager() {
    return db.lookup(this.employee, `manager`);
  }

  getPeerReviews() {
    const peers = this.lookupPeers();
    // ...
  }

  perfReview() {
    this.getPeerReviews();
    this.getManagerReview();
    this.getSelfReview();
  }

  getManagerReview() {
    const manager = this.lookupManager();
  }

  getSelfReview() {
    // ...
  }
}

const review = new PerformanceReview(employee);
review.perfReview();

Good

class PerformanceReview {
  constructor(employee) {
    this.employee = employee;
  }

  perfReview() {
    this.getPeerReviews();
    this.getManagerReview();
    this.getSelfReview();
  }

  getPeerReviews() {
    const peers = this.lookupPeers();
    // ...
  }

  lookupPeers() {
    return db.lookup(this.employee, `peers`);
  }

  getManagerReview() {
    const manager = this.lookupManager();
  }

  lookupManager() {
    return db.lookup(this.employee, `manager`);
  }

  getSelfReview() {
    // ...
  }
}

const review = new PerformanceReview(employee);
review.perfReview();

註釋

只有業務邏輯需要註釋

程式碼註釋不是越多越好。

Bad:

function hashIt(data) {
  // 這是初始值
  let hash = 0;

  // 陣列的長度
  const length = data.length;

  // 迴圈陣列
  for (let i = 0; i < length; i++) {
    // 獲取字元程式碼
    const char = data.charCodeAt(i);
    // 修改 hash
    hash = ((hash << 5) - hash) + char;
    // 轉換為32位整數
    hash &= hash;
  }
}

Good

function hashIt(data) {
  let hash = 0;
  const length = data.length;

  for (let i = 0; i < length; i++) {
    const char = data.charCodeAt(i);
    hash = ((hash << 5) - hash) + char;

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

刪掉註釋的程式碼

git 存在的意義就是儲存你的舊程式碼,所以註釋的程式碼趕緊刪掉吧。

Bad:

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

Good

doStuff();

不要記日記

記住你有 git!,git log 可以幫你幹這事。

Bad:

/**
 * 2016-12-20: 刪除了 xxx
 * 2016-10-01: 改進了 xxx
 * 2016-02-03: 刪除了第12行的型別檢查
 * 2015-03-14: 增加了一個合併的方法
 */
function combine(a, b) {
  return a + b;
}

Good

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

註釋不需要高亮

註釋高亮,並不能起到提示的作用,反而會干擾你閱讀程式碼。

Bad:

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

////////////////////////////////////////////////////////////////////////////////
// Action setup
////////////////////////////////////////////////////////////////////////////////
const actions = function() {
  // ...
};

Good

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

const actions = function() {
  // ...
};

ref

翻譯自ryanmcdermott《clean-code-javascript》,本文對原文進行了一些修改。


相關文章