次篇文章是轉載 此篇文章

無瑕的程式碼 JavaScript

原作: https://github.com/ryanmcdermott/clean-code-javascript
原作者: https://github.com/ryanmcdermott
譯者: https://github.com/trylovetom

介紹(Introduction)

透過計算閱讀程式碼時的咒罵次數,來評估軟體品質
透過計算閱讀程式碼時的咒罵次數,來評估軟體品質

文章作者根據 Robert C. Martin 的《無暇的程式碼》,撰寫一份適用於 JavaScript 的原則。本文不是風格指南(Style Guide),而是教導你撰寫出可閱讀、可重複使用與可重構的 JS 程式碼。

注意!你不必嚴格遵守每一項原則,有些甚至不被大眾所認同。雖然這只是份指南,卻是來自《無暇的程式碼》作者的多年結晶。

軟體工程只發展了五十年,仍然有很多地方值得去探討。當軟體與建築一樣古老時,也許會有一些墨守成規的原則。但現在,先讓這份指南當試金石,作為你和團隊的 JS 程式碼標準。

還有一件事情:知道這些原則,並不會立刻讓你成為出色的開發者,長期奉行它們,不代表你能高枕無憂不再犯錯。但是,千里之行,始於足下,時常與志同道合們進行討論(Code Review),改善不完備之處。不要因為自己寫出來的程式碼很糟糕而害怕分享,而是要畏懼自己居然寫出了這樣的程式碼!

譯者序

獻給傲嬌文創的所有工程師夥伴,與熱愛 JavaScript 的各位。

《無暇的程式碼》是一本好書,是不可否認這個事實。我熱愛著 JS,當找到這份 JS 版本的後,我無比開心立刻著手翻譯,因此有了這份《無暇的程式碼 JavaScript》。對於 Clean Code 的書名採用博碩文化的翻譯,也是對原版譯者的尊敬。本翻譯中會穿插一些我對文章中註解,也請讀者原諒我的叨擾。另外專業術語的翻譯有可能會有出入,我會標示出英文原文,避免讀者誤解。如有翻譯或是理解上的錯誤,煩請聯絡我,謝謝。(聯絡方式在上方,我的 GitHub 中。)

變數(Variables)

使用具有意義且可閱讀的名稱

糟糕的:

1
const yyyymmdstr = moment().format('YYYY/MM/DD')

適當的:

1
const currentDate = moment().format('YYYY/MM/DD')

⬆ 回到目錄

相同類型的變數使用相同的名稱

糟糕的:

1
2
3
getUserInfo()
getClientData()
getCustomerRecord()

適當的:

1
getUser()

⬆ 回到目錄

使用可搜尋的名稱

使用易於閱讀與搜尋的名稱非常重要,因為我們要閱讀的程式碼遠比自己寫得多。使用沒有意義的名稱,會導致程式碼難以理解,對後續閱讀者是個糟糕的體驗。另外使用以下工具,可以協助你找出未命名的常數:

糟糕的:

1
2
// 86400000 代表什麼意義?
setTimeout(blastOff, 86400000)

適當的:

1
2
3
4
// 宣告(Declare)一個有意義的常數(constants)
const MILLISECONDS_IN_A_DAY = 86400000

setTimeout(blastOff, MILLISECONDS_IN_A_DAY)

⬆ 回到目錄

使用可解釋的變數

糟糕的:

1
2
3
4
5
6
const address = 'One Infinite Loop, Cupertino 95014'
const cityZipCodeRegex = /^[^,\\]+[,\\\s]+(.+?)\s*(\d{5})?$/
saveCityZipCode(
  address.match(cityZipCodeRegex)[1],
  address.match(cityZipCodeRegex)[2]
)

適當的:

1
2
3
4
const address = 'One Infinite Loop, Cupertino 95014'
const cityZipCodeRegex = /^[^,\\]+[,\\\s]+(.+?)\s*(\d{5})?$/
const [, city, zipCode] = address.match(cityZipCodeRegex) || []
saveCityZipCode(city, zipCode)

譯者附註

address.match(cityZipCodeRegex) 取出了字串中的 city 與 zipCode 並以陣列(Array)的方式輸出。在糟糕的範例中,你不會知道哪個是 city,哪個是 zipCode。在適當的範例中,則清楚地解釋了。

⬆ 回到目錄

避免心理作用(Mental Mapping)

清晰(Explicit)的表達會比隱藏(Implicit)更好。

糟糕的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
const locations = ['Austin', 'New York', 'San Francisco']
locations.forEach((l) => {
  doStuff()
  doSomeOtherStuff()
  // ...
  // ...
  // ...
  // 等等,這 `l` 是…?
  dispatch(l)
})

適當的:

1
2
3
4
5
6
7
8
9
const locations = ['Austin', 'New York', 'San Francisco']
locations.forEach((location) => {
  doStuff()
  doSomeOtherStuff()
  // ...
  // ...
  // ...
  dispatch(location)
})

