原作: https://github.com/ryanmcdermott/clean-code-javascript
原作者: https://github.com/ryanmcdermott
譯者: https://github.com/trylovetom
- 介紹(Introduction)
- 變數(Variables)
- 函數(Functions)
- 物件(Objects)與資料結構(Data Structures)
- 類別(Classes)
- 物件導向基本原則(SOLID)
- 測試(Testing)
- 並發(Concurrency)
- 錯誤處理(Error Handling)
- 格式化(Formatting)
- 註解(Comments)
- 翻譯(Translation)
透過計算閱讀程式碼時的咒罵次數,來評估軟體品質
文章作者根據 Robert C. Martin 的《無暇的程式碼》,撰寫一份適用於 JavaScript 的原則。本文不是風格指南(Style Guide),而是教導你撰寫出可閱讀、可重複使用與可重構的 JS 程式碼。
注意!你不必嚴格遵守每一項原則,有些甚至不被大眾所認同。雖然這只是份指南,卻是來自《無暇的程式碼》作者的多年結晶。
軟體工程只發展了五十年,仍然有很多地方值得去探討。當軟體與建築一樣古老時,也許會有一些墨守成規的原則。但現在,先讓這份指南當試金石,作為你和團隊的 JS 程式碼標準。
還有一件事情:知道這些原則,並不會立刻讓你成為出色的開發者,長期奉行它們,不代表你能高枕無憂不再犯錯。但是,千里之行,始於足下,時常與志同道合們進行討論(Code Review),改善不完備之處。不要因為自己寫出來的程式碼很糟糕而害怕分享,而是要畏懼自己居然寫出了這樣的程式碼!
譯者序
獻給傲嬌文創的所有工程師夥伴,與熱愛 JavaScript 的各位。
《無暇的程式碼》是一本好書,是不可否認這個事實。我熱愛著 JS,當找到這份 JS 版本的後,我無比開心立刻著手翻譯,因此有了這份《無暇的程式碼 JavaScript》。對於 Clean Code 的書名採用博碩文化的翻譯,也是對原版譯者的尊敬。本翻譯中會穿插一些我對文章中註解,也請讀者原諒我的叨擾。另外專業術語的翻譯有可能會有出入,我會標示出英文原文,避免讀者誤解。如有翻譯或是理解上的錯誤,煩請聯絡我,謝謝。(聯絡方式在上方,我的 GitHub 中。)
糟糕的:
const yyyymmdstr = moment().format('YYYY/MM/DD');適當的:
const currentDate = moment().format('YYYY/MM/DD');糟糕的:
getUserInfo();
getClientData();
getCustomerRecord();適當的:
getUser();使用易於閱讀與搜尋的名稱非常重要,因為我們要閱讀的程式碼遠比自己寫得多。使用沒有意義的名稱,會導致程式碼難以理解,對後續閱讀者是個糟糕的體驗。另外使用以下工具,可以協助你找出未命名的常數:
糟糕的:
// 86400000 代表什麼意義?
setTimeout(blastOff, 86400000);適當的:
// 宣告(Declare)一個有意義的常數(constants)
const MILLISECONDS_IN_A_DAY = 86400000;
setTimeout(blastOff, MILLISECONDS_IN_A_DAY);糟糕的:
const address = 'One Infinite Loop, Cupertino 95014';
const cityZipCodeRegex = /^[^,\\]+[,\\\s]+(.+?)\s*(\d{5})?$/;
saveCityZipCode(
  address.match(cityZipCodeRegex)[1],
  address.match(cityZipCodeRegex)[2]
);適當的:
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。在適當的範例中,則清楚地解釋了。
清晰(Explicit)的表達會比隱藏(Implicit)更好。
糟糕的:
const locations = ['Austin', 'New York', 'San Francisco'];
locations.forEach(l => {
  doStuff();
  doSomeOtherStuff();
  // ...
  // ...
  // ...
  // 等等,這 `l` 是…?
  dispatch(l);
});適當的:
const locations = ['Austin', 'New York', 'San Francisco'];
locations.forEach(location => {
  doStuff();
  doSomeOtherStuff();
  // ...
  // ...
  // ...
  dispatch(location);
});譯者附註
在糟糕的範例中,程式碼的作者認為從 locations 取出的都是地址,所以選用縮減後的 l  作為名稱。不過這只有作者自己這麼認為,其他人可不一定知道。避免「我認為」、「我以為」、「我覺得」,這樣的心理作用。
如果你的類別與物件名稱是有關聯意義的,就不用在內部變數上再次重複。
糟糕的:
const Car = {
  carMake: 'Honda',
  carModel: 'Accord',
  carColor: 'Blue'
};
function paintCar(car, color) {
  car.carColor = color;
}適當的:
const Car = {
  make: 'Honda',
  model: 'Accord',
  color: 'Blue'
};
function paintCar(color) {
  car.color = color;
}使用預設參數較整潔,但請注意,當參數為 undefined 時才會作用,其他種類的虛值(Falsy)不會,像是 ''、false、null、0、NaN 等。
糟糕的:
function createMicrobrewery(name) {
  const breweryName = name || 'Hipster Brew Co.';
  // ...
}適當的:
function createMicrobrewery(name = 'Hipster Brew Co.') {
  // ...
}譯者附註
預設參數非常好用,可以結合工廠模式做出很多應用。另外建議統一使用 undefined 代替 null 當作空值的回傳值。
此處原文為 Arguments,但是函數的參數定義應該為 Parameter,而呼叫函數時傳遞的引數才是 Arguments。
限制函數的參數數量非常重要,因為能讓你更容易地測試。過多的參數代表著過多的組合,會導致你不得不編寫出大量測試。
一個至二個是最理想的,盡可能避免大於三個以上。如果你有超過兩個以上的參數,代表你的函數做太多事情。如果無法避免時,可以有效地使用物件替代大量的參數。
為了讓你可以清晰地表達,預期使用哪些物件的屬性(Properties),可以使用 ES2015/ES6 提供的解構(Destructuring)語法。使用這種語法有以下優點:
- 函數需要物件的哪些屬性,可以像參數一樣清晰地表達。
- 解構語法會複製來自物件的原始型態(Primitive),這能幫助你避免副作用(Side Effect)。注意!巢狀物件與陣列,並不會被解構語法複製。
- 使用解構語法能讓物件屬性被程式碼檢查器(Linter)作用,提醒你哪些屬性未被使用到。
糟糕的:
function createMenu(title, body, buttonText, cancellable) {
  // ...
}適當的:
function createMenu({ title, body, buttonText, cancellable }) {
  // ...
}
createMenu({
  title: 'Foo',
  body: 'Bar',
  buttonText: 'Baz',
  cancellable: true
});譯者附註
這種方法非常適合用於工廠模式,結合上一章的預設參數,譯者推薦的使用方式如下:
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)與清晰地閱讀。如果嚴格遵守此項原則,你將會領先許多開發者。
糟糕的:
function emailClients(clients) {
  clients.forEach(client => {
    const clientRecord = database.lookup(client);
    if (clientRecord.isActive()) {
      email(client);
    }
  });
}適當的:
function emailActiveClients(clients) {
  clients.filter(isActiveClient).forEach(email);
}
function isActiveClient(client) {
  const clientRecord = database.lookup(client);
  return clientRecord.isActive();
}糟糕的:
function addToDate(date, month) {
  // ...
}
const date = new Date();
// 難以從函數名稱看出到底加入了什麼
addToDate(date, 1);適當的:
function addMonthToDate(month, date) {
  // ...
}
const date = new Date();
addMonthToDate(1, date);譯者附註
建議函數命名以動詞開頭,像是 doSomething()、setupUserProfile()。
當你的函數需要的抽象多於一層時,代表你的函數做太多事情了。將其分解以利重用與測試。
糟糕的:
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...
  });
}適當的:
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;
}譯者附註
這個原則與前面提到的「一個函數只做一件事情(單一性)」概念相似。
絕對避免重複的程式碼,重複的程式碼代表著更動邏輯時,需要同時修改多處。
想像一下你經營著一家餐廳,需要持續追蹤存貨:番茄、洋蔥、大蒜與各種香料等。如果你有多份紀錄表,當使用蕃茄做完一道料理時,需要更新多份記錄表,可能會忘記更新其中一份。如果你只有一份記錄表,就不會有此問題!
通常重複的程式碼,是因為有兩個稍微不同的東西。它們之間絕大部分相同,但些微不同之處,迫使你使用多個函數處理相似的事情。如果出現重複的程式碼,可以使用函數、模組或是類別來抽象化處理。
正確的抽象化是非常關鍵的,這也是為什麼你應該遵循類別(Classes)章節中,物件導向基本原則 SOLID 的原因。請小心,較差的抽象化會比重複的程式碼更糟!這麼說吧,如果你有把握做出好的抽象化,盡情放手去做。別讓程式碼出現重複的地方,不然你會需要修改更多的程式碼。
糟糕的:
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);
  });
}適當的:
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)造成功能開發窒礙難行,設計了良好架構,但實際上並未被使用到。最後總結了一個建議作為附加原則:一開始撰寫程式碼先以功能開發優先,當你發現有兩個以上的地方重複時,再來考慮要不要重構。
糟糕的:
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);適當的:
const menuConfig = {
  title: 'Order',
  // 使用者漏掉了 'body'
  buttonText: 'Send',
  cancellable: true
};
function createMenu(config) {
  let finalConfig = Object.assign(
    {
      title: 'Foo',
      body: 'Bar',
      buttonText: 'Baz',
      cancellable: true
    },
    config
  );
  return finalConfig;
  // config 現在等同於: {title: 'Order', body: 'Bar', buttonText: 'Send', cancellable: true}
  // ...
}
createMenu(menuConfig);當你的函數使用了旗標當作參數時,代表函數做不只一件事情,依照不同旗標路徑切分你的函數。
糟糕的:
function createFile(name, temp) {
  if (temp) {
    fs.create(`./temp/${name}`);
  } else {
    fs.create(name);
  }
}適當的:
function createFile(name) {
  fs.create(name);
}
function createTempFile(name) {
  createFile(`./temp/${name}`);
}當函數作用在除了回傳值外的地方,像是讀寫文件、修改全域變數或是將你的錢轉帳到其他人帳戶,則稱為副作用。
程式在某些情況下是需要副作用的,像是上面所提到的例子。這時你應該將這些功能集中在一起,不要同時有多個函數或是類別同時操作資源,應該只用一個服務(Service)完成這些事情。
常見的問題像是:
- 在沒有任何架構下,同時多個物件中分享共有狀態。
- 可變的狀態,且可以被任何人寫入
- 副作用發生的地方沒有被集中。
如果你能避免這些問題,你會比大多數的工程師快樂。
糟糕的:
// 全域變數被以下函數使用
// 假如有其他的函數使用了這個名稱,現在他變成了陣列,將會被破壞而出錯。
let name = 'Ryan McDermott';
function splitIntoFirstAndLastName() {
  name = name.split(' ');
}
splitIntoFirstAndLastName();
console.log(name); // ['Ryan', 'McDermott'];適當的:
function splitIntoFirstAndLastName(name) {
  return name.split(' ');
}
const name = 'Ryan McDermott';
const newName = splitIntoFirstAndLastName(name);
console.log(name); // 'Ryan McDermott';
console.log(newName); // ['Ryan', 'McDermott'];在 JavaScript 中,原始資料類型傳遞數值(Value),物件/陣列傳遞參照(Reference)。在本案例中,你的函數改變了購物車清單 cart 中的陣列,像是你增加了一個商品,其他使用購物車清單的函數將會被影響。這做法有好有壞,讓我解釋一下問題所在:
使用者按下付款按鈕後,將會呼叫 purchase 函數,產生一個網路請求傳送購物車清單陣列到伺服器。因為較差的網路連線,必須多嘗試幾次。此時使用者不小心又按下加入購物車按鈕,因為是參考的關係,請求將會送出新加入的商品。
較好的解決辦法是 addItemToCart 函數,執行前複製新的一份購物車清單,修改複製的資料後再回傳。這能確保其他的函數的購物車清單沒有任何機會被被參考所影響。
使用這方法前,有兩個警告要告知:
- 
當採用這種做法後,你會發現,需要修改輸入物件的情況非常少。大多數的程式碼可以在沒有副作用的情況下重構! 
- 
複製大型物件,需要花費高昂的效能。幸好,我們有好的函數庫,可以提升複製物件與陣列的速度與減少記憶體使用。 
糟糕的:
const addItemToCart = (cart, item) => {
  cart.push({ item, date: Date.now() });
};適當的:
const addItemToCart = (cart, item) => {
  return [...cart, { item, date: Date.now() }];
};譯者附註
譯者這邊另外再提供一個案例,函數 checkIs18Age 用來檢查是否成年。第一個寫法中,引用了全域(global)變數 minimum,這功能看起來能正常運作沒問題,但是今天如果有其他函數 setMinAge 修改了全域變數 minimum,函數 checkIs18Age 將會因為副作用的關係變的無法預期,甚至失去它的作用。
較好的寫法是,採用純函數(pure function),使用一個可預期的變數,來避免副作用影響。
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
}另外使用解構的方式複製資料,只會複製第一層而已。
// 巢狀解構只能複製第一層
const obj1 = { subObj: { message: 'Hey' } }
const obj2 = { ...obj1 }
obj2.subObj.message = 'Yo'
console.log(obj1.subObj.message) // 'Yo'如果要複製巢狀結構,你需要深度複製。可以使用一些函數庫 loadsh 的 _.CloneDeep 或是 ramda 的 clone。或是使用 JSON.parse(JSON.stringify(object)) 來實現。不過使用 JSON 的話,會失去 Function 的複製。
// 使用 JSON 來深度複製
const obj1 = { subObj: { message: 'Hey' } }
const obj2 = JSON.parse(JSON.stringify(obj1))
obj2.subObj.message = 'Yo'
console.log(obj1.subObj.message) // 'Hey'在 JavaScript 中弄髒全域是個不好的做法,因為你可能會影響到其他函數庫或是 API。舉個例子,如果妳想要在 JavaScript 的原生陣列方法,擴展  diff 方法,用 B 陣列來去除 A 陣列中的元素(Element)。常見做法你可能會在  Array.prototype 中增加一個全新的函數,如果其他函數庫也有自己的 diff 實現的話將會互相影響。這就是為什麼我們需要使用 ES2015/ES6 的類別,來擴展的原因。
糟糕的:
Array.prototype.diff = function diff(comparisonArray) {
  const hash = new Set(comparisonArray);
  return this.filter(elem => !hash.has(elem));
};適當的:
class SuperArray extends Array {
  diff(comparisonArray) {
    const hash = new Set(comparisonArray);
    return this.filter(elem => !hash.has(elem));
  }
}JavaScript 不是像 Haskell 一樣的函數式語言,但它具有類似特性。函數式程式設計更加乾淨且容易被測試。當你在寫程式時,盡量選擇此設計方式。
糟糕的:
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;
}適當的:
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
);糟糕的:
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) 達到相同的效果。第二個問題來了,「為什麼我們需要這樣做呢?」依據前面概念,為了保持程式碼的乾淨,當類別或是函數出現 if 語法,代表你的函數做了超過一件事情。記住,一個函數只做一件事情!
糟糕的:
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();
    }
  }
}適當的:
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();
  }
}JavaScript 為弱型別語言,代表函數應能處理任何型別的引數(argument)。有時這會帶給你一些麻煩,讓你需要做型別檢查。有很多方法可以避免這種問題發生,第一種就是統一所有的 API。
糟糕的:
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'));
  }
}適當的:
function travelToTexas(vehicle) {
  vehicle.move(this.currentLocation, new Location('texas'));
}譯者附註
此範例統一了所有的車輛移動的參數、方法與實作,所以不再需要區分不同的類別的車輛呼叫不同的方法。
假設你需要型別檢查原始數值,像是字串與整數且你無法使用多態性處理,考慮使用 TypeScript 吧。他是提供標準 JavaScript 靜態類型的的最佳選擇。手動型別檢查需要很多額外處理,你得到的是虛假的型別安全,且失去的可讀性。保持你的 JavaScript 程式碼的整潔、寫好測試與足夠的程式碼審查(Code Review)。如果加上使用 TypeScript 會是更好的選擇。
糟糕的:
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');
}適當的:
function combine(val1, val2) {
  return val1 + val2;
}現代瀏覽器在運行時幫你做了很多優化。大多數的情況,你所做的優化都是在浪費你的時間。這裏有些很好的資源,去了解哪些優化是無用的。
糟糕的:
// 在舊的瀏覽器中並不會快取(cache)`list.length`,每次迭代(iteration)時的重新計算相當損耗效能。
// 這在新瀏覽器中已被優化,你不用手動去快取。
for (let i = 0, len = list.length; i < len; i++) {
  // ...
}適當的:
for (let i = 0; i < list.length; i++) {
  // ...
}譯者附註
簡單來說,你不用在意程式語言層面上優化,因為這部分會因為版本更新而得到優化。但不要因此放棄所有的優化,演算法的優化才是你該注意的地方!
沒有任何理由保留無用的程式碼,如果他們沒有被使用到,移除它!讓它們被保留在版本歷史中。
糟糕的:
function oldRequestModule(url) {
  // ...
}
function newRequestModule(url) {
  // ...
}
const req = newRequestModule;
inventoryTracker('apples', req, 'www.inventory-awesome.io');適當的:
function newRequestModule(url) {
  // ...
}
const req = newRequestModule;
inventoryTracker('apples', req, 'www.inventory-awesome.io');使用 getters 與 setters 來存取物件中資料,會比單純使用屬性(property)來的好。因為:
- 當你想要在取得物件屬性時做更多事情,你不用找出所有的程式碼修改。
- 透過 set可以建立規則進行資料校驗。
- 封裝內部邏輯。
- 存取時增加日誌(logging)與錯誤處理(error handling)。
- 你可以延遲載入你的物件屬性,像是來自伺服器的資料。
糟糕的:
function makeBankAccount() {
  // ...
  return {
    balance: 0
    // ...
  };
}
const account = makeBankAccount();
account.balance = 100;適當的:
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);可以透過閉包(closures)來私有化參數(使用於 ES5 以下)。
糟糕的:
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適當的:
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 DoeES5 的類別定義非常難以閱讀、繼承、建造與定義方法。假設你需要繼承(請注意你有可能不需要),偏好使用 ES2015/ES6 的類別。除非你需要大型且複雜的物件,不然使用小型函數會比類別更好。
糟糕的:
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() {};適當的:
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() {
    // ...
  }
}這個模式(pattern)在 JavaScript 中非常有用,你可以在很多函數庫中看到,像是 jQuery 與 Lodash。它可以讓你的程式碼表達的更好。
基於這個原因,方法鏈可以讓你的程式碼看起來更加簡潔。在你的類別函數中,只需要回傳 this 在每一個函數中,你就可以鏈結所有類別中的方法。
糟糕的:
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();適當的:
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();正如四人幫的設計模式,可以的話你應該優先使用組合而不是繼承。有許多好理由去使用繼承或是組合。重點是,如果你主觀認定是繼承,嘗試想一下組合能否替問題帶來更好的解法。你應該偏好使用組合更甚於繼承。
什麼時候使用繼承?這取決於你手上的問題,不過這有一些不錯的參考,說明什麼什麼時候繼承比組合更好用:
- 你的繼承為是一種(is-a)的關係,而不是有一個(has-a)。例如「人類是一種動物 Human -> Animal」vs.「使用者有一個使用者資料 User -> UserDetails」。
- 你能重複使用基類(base classes)的程式碼。例如,人類能像動物一樣移動。
- 你希望能通過修改基類的程式碼來進行全域修改。例如,改變所有動物移動時的熱量消耗。
糟糕的:
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;
  }
  // ...
}適當的:
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);
  }
  // ...
}正如 Clean Code 所述:「永遠不要有超過一個理由來修改一個類型」。給一個類別塞滿許多功能,如同你在航班上只能帶一個行李箱一樣。這樣做的問題是,你的類別不會有理想的內聚性,將會有太多理由來對它進行修改。最小化需要修改一個類別的次數很重要,因為一個類別有太多的功能的話,一旦你修改一小部分,將會很難弄清楚它會對程式碼的其他模組造成什麼影響。
糟糕的:
class UserSettings {
  constructor(user) {
    this.user = user;
  }
  changeSettings(settings) {
    if (this.verifyCredentials()) {
      // ...
    }
  }
  verifyCredentials() {
    // ...
  }
}適當的:
class UserAuth {
  constructor(user) {
    this.user = user;
  }
  verifyCredentials() {
    // ...
  }
}
class UserSettings {
  constructor(user) {
    this.user = user;
    this.auth = new UserAuth(user);
  }
  changeSettings(settings) {
    if (this.auth.verifyCredentials()) {
      // ...
    }
  }
}Bertrand Meyer 說過,「軟體實體(類別、模組、函數)應為開放擴展,但是關閉修改」。這原則基本上說明你應該同意使用者增加功能,而不用修改現有程式碼。
糟糕的:
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
}適當的:
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
    });
  }
}這是一個驚人但簡單的概念,正式的定義為:「假如類別 S 是類別 T 的子類別,那麼類別 T 的物件(Object)可以被替換成類別 S 的物件(例如,類別 S 的物件可作為類別 T 的物件的替代品),而不需要改變任何程式的屬性(正確性、被執行的任務等)。
最好的解釋是這樣,假如你有父類別與子類別,父類別與子類別可以互換,而沒有問題發生。讓我們看看一個經典的正方形與長方形的案例。在數學上,正方形是長方形的一種,但如果你使用是一種(is-a)的關係用繼承來實現,你很快會發現問題。
糟糕的:
class Rectangle {
  constructor() {
    this.width = 0;
    this.height = 0;
  }
  setColor(color) {
    // ...
  }
  render(area) {
    // ...
  }
  setWidth(width) {
    this.width = width;
  }
  setHeight(height) {
    this.height = height;
  }
  getArea() {
    return this.width * this.height;
  }
}
class Square extends Rectangle {
  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);適當的:
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);譯者附註
setWidth 與 setHeight 方法無法被繼承,使用上的不一致,更造成了結果錯誤。
JavaScript 沒有介面(interfaces),所以這個原則比較不像其他語言一樣嚴格。不過它在 JavaScript 這種缺少類型的語言來說一樣重要。
ISP 原則是「客戶端不應該被強制依賴他們不需要的介面。」因為 JavaScript 是一種弱型別的語言,所以介面是一種隱式(implicit)的協議。
巨大的設定物件(objects)是這個原則的好範例,不需要客戶去設定大量的選項是有好處的,因為多數的情況下,他們不需要全部的設定。讓他們可以被選擇,可以防止出現一個過胖的介面。
糟糕的:
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。
  // ...
});適當的:
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() {}
  }
});這原則說明兩個必要事情:
- 高層級的模組(modules)不應該依賴於低層級的模組。它們兩者必須依賴於抽象。
- 抽象(abstract)不應該依賴於具體實現(implement),具體實現則應依賴於抽象。
這原則一開始很難理解,但如果你使用過 AngularJS,你應該已經知道,使用依賴注入(Dependency Injection)來實現這個原則。雖然不是同一種概念,但透過依賴注入讓高層級模組遠離低層級模組的細節與設定。這樣做的巨大好處是,降低模組間的耦合。耦合是很糟的開發模式,因為會導致程式碼難以重構(refactor)。
如上所示,JavaScript 沒有介面,所以被依賴的抽象是隱式協議。也是就說,一個物件/類別的屬性直接暴露給另外一個。在以下的範例中,任何的請求模組(Request Module)的隱式協議 InventoryTracker 都會有一個 requestItems 的方法。
糟糕的:
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();適當的:
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。造成耦合,測試將會被難以撰寫,修改程式碼時將會牽一髮而動全身。
測試比發布更加重要。當發布時,如果你沒有測試或是測試不夠充分,你會無法確認有沒有任何功能被破壞。測試的量,由團隊決定,但是擁有 100% 的測試覆蓋率(包含狀態與分支),是你為什麼能有高度自信與內心平靜的原因。所以你需要一個偉大的測試框架,也需要一個好的覆蓋率(coverage)工具。
沒有任何藉口不寫測試。這裡有很多好的 JS 測試框架,選一個你的團隊喜歡的。選擇好之後,接下來的目標是為任何新功能或是模組撰寫測試。如果你喜好測試驅動開發(Test Driven Development)的方式,那就太棒了,重點是確保上線前或是重構之前,達到足夠的覆蓋率。
譯者附註
測試是一種保障,當你趕著修正錯誤時,測試會告訴你會不會改了 A 壞了 B。確保每次上線前的功能皆可正常運作。另外測試有分種類,詳情見連結測試的種類。
糟糕的:
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);
  });
});適當的:
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);
  });
});譯者附註
如果你單個測試,測試過多的功能或是概念,當這個測試出錯的時候,你將會難以找到出錯的程式碼。
回呼函式不怎麼簡潔,他們會導致過多的巢狀。在 ES2016/ES6,Promises 已經是內建的全局類型(global type)。使用它們吧!
糟糕的:
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');
        }
      });
    }
  }
);適當的:
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);
  });Promises 是回呼函式的一種非常簡潔的替代品,但是 ES2017/ES8 帶來了 async 與 await,提供了一個更簡潔的方案。你需要的只是一個前綴為 async 關鍵字的函數,接下來你編寫邏輯時就不需要使用 then 函數鍊。如果你能使用 ES2017/ES8 的進階功能的話,今天就使用它吧!