譯者附註

在糟糕的範例中,程式碼的作者認為從 locations 取出的都是地址,所以選用縮減後的 l 作為名稱。不過這只有作者自己這麼認為,其他人可不一定知道。避免「我認為」、「我以為」、「我覺得」,這樣的心理作用。

⬆ 回到目錄

避免使用不必要的描述(Context)

如果你的類別與物件名稱是有關聯意義的,就不用在內部變數上再次重複。

糟糕的:

1
2
3
4
5
6
7
8
9
const Car = {
  carMake: 'Honda',
  carModel: 'Accord',
  carColor: 'Blue',
}

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

適當的:

1
2
3
4
5
6
7
8
9
const Car = {
  make: 'Honda',
  model: 'Accord',
  color: 'Blue',
}

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

⬆ 回到目錄

使用預設參數(Parameter)代替條件判斷(Conditionals)

使用預設參數較整潔,但請注意,當參數為 undefined 時才會作用,其他種類的虛值(Falsy)不會,像是 ''falsenull0NaN 等。

糟糕的:

1
2
3
4
function createMicrobrewery(name) {
  const breweryName = name || 'Hipster Brew Co.'
  // ...
}

適當的:

1
2
3
function createMicrobrewery(name = 'Hipster Brew Co.') {
  // ...
}

譯者附註

預設參數非常好用,可以結合工廠模式做出很多應用。另外建議統一使用 undefined 代替 null 當作空值的回傳值。

此處原文為 Arguments,但是函數的參數定義應該為 Parameter,而呼叫函數時傳遞的引數才是 Arguments

⬆ 回到目錄

函數(Functions)

參數(Parameter) (少於 2 個較佳)

限制函數的參數數量非常重要,因為能讓你更容易地測試。過多的參數代表著過多的組合,會導致你不得不編寫出大量測試。

一個至二個是最理想的,盡可能避免大於三個以上。如果你有超過兩個以上的參數,代表你的函數做太多事情。如果無法避免時,可以有效地使用物件替代大量的參數。

為了讓你可以清晰地表達,預期使用哪些物件的屬性(Properties),可以使用 ES2015/ES6 提供的解構(Destructuring)語法。使用這種語法有以下優點:

  1. 函數需要物件的哪些屬性,可以像參數一樣清晰地表達。
  2. 解構語法會複製來自物件的原始型態(Primitive),這能幫助你避免邊際效應(Side Effect)。注意!巢狀物件與陣列,並不會被解構語法複製。
  3. 使用解構語法能讓物件屬性被程式碼檢查器(Linter)作用,提醒你哪些屬性未被使用到。

糟糕的:

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

適當的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
function createMenu({ title, body, buttonText, cancellable }) {
  // ...
}

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

譯者附註

這種方法非常適合用於工廠模式,結合上一章的預設參數,譯者推薦的使用方式如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
function createMenu({
  title = 'Default Title', // 傳遞的物件不齊全,使用預設屬性
  body = '',
  buttonText = 'My Button',
  cancellable = true,
} = {}) {
  // 如未傳遞任何參數使用預設空物件,使用 `= {}` 可避免 TypeError: Cannot destructure property `...` of 'undefined' or 'null'.
  return {
    title,
    body,
    buttonText,
    cancellable,
  }
}

const myMenu = createMenu({
  title: 'Foo',
  body: 'Bar',
  buttonText: 'Baz',
  cancellable: true,
})

想了解更多工廠模式與 ES6 的結合,可參考連結文章:JavaScript Factory Functions with ES6+

⬆ 回到目錄

一個函數只做一件事情(單一性)

這是個非常重要的原則,當你的函數做超過一件事情時,它會更難以被撰寫、測試與理解。當你隔離(Isolate)你的函數到只做一件事情時,它能更容易地被重構(Refactor)與清晰地閱讀。如果嚴格遵守此項原則,你將會領先許多開發者。

糟糕的:

1
2
3
4
5
6
7
8
function emailClients(clients) {
  clients.forEach((client) => {
    const clientRecord = database.lookup(client)
    if (clientRecord.isActive()) {
      email(client)
    }
  })
}

適當的:

1
2
3
4
5
6
7
8
function emailActiveClients(clients) {
  clients.filter(isActiveClient).forEach(email)
}

function isActiveClient(client) {
  const clientRecord = database.lookup(client)
  return clientRecord.isActive()
}

⬆ 回到目錄

函數名稱應該說明它做的內容

糟糕的:

1
2
3
4
5
6
7
8
function addToDate(date, month) {
  // ...
}

const date = new Date()

// 難以從函數名稱看出到底加入了什麼
addToDate(date, 1)

適當的:

1
2
3
4
5
6
function addMonthToDate(month, date) {
  // ...
}

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

譯者附註

建議函數命名以動詞開頭,像是 doSomething()setupUserProfile()

⬆ 回到目錄

函數應該只做一層抽象(Abstraction)

當你的函數需要的抽象多於一層時,代表你的函數做太多事情了。將其分解以利重用與測試。

糟糕的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
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...
  })
}

適當的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
function parseBetterJSAlternative(code) {
  const tokens = tokenize(code)
  const syntaxTree = parse(tokens)
  syntaxTree.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 parse(tokens) {
  const syntaxTree = []
  tokens.forEach((token) => {
    syntaxTree.push(/* ... */)
  })

  return syntaxTree
}

譯者附註

這個原則與前面提到的「一個函數只做一件事情(單一性)」概念相似。

⬆ 回到目錄

移除重複的(Duplicate)程式碼

絕對避免重複的程式碼,重複的程式碼代表者更動邏輯時,需要同時修改多處。

想像一下你經營著一家餐廳,需要持續追蹤存貨:番茄、洋蔥、大蒜與各種香料等。如果你有多份紀錄表,當使用蕃茄做完一道料理時,需要更新多份記錄表,可能會忘記更新其中一份。如果你只有一份記錄表,就不會有此問題!

通常重複的程式碼,是因為有兩個稍微不同的東西。它們之間絕大部分相同,但些微不同之處,迫使你使用多個函數處理相似的事情。如果出現重複的程式碼,可以使用函數、模組或是類別來抽象化處理。

正確的抽象化是非常關鍵的,這也是為什麼你應該遵循類別(Classes)章節中,物件導向基本原則 SOLID 的原因。請小心,較差的抽象化會比重複的程式碼更糟!這麼說吧,如果你有把握做出好的抽象化,盡情放手去做。別讓程式碼出現重複的地方,不然你會需要修改更多的程式碼。

糟糕的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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)
  })
}

適當的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
function showEmployeeList(employees) {
  employees.forEach((employee) => {
    const expectedSalary = employee.calculateExpectedSalary()
    const experience = employee.getExperience()

    const data = {
      expectedSalary,
      experience,
    }

    switch (employee.type) {
      case 'manager':
        data.portfolio = employee.getMBAProjects()
        break
      case 'developer':
        data.githubLink = employee.getGithubLink()
        break
    }

    render(data)
  })
}

譯者附註

剛讀完這個原則時,我非常遵守,但是個人龜毛的個性,造成了不少麻煩,我會在開發時不斷的思考是否會出現了重複的程式碼,甚至考慮到了之後的重用性。代價就是過度設計(Over Engineering)造成功能開發窒礙難行,設計了良好架構,但實際上並未被使用到。最後總結了一個建議作為附加原則:一開始撰寫程式碼先以功能開發優先,當你發現有兩個以上的地方重複時,再來考慮要不要重構。

⬆ 回到目錄

使用 Object.assign 設定 Object 的預設值

糟糕的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
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)

適當的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const menuConfig = {
  title: 'Order',
  // 使用者漏掉了 'body'
  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)作為參數

當你的函數使用了旗標當作參數時,代表函數做不只一件事情,依照不同旗標路徑切分你的函數。

糟糕的:

1
2
3
4
5
6
7
function createFile(name, temp) {
  if (temp) {
    fs.create(`./temp/${name}`)
  } else {
    fs.create(name)
  }
}

適當的:

1
2
3
4
5
6
7
function createFile(name) {
  fs.create(name)
}

function createTempFile(name) {
  createFile(`./temp/${name}`)
}

⬆ 回到目錄

避免副作用(Side Effects)

當函數作用在除了回傳值外的地方,像是讀寫文件、修改全域變數或是將你的錢轉帳到其他人帳戶,則稱為副作用。

程式在某些情況下是需要副作用的,像是上面所提到的例子。這時你應該將這些功能集中在一起,不要同時有多個函數或是類別同時操作資源,應該只用一個服務(Service)完成這些事情。

常見的問題像是:

  • 在沒有任何架構下,同時多個物件中分享共有狀態。
  • 可變的狀態,且可以被任何人寫入
  • 副作用發生的地方沒有被集中。

如果你能避免這些問題,你會比大多數的工程師快樂。

糟糕的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// 全域變數被以下函數使用
// 假如有其他的函數使用了這個名稱,現在他變成了陣列,將會被破壞而出錯。
let name = 'Ryan McDermott'

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

splitIntoFirstAndLastName()

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

適當的:

1
2
3
4
5
6
7
8
9
function splitIntoFirstAndLastName(name) {
  return name.split(' ')
}

const name = 'Ryan McDermott'
const newName = splitIntoFirstAndLastName(name)

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

⬆ 回到目錄

避免副作用(Side Effects)第二部分