糟糕的:
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);
  });適當的:
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);
  }
}拋出錯誤是一件好事情!代表運行時可以成功辨識程式中的錯誤,通過停止執行目前堆疊(stack)上的執行函數,結束(Node.js 中的)目前程序(process),並在控制台中用一個堆疊追蹤(stack trace)提醒你。
捕捉到一個錯誤時而不做任何處理,會讓你失去修復或是反應錯誤的能力。將錯誤紀錄於控制台(console.log)也不怎麼好,因為你往往會迷失在控制台大量的記錄之中。如果你使用 try/catch 包住程式碼,代表你預期這裡可能會出錯,因此當錯誤發生時你必須要有個處理方法。
糟糕的:
try {
  functionThatMightThrow();
} catch (error) {
  console.log(error);
}適當的:
try {
  functionThatMightThrow();
} catch (error) {
  // 可以這樣(會比 console.log 更吵)
  console.error(error);
  // 或這種方法
  notifyUserOfError(error);
  // 另外一種方法
  reportErrorToService(error);
  // 或是全部都做!
}原因如上節所述,不要忽略任何捕捉到的錯誤。
糟糕的:
getdata()
  .then(data => {
    functionThatMightThrow(data);
  })
  .catch(error => {
    console.log(error);
  });適當的:
getdata()
  .then(data => {
    functionThatMightThrow(data);
  })
  .catch(error => {
    // 可以這樣(會比 console.log 更吵)
    console.error(error);
    // 或這種方法
    notifyUserOfError(error);
    // 另外一種方法
    reportErrorToService(error);
    // 或是全部都做!
  });格式化是很主觀的,就像其他規則一樣沒有硬性規定,沒有必要為了格式而爭論,這裡有大量的自動化格式工具,選一個就是了!對工程師來說,爭論格式就是在浪費時間與金錢。
針對自動格式化工具不能涵蓋的問題,這裡有一些指南。
JavaScript 是動態型別的語言,所以從大小寫可以看出關於變數、函數等很多的事情。這些規則是很主觀的,所以你的團隊可以自由選擇。重點是,不管選了去什麼,就保持一致。
糟糕的:
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 {}適當的:
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 {}如果一個函數呼叫另外一個,在程式碼中兩個函數的垂直位置應該靠近。理想情況下,呼叫函數應於被呼叫函數的正上方。我們傾向於從上到下的閱讀方式,就像看報紙一樣。基於這個原因,讓你的程式碼可以依照這種方式閱讀。
糟糕的:
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();適當的:
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();註解是代表的辯解,而不是要求。多數情況下,好的程式碼本身就是文件。
糟糕的:
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;
  }
}適當的:
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;
  }
}有了版本控制,舊的程式碼留在歷史紀錄中就好。
糟糕的:
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;
}譯者附註
在註解中寫歷史紀錄並沒有在版本控制中來得有效,另外補充有關歷史紀錄如何撰寫的規範(如何撰寫 Git Commit Message)。
它們只會增加干擾。讓函數與變數的名稱沿著合適的縮排與格式化,為你的程式碼帶來良好的視覺結構。
糟糕的:
////////////////////////////////////////////////////////////////////////////////
// Scope Model Instantiation
////////////////////////////////////////////////////////////////////////////////
$scope.model = {
  menu: 'foo',
  nav: 'bar'
};
////////////////////////////////////////////////////////////////////////////////
// Action setup
////////////////////////////////////////////////////////////////////////////////
const actions = function() {
  // ...
};適當的:
$scope.model = {
  menu: 'foo',
  nav: 'bar'
};
const actions = function() {
  // ...
};以下為所有的翻譯版本。
 French:
GavBaros/clean-code-javascript-fr French:
GavBaros/clean-code-javascript-fr
 Brazilian Portuguese: fesnt/clean-code-javascript Brazilian Portuguese: fesnt/clean-code-javascript
 Spanish: andersontr15/clean-code-javascript Spanish: andersontr15/clean-code-javascript
 Spanish: tureey/clean-code-javascript Spanish: tureey/clean-code-javascript
 Simplified Chinese: Simplified Chinese:
 Traditional Chinese: AllJointTW/clean-code-javascript Traditional Chinese: AllJointTW/clean-code-javascript
 German: marcbruederlin/clean-code-javascript German: marcbruederlin/clean-code-javascript
 Korean: qkraudghgh/clean-code-javascript-ko Korean: qkraudghgh/clean-code-javascript-ko
 Polish: greg-dev/clean-code-javascript-pl Polish: greg-dev/clean-code-javascript-pl
 Russian: Russian:
 Vietnamese: hienvd/clean-code-javascript/ Vietnamese: hienvd/clean-code-javascript/
 Japanese: mitsuruog/clean-code-javascript/ Japanese: mitsuruog/clean-code-javascript/
 Indonesia:
andirkh/clean-code-javascript/ Indonesia:
andirkh/clean-code-javascript/
 Italian:
frappacchio/clean-code-javascript/ Italian:
frappacchio/clean-code-javascript/