在 JavaScript 中,原始資料類型傳遞數值(Value),物件/陣列傳遞參照(Reference)。在本案例中,你的函數改變了購物車清單 cart 中的陣列,像是你增加了一個商品,其他使用購物車清單的函數將會被影響。這做法有好有壞,讓我解釋一下問題所在:

使用者按下付款按鈕後,將會呼叫 purchase 函數,產生一個網路請求傳送購物車清單陣列到伺服器。因為較差的網路連線,必須多嘗試幾次。此時使用者不小心又按下加入購物車按鈕,因為是參考的關係,請求將會送出新加入的商品。

較好的解決辦法是 addItemToCart 函數,執行前複製新的一份購物車清單,修改複製的資料後再回傳。這能確保其他的函數的購物車清單沒有任何機會被被參考所影響。

使用這方法前,有兩個警告要告知:

  1. 當採用這種做法後,你會發現,需要修改輸入物件的情況非常少。大多數的程式碼可以在沒有副作用的情況下重構!

  2. 複製大型物件,需要花費高昂的效能。幸好,我們有好的函數庫,可以提升複製物件與陣列的速度與減少記憶體使用。

糟糕的:

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

適當的:

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

譯者附註

譯者這邊另外再提供一個案例,函數 checkIs18Age 用來檢查是否成年。第一個寫法中,引用了全域(global)變數 minimum,這功能看起來能正常運作沒問題,但是今天如果有其他函數 setMinAge 修改了全域變數 minimum,函數 checkIs18Age 將會因為副作用的關係變的無法預期,甚至失去它的作用。

較好的寫法是,採用純函數(pure function),使用一個可預期的變數,來避免副作用影響。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
let minimum = 18

// impure with side effect
const checkIs18Age = (age) => age >= minimum
const setMinAge = (age) => (minimum = age)

// pure
const checkIs18Age = (age) => {
  let minimum = 18
  return age >= minimum
}

另外使用解構的方式複製資料,只會複製第一層而已。

1
2
3
4
5
6
// 巢狀解構只能複製第一層
const obj1 = { subObj: { message: 'Hey' } }
const obj2 = { ...obj1 }

obj2.subObj.message = 'Yo'
console.log(obj1.subObj.message) // 'Yo'

如果要複製巢狀結構,你需要深度複製。可以使用一些函數庫 loadsh_.CloneDeep 或是 ramdaclone。或是使用 JSON.parse(JSON.stringify(object)) 來實現。不過使用 JSON 的話,會失去 Function 的複製。

1
2
3
4
5
6
// 使用 JSON 來深度複製
const obj1 = { subObj: { message: 'Hey' } }
const obj2 = JSON.parse(JSON.stringify(obj1))

obj2.subObj.message = 'Yo'
console.log(obj1.subObj.message) // 'Hey'

⬆ 回到目錄

別寫全域函數(Global Function)

在 JavaScript 中弄髒全域是個不好的做法,因為你可能會影響到其他函數庫或是 API。舉個例子,如果妳想要在 JavaScript 的原生陣列方法,擴展 diff 方法,用 B 陣列來去除 A 陣列中的元素(Element)。常見做法你可能會在 Array.prototype 中增加一個全新的函數,如果其他函數庫也有自己的 diff 實現的話將會互相影響。這就是為什麼我們需要使用 ES2015/ES6 的類別,來擴展的原因。

糟糕的:

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

適當的:

1
2
3
4
5
6
class SuperArray extends Array {
  diff(comparisonArray) {
    const hash = new Set(comparisonArray)
    return this.filter((elem) => !hash.has(elem))
  }
}

⬆ 回到目錄

偏好使用函數式程式(Functional Programming)設計代替命令式程式設計(Imperative Programming)

JavaScript 不是像 Haskell 一樣的函數式語言,但它具有類似特性。函數式程式設計更加乾淨且容易被測試。當你在寫程式時,盡量選擇此設計方式。

糟糕的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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
}

適當的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const programmerOutput = [
  {
    name: 'Uncle Bobby',
    linesOfCode: 500,
  },
  {
    name: 'Suzie Q',
    linesOfCode: 1500,
  },
  {
    name: 'Jimmy Gosling',
    linesOfCode: 150,
  },
  {
    name: 'Gracie Hopper',
    linesOfCode: 1000,
  },
]

const totalOutput = programmerOutput.reduce(
  (totalLines, output) => totalLines + output.linesOfCode,
  0
)

⬆ 回到目錄

封裝狀態(Encapsulate Conditionals)

糟糕的:

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

適當的:

1
2
3
4
5
6
7
function shouldShowSpinner(fsm, listNode) {
  return fsm.state === 'fetching' && isEmpty(listNode)
}

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

⬆ 回到目錄

避免負面狀態(Negative Conditionals)

糟糕的:

1
2
3
4
5
6
7
function isDOMNodeNotPresent(node) {
  // ...
}

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

適當的:

1
2
3
4
5
6
7
function isDOMNodePresent(node) {
  *// ...*
}

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

⬆ 回到目錄

避免狀態

當你第一次聽到時,這聽起來是不可能的任務。大部分的人會說:「怎麼可能不使用 if 語法?」事實上你可以使用多態性(Polymorphism) 達到相同的效果。第二個問題來了,「為什麼我們需要這樣做呢?」依據前面概念,為了保持程式碼的乾淨,當類別或是函數出現 if 語法,代表你的函數做了超過一件事情。記住,一個函數只做一件事情!

糟糕的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
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()
    }
  }
}

適當的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Airplane {
  // ...
}

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

⬆ 回到目錄

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

JavaScript 為弱型別語言,代表函數應能處理任何型別的引數(argument)。有時這會帶給你一些麻煩,讓你需要做型別檢查。有很多方法可以避免這種問題發生,第一種就是統一所有的 API。

糟糕的:

1
2
3
4
5
6
7
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'))
  }
}

適當的:

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

譯者附註

此範例統一了所有的車輛移動的參數、方法與實作,所以不再需要區分不同的類別的車輛呼叫不同的方法。

⬆ 回到目錄

避免型別檢查:第二部分

假設你需要型別檢查原始數值,像是字串與整數且你無法使用多態性處理,考慮使用 TypeScript 吧。他是提供標準 JavaScript 靜態類型的的最佳選擇。手動型別檢查需要很多額外處理,你得到的是虛假的型別安全,且失去的可讀性。保持你的 JavaScript 程式碼的整潔、寫好測試與足夠的程式碼審查(Code Review)。如果加上使用 TypeScript 會是更好的選擇。

糟糕的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
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')
}

適當的:

1
2
3
function combine(val1, val2) {
  return val1 + val2
}

⬆ 回到目錄

別過度優化

現代瀏覽器在運行時幫你做了很多優化。大多數的情況,你所做的優化都是在浪費你的時間。這裏有些很好的資源,去了解哪些優化是無用的。

糟糕的:

1
2
3
4
5
// 在舊的瀏覽器中並不會快取(cache)`list.length`,每次迭代(iteration)時的重新計算相當損耗效能。
// 這在新瀏覽器中已被優化,你不用手動去快取。
for (let i = 0, len = list.length; i < len; i++) {
  // ...
}

適當的:

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

譯者附註

簡單來說,你不用在意程式語言層面上優化,因為這部分會因為版本更新而得到優化。但不要因此放棄所有的優化,演算法的優化才是你該注意的地方!

⬆ 回到目錄

移除無用的程式碼(Dead Code)

沒有任何理由保留無用的程式碼,如果他們沒有被使用到,移除它!讓它們被保留在版本歷史中。

糟糕的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
function oldRequestModule(url) {
  // ...
}

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

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

適當的:

1
2
3
4
5
6
function newRequestModule(url) {
  // ...
}

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

⬆ 回到目錄

物件(Objects)與資料結構(Data Structures)

使用 getters 與 setters

使用 getterssetters 來存取物件中資料,會比單純使用屬性(property)來的好。因為:

  • 當你想要在取得物件屬性時做更多事情,你不用找出所有的程式碼修改。
  • 透過 set 可以建立規則進行資料校驗。
  • 封裝內部邏輯。
  • 存取時增加日誌(logging)與錯誤處理(error handling)。
  • 你可以延遲載入你的物件屬性,像是來自伺服器的資料。

糟糕的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
function makeBankAccount() {
  // ...

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

const account = makeBankAccount()
account.balance = 100

適當的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function makeBankAccount() {
  // 私有變數
  let balance = 0

  // 'getter',經由下方的返回物件對外公開
  function getBalance() {
    return balance
  }

  // 'setter',經由下方的返回物件對外公開
  function setBalance(amount) {
    // ... 更新前先進行驗證
    balance = amount
  }

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

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

⬆ 回到目錄

讓物件擁有私有成員(members)

可以透過閉包(closures)來私有化參數(使用於 ES5 以下)。

糟糕的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
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

適當的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
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

⬆ 回到目錄

類別(Classes)

類別語法偏好使用 ES2015/ES6 的類別更甚於 ES5 函數

ES5 的類別定義非常難以閱讀、繼承、建造與定義方法。假設你需要繼承(請注意你有可能不需要),偏好使用 ES2015/ES6 的類別。除非你需要大型且複雜的物件,不然使用小型函數會比類別更好。

糟糕的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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() {}

適當的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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() {
    // ...
  }
}

⬆ 回到目錄

使用方法鏈(method chaining)

這個模式(pattern)在 JavaScript 中非常有用,你可以在很多函數庫中看到,像是 jQuery 與 Lodash。它可以讓你的程式碼表達的更好。

基於這個原因,方法鏈可以讓你的程式碼看起來更加簡潔。在你的類別函數中,只需要回傳 this 在每一個函數中,你就可以鏈結所有類別中的方法。

糟糕的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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()

適當的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class Car {
  constructor(make, model, color) {
    this.make = make
    this.model = model
    this.color = color
  }

  setMake(make) {
    this.make = make
    // 注意:回傳 this 以鏈結
    return this
  }

  setModel(model) {
    this.model = model
    // 注意:回傳 this 以鏈結
    return this
  }

  setColor(color) {
    this.color = color
    // 注意:回傳 this 以鏈結
    return this
  }

  save() {
    console.log(this.make, this.model, this.color)
    // 注意:回傳 this 以鏈結
    return this
  }
}

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

⬆ 回到目錄

偏好組合(composition)更甚於繼承(inheritance)

正如四人幫的設計模式,可以的話你應該優先使用組合而不是繼承。有許多好理由去使用繼承或是組合。重點是,如果你主觀認定是繼承,嘗試想一下組合能否替問題帶來更好的解法。你應該偏好使用組合更甚於繼承。

什麼時候使用繼承?這取決於你手上的問題,不過這有一些不錯的參考,說明什麼什麼時候繼承比組合更好用:

  1. 你的繼承為是一種(is-a)的關係,而不是有一個(has-a)。例如「人類是一種動物 Human -> Animal」vs.「使用者有一個使用者資料 User -> UserDetails」。
  2. 你能重複使用基類(base blasses)的程式碼。例如,人類能像動物一樣移動。
  3. 你希望能通過修改基類的程式碼來進行全域修改。例如,改變所有動物移動時的熱量消耗。

糟糕的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
class Employee {
  constructor(name, email) {
    this.name = name
    this.email = email
  }

  // ...
}

// 因為僱員有稅率金資料,而不是一種僱員
class EmployeeTaxData extends Employee {
  constructor(ssn, salary) {
    super()
    this.ssn = ssn
    this.salary = salary
  }

  // ...
}

適當的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
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)

單一功能原則 Single Responsibility Principle (SRP)

正如 Clean Code 所述:「永遠不要有超過一個理由來修改一個類型」。給一個類別塞滿許多功能,如同你在航班上只能帶一個行李箱一樣。這樣做的問題是,你的類別不會有理想的內聚性,將會有太多理由來對它進行修改。最小化需要修改一個類別的次數很重要,因為一個類別有太多的功能的話,一旦你修改一小部分,將會很難弄清楚它會對程式碼的其他模組造成什麼影響。

糟糕的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
class UserSettings {
  constructor(user) {
    this.user = user
  }

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

  verifyCredentials() {
    // ...
  }
}

適當的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
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()) {
      // ...
    }
  }
}

⬆ 回到目錄

開閉原則 Open/Closed Principle (OCP)

Bertrand Meyer 說過,「軟體實體(類別、模組、函數)應為開放擴展,但是關閉修改」。這原則基本上說明你應該同意使用者增加功能,而不用修改現有程式碼。

糟糕的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
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) => {
        // 轉換回應並回傳
      })
    } else if (this.adapter.name === 'nodeAdapter') {
      return makeHttpCall(url).then((response) => {
        // 轉換回應並回傳
      })
    }
  }
}

function makeAjaxCall(url) {
  // 發送請求並回傳 promise
}

function makeHttpCall(url) {
  // 發送請求並回傳 promise
}

適當的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class AjaxAdapter extends Adapter {
  constructor() {
    super()
    this.name = 'ajaxAdapter'
  }

  request(url) {
    // 發送請求並回傳 promise
  }
}

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

  request(url) {
    // 發送請求並回傳 promise
  }
}

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

  fetch(url) {
    return this.adapter.request(url).then((response) => {
      // transform response and return
    })
  }
}

⬆ 回到目錄

里氏替換原則 Liskov Substitution Principle (LSP)

這是一個驚人但簡單的概念,正式的定義為:「假如類別 S 是類別 T 的子類別,那麼類別 T 的物件(Object)可以被替換成類別 S 的物件(例如,類別 S 的物件可作為類別 T 的物件的替代品),而不需要改變任何程式的屬性(正確性、被執行的任務等)。

最好的解釋是這樣,假如你有父類別與子類別,父類別與子類別可以互換,而沒有問題發生。讓我們看看一個經典的正方形與長方形的案例。在數學上,正方形是長方形的一種,但如果你使用是一種(is-a)的關係用繼承來實現,你很快會發現問題。

糟糕的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
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() // 糟糕:結果為 25,應該為 20 才正確
    rectangle.render(area)
  })
}

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

適當的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
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)

譯者附註

setWidthsetHeight 方法無法被繼承,使用上的不一致,更造成了結果錯誤。

⬆ 回到目錄

介面隔離原則 Interface Segregation Principle (ISP)

JavaScript 沒有介面(interfaces),所以這個原則比較不像其他語言一樣嚴格。不過它在 JavaScript 這種缺少類型的語言來說一樣重要。

ISP 原則是「客戶端不應該被強制依賴他們不需要的介面。」因為 JavaScript 是一種弱型別的語言,所以介面是一種隱式(implicit)的協議。

巨大的設定物件(objects)是這個原則的好範例,不需要客戶去設定大量的選項是有好處的,因為多數的情況下,他們不需要全部的設定。讓他們可以被選擇,可以防止出現一個過胖的介面。

糟糕的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
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() {}, // 大多數的情況下,執行 traverse 時,我們其實不需要使用 animation。
  // ...
})

適當的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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 可被選擇要不要使用
    animationModule() {},
  },
})

⬆ 回到目錄

依賴反轉原則 Dependency Inversion Principle (DIP)

這原則說明兩個必要事情:

  1. 高層級的模組(modules)不應該依賴於低層級的模組。它們兩者必須依賴於抽象。
  2. 抽象(abstract)不應該依賴於具體實現(implement),具體實現則需要應依賴於抽象。

這原則一開始很難理解,但如果你使用過 AngularJS,你應該已經知道,使用依賴注入(Dependency Injection)來實現這個原則。雖然不是同一種概念,但透過依賴注入讓高層級模組遠離低層級模組的細節與設定。這樣做的巨大好處是,降低模組間的耦合。耦合是很糟的開發模式,因為會導致程式碼難以重構(refactor)。

如上所示,JavaScript 沒有介面,所以被依賴的抽象是隱式協議。也是就說,一個物件/類別的屬性直接暴露給另外一個。在以下的範例中,任何的請求模組(Request Module)的隱式協議 InventoryTracker 都會有一個 requestItems 的方法。

糟糕的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class InventoryRequester {
  constructor() {
    this.REQ_METHODS = ['HTTP']
  }

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

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

    // 糟糕的:我們建立了一種依賴,依賴於特定(InventoryRequester)請求模組的實現。
    // 我們實際上只有 `requestItems` 方法依賴於名為 `request` 的請求方法。
    this.requester = new InventoryRequester()
  }

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

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

適當的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
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) {
    // ...
  }
}

// 通過外部創建時將依賴注入,我們可以輕鬆地用全新的 WebSockets 的請求模組替換。
const inventoryTracker = new InventoryTracker(
  ['apples', 'bananas'],
  new InventoryRequesterV2()
)
inventoryTracker.requestItems()

譯者附註

在糟糕的案例中,InventoryTracker 沒有提供替換請求模組的可能,InventoryTracker 依賴於 InventoryRequester。造成耦合,測試將會被難以撰寫,修改程式碼時將會牽一髮而動全身。

⬆ 回到目錄

測試(Testing)

測試比發布更加重要。當發布時,如果你沒有測試或是測試不夠充分,你會無法確認有沒有任何功能被破壞。測試的量,由團隊決定,但是擁有 100% 的測試覆蓋率(包含狀態與分支),是你為什麼能有高度自信與內心平靜的原因。所以你需要一個偉大的測試框架,也需要一個好的覆蓋率(coverage)工具

沒有任何藉口不寫測試。這裡有很多好的 JS 測試框架,選一個你的團隊喜歡的。選擇好之後,接下來的目標是為任何新功能或是模組撰寫測試。如果你喜好測試驅動開發(Test Driven Development)的方式,那就太棒了,重點是確保上線前或是重構之前,達到足夠的覆蓋率。

譯者附註

測試是一種保障,當你趕著修正錯誤時,測試會告訴你會不會改了 A 壞了 B。確保每次上線前的功能皆可正常運作。另外測試有分種類,詳情見連結測試的種類

每個測試只測試一個概念

糟糕的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
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)
  })
})

適當的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
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)
  })
})

譯者附註

如果你單個測試,測試過多的功能或是概念,當這個測試出錯的時候,你將會難以找到出錯的程式碼。

⬆ 回到目錄

並發(Concurrency)

使用 Promises,不要使用回呼函式(callback)

回呼函式不怎麼簡潔,他們會導致過多的巢狀。在 ES2016/ES6,Promises 已經是內建的全局類型(global type)。使用它們吧!

糟糕的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
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')
        }
      })
    }
  }
)

適當的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import { get } from 'request'
import { writeFile } from 'fs'

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 更加簡潔

Promises 是回呼函式的一種非常簡潔的替代品,但是 ES2017/ES8 帶來了 async 與 await,提供了一個更簡潔的方案。你需要的只是一個前綴為 async 關鍵字的函數,接下來你編寫邏輯時就不需要使用 then 函數鍊。如果你能使用 ES2017/ES8 的進階功能的話,今天就使用它吧!

糟糕的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
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)
  })

適當的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
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)
  }
}

⬆ 回到目錄

錯誤處理(Error Handling)

拋出錯誤是一件好事情!代表運行時可以成功辨識程式中的錯誤,通過停止執行目前堆疊(stack)上的執行函數,結束(Node.js 中的)目前程序(process),並在控制台中用一個堆疊追蹤(stack trace)提醒你。

不要忽略捕捉到的錯誤

捕捉到一個錯誤時而不做任何處理,會讓你失去修復或是反應錯誤的能力。將錯誤紀錄於控制台(console.log)也不怎麼好,因為你往往會迷失在控制台大量的記錄之中。如果你使用 try/catch 包住程式碼,代表你預期這裡可能會出錯,因此當錯誤發生時你必須要有個處理方法。

糟糕的:

1
2
3
4
5
try {
  functionThatMightThrow()
} catch (error) {
  console.log(error)
}

適當的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
try {
  functionThatMightThrow()
} catch (error) {
  // 可以這樣(會比 console.log 更吵)
  console.error(error)
  // 或這種方法
  notifyUserOfError(error)
  // 另外一種方法
  reportErrorToService(error)
  // 或是全部都做!
}

不要忽略被拒絕的 Promises

原因如上節所述,不要忽略任何捕捉到的錯誤。

糟糕的:

1
2
3
4
5
6
7
getdata()
  .then((data) => {
    functionThatMightThrow(data)
  })
  .catch((error) => {
    console.log(error)
  })

適當的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
getdata()
  .then((data) => {
    functionThatMightThrow(data)
  })
  .catch((error) => {
    // 可以這樣(會比 console.log 更吵)
    console.error(error)
    // 或這種方法
    notifyUserOfError(error)
    // 另外一種方法
    reportErrorToService(error)
    // 或是全部都做!
  })

⬆ 回到目錄

格式化(Formatting)

格式化是很主觀的,就像其他規則一樣沒有硬性規定,沒有必要為了格式而爭論,這裡有大量的自動化格式工具,選一個就是了!對工程師來說,爭論格式就是在浪費時間與金錢。

針對自動格式化工具不能涵蓋的問題,這裡有一些指南。

使用一致的大小寫

JavaScript 是無型別的,所以從大小寫可以看出關於變數、函數等很多的事情。這些規則是很主觀的,所以你的團隊可以自由選擇。重點是,不管選了去什麼,就保持一致。

糟糕的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
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 {}

適當的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
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 {}

⬆ 回到目錄

函數的呼叫者應該與被呼叫者靠近

如果一個函數呼叫另外一個,在程式碼中兩個函數的垂直位置應該靠近。理想情況下,呼叫函數應於被呼叫函數的正上方。我們傾向於從上到下的閱讀方式,就像看報紙一樣。基於這個原因,讓你的程式碼可以依照這種方式閱讀。

糟糕的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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()

適當的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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()

⬆ 回到目錄

註解(Comments)

只對商業邏輯複雜的部分撰寫註解

註解是代表的辯解,而不是要求。多數情況下,好的程式碼本身就是文件。

糟糕的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
function hashIt(data) {
  // The hash
  let hash = 0

  // Length of string
  const length = data.length

  // Loop through every character in data
  for (let i = 0; i < length; i++) {
    // Get character code.
    const char = data.charCodeAt(i)
    // Make the hash
    hash = (hash << 5) - hash + char
    // Convert to 32-bit integer
    hash &= hash
  }
}

適當的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
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

    // Convert to 32-bit integer
    hash &= hash
  }
}

⬆ 回到目錄

不要在程式碼中保留被註解掉的程式碼

有了版本控制,舊的程式碼留在歷史紀錄中就好。

糟糕的:

1
2
3
4
doStuff()
// doOtherStuff();
// doSomeMoreStuff();
// doSoMuchStuff();

適當的:

1
doStuff()

⬆ 回到目錄

不要留有日誌式的註解

記住,使用版本控制!不需要無用的、註解掉的程式碼,尤其是日誌式的註解。使用 git log 來保存歷史紀錄。

糟糕的:

1
2
3
4
5
6
7
8
9
/**
 * 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
}

適當的:

1
2
3
function combine(a, b) {
  return a + b
}

譯者附註

在註解中寫歷史紀錄並沒有在版本控制中來得有效,另外補充有關歷史紀錄如何撰寫的規範(如何撰寫 Git Commit Message)。

⬆ 回到目錄

避免位置標示

它們只會增加干擾。讓函數與變數的名稱沿著合適的縮排與格式化,為你的程式碼帶來良好的視覺結構。

糟糕的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
////////////////////////////////////////////////////////////////////////////////
// Scope Model Instantiation
////////////////////////////////////////////////////////////////////////////////
$scope.model = {
  menu: 'foo',
  nav: 'bar',
}

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

適當的:

1
2
3
4
5
6
7
8
$scope.model = {
  menu: 'foo',
  nav: 'bar',
}

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

⬆ 回到目錄

翻譯(Translation)

以下為所有的翻譯版本。

⬆ 回到目錄