├── .gitattributes ├── LICENSE └── README.md /.gitattributes: -------------------------------------------------------------------------------- 1 | *.md linguist-documentation=false 2 | *.md linguist-language=JavaScript 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Ryan McDermott 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 無瑕的程式碼 JavaScript 2 | > 原作: [https://github.com/ryanmcdermott/clean-code-javascript](https://github.com/ryanmcdermott/clean-code-javascript)
3 | > 原作者: [https://github.com/ryanmcdermott](https://github.com/ryanmcdermott)
4 | > 譯者: [https://github.com/trylovetom](https://github.com/trylovetom) 5 | 6 | ## 目錄(Table of Contents) 7 | 1. [介紹(Introduction)](#介紹Introduction) 8 | 2. [變數(Variables)](#變數Variables) 9 | 3. [函數(Functions)](#函數Functions) 10 | 4. [物件(Objects)與資料結構(Data Structures)](#物件Objects與資料結構Data-Structures) 11 | 5. [類別(Classes)](#類別Classes) 12 | 6. [物件導向基本原則(SOLID)](#物件導向基本原則SOLID) 13 | 7. [測試(Testing)](#測試Testing) 14 | 8. [並發(Concurrency)](#並發Concurrency) 15 | 9. [錯誤處理(Error Handling)](#錯誤處理Error-Handling) 16 | 10. [格式化(Formatting)](#格式化Formatting) 17 | 11. [註解(Comments)](#註解Comments) 18 | 12. [翻譯(Translation)](#翻譯Translation) 19 | 20 | ## 介紹(Introduction) 21 | ![透過計算閱讀程式碼時的咒罵次數,來評估軟體品質](http://www.osnews.com/images/comics/wtfm.jpg)
透過計算閱讀程式碼時的咒罵次數,來評估軟體品質

22 | 文章作者根據 Robert C. Martin 的[《無暇的程式碼》](https://www.amazon.com/Clean-Code-Handbook-Software-Craftsmanship/dp/0132350882),撰寫一份適用於 JavaScript 的原則。本文不是風格指南(Style Guide),而是教導你撰寫出[可閱讀、可重複使用與可重構](https://github.com/ryanmcdermott/3rs-of-software-architecture)的 JS 程式碼。 23 | 24 | 注意!你不必嚴格遵守每一項原則,有些甚至不被大眾所認同。雖然這只是份指南,卻是來自《無暇的程式碼》作者的多年結晶。 25 | 26 | 軟體工程只發展了五十年,仍然有很多地方值得去探討。當軟體與建築一樣古老時,也許會有一些墨守成規的原則。但現在,先讓這份指南當試金石,作為你和團隊的 JS 程式碼標準。 27 | 28 | 還有一件事情:知道這些原則,並不會立刻讓你成為出色的開發者,長期奉行它們,不代表你能高枕無憂不再犯錯。但是,千里之行,始於足下,時常與志同道合們進行討論(Code Review),改善不完備之處。不要因為自己寫出來的程式碼很糟糕而害怕分享,而是要畏懼自己居然寫出了這樣的程式碼! 29 | 30 | **譯者序** 31 | > 獻給[傲嬌文創](https://alljoint.tw/)的所有工程師夥伴,與熱愛 JavaScript 的各位。 32 | 33 | 《無暇的程式碼》是一本好書,是不可否認這個事實。我熱愛著 JS,當找到這份 JS 版本的後,我無比開心立刻著手翻譯,因此有了這份《無暇的程式碼 JavaScript》。對於 *Clean Code* 的書名採用博碩文化的翻譯,也是對原版譯者的尊敬。本翻譯中會穿插一些我對文章中註解,也請讀者原諒我的叨擾。另外專業術語的翻譯有可能會有出入,我會標示出英文原文,避免讀者誤解。如有翻譯或是理解上的錯誤,煩請聯絡我,謝謝。(聯絡方式在上方,我的 GitHub 中。) 34 | 35 | ## 變數(Variables) 36 | ### 使用具有意義且可閱讀的名稱 37 | **糟糕的:** 38 | ```javascript 39 | const yyyymmdstr = moment().format('YYYY/MM/DD'); 40 | ``` 41 | 42 | **適當的:** 43 | ```javascript 44 | const currentDate = moment().format('YYYY/MM/DD'); 45 | ``` 46 | 47 | **[⬆ 回到目錄](#目錄table-of-contents)** 48 | 49 | ### 相同類型的變數使用相同的名稱 50 | **糟糕的:** 51 | ```javascript 52 | getUserInfo(); 53 | getClientData(); 54 | getCustomerRecord(); 55 | ``` 56 | 57 | **適當的:** 58 | ```javascript 59 | getUser(); 60 | ``` 61 | 62 | **[⬆ 回到目錄](#目錄table-of-contents)** 63 | 64 | ### 使用可搜尋的名稱 65 | 使用易於閱讀與搜尋的名稱非常重要,因為我們要閱讀的程式碼遠比自己寫得多。使用沒有意義的名稱,會導致程式碼難以理解,對後續閱讀者是個糟糕的體驗。另外使用以下工具,可以協助你找出未命名的常數: 66 | * [buddy.js](https://github.com/danielstjules/buddy.js) 67 | * [ESLint](https://github.com/eslint/eslint/blob/660e0918933e6e7fede26bc675a0763a6b357c94/docs/rules/no-magic-numbers.md) 68 | 69 | **糟糕的:** 70 | ```javascript 71 | // 86400000 代表什麼意義? 72 | setTimeout(blastOff, 86400000); 73 | ``` 74 | 75 | **適當的:** 76 | ```javascript 77 | // 宣告(Declare)一個有意義的常數(constants) 78 | const MILLISECONDS_IN_A_DAY = 86400000; 79 | 80 | setTimeout(blastOff, MILLISECONDS_IN_A_DAY); 81 | ``` 82 | 83 | **[⬆ 回到目錄](#目錄table-of-contents)** 84 | 85 | ### 使用可解釋的變數 86 | **糟糕的:** 87 | ```javascript 88 | const address = 'One Infinite Loop, Cupertino 95014'; 89 | const cityZipCodeRegex = /^[^,\\]+[,\\\s]+(.+?)\s*(\d{5})?$/; 90 | saveCityZipCode( 91 | address.match(cityZipCodeRegex)[1], 92 | address.match(cityZipCodeRegex)[2] 93 | ); 94 | ``` 95 | 96 | **適當的:** 97 | ```javascript 98 | const address = 'One Infinite Loop, Cupertino 95014'; 99 | const cityZipCodeRegex = /^[^,\\]+[,\\\s]+(.+?)\s*(\d{5})?$/; 100 | const [_, city, zipCode] = address.match(cityZipCodeRegex) || []; 101 | saveCityZipCode(city, zipCode); 102 | ``` 103 | 104 | **譯者附註** 105 | 106 | `address.match(cityZipCodeRegex)` 取出了字串中的 city 與 zipCode 並以陣列(Array)的方式輸出。在糟糕的範例中,你不會知道哪個是 city,哪個是 zipCode。在適當的範例中,則清楚地解釋了。 107 | 108 | **[⬆ 回到目錄](#目錄table-of-contents)** 109 | 110 | ### 避免心理作用(Mental Mapping) 111 | 清晰(Explicit)的表達會比隱藏(Implicit)更好。 112 | 113 | **糟糕的:** 114 | ```javascript 115 | const locations = ['Austin', 'New York', 'San Francisco']; 116 | locations.forEach(l => { 117 | doStuff(); 118 | doSomeOtherStuff(); 119 | // ... 120 | // ... 121 | // ... 122 | // 等等,這 `l` 是…? 123 | dispatch(l); 124 | }); 125 | ``` 126 | 127 | **適當的:** 128 | ```javascript 129 | const locations = ['Austin', 'New York', 'San Francisco']; 130 | locations.forEach(location => { 131 | doStuff(); 132 | doSomeOtherStuff(); 133 | // ... 134 | // ... 135 | // ... 136 | dispatch(location); 137 | }); 138 | ``` 139 | 140 | **譯者附註** 141 | 142 | 在糟糕的範例中,程式碼的作者認為從 `locations` 取出的都是地址,所以選用縮減後的 `l` 作為名稱。不過這只有作者自己這麼認為,其他人可不一定知道。避免「我認為」、「我以為」、「我覺得」,這樣的心理作用。 143 | 144 | **[⬆ 回到目錄](#目錄table-of-contents)** 145 | 146 | ### 避免使用不必要的描述(Context) 147 | 如果你的類別與物件名稱是有關聯意義的,就不用在內部變數上再次重複。 148 | 149 | **糟糕的:** 150 | ```javascript 151 | const Car = { 152 | carMake: 'Honda', 153 | carModel: 'Accord', 154 | carColor: 'Blue' 155 | }; 156 | 157 | function paintCar(car, color) { 158 | car.carColor = color; 159 | } 160 | ``` 161 | 162 | **適當的:** 163 | ```javascript 164 | const Car = { 165 | make: 'Honda', 166 | model: 'Accord', 167 | color: 'Blue' 168 | }; 169 | 170 | function paintCar(color) { 171 | car.color = color; 172 | } 173 | ``` 174 | 175 | **[⬆ 回到目錄](#目錄table-of-contents)** 176 | 177 | ### 使用預設參數(Parameter)代替條件判斷(Conditionals) 178 | 使用預設參數較整潔,但請注意,當參數為 `undefined` 時才會作用,其他種類的虛值(Falsy)不會,像是 `''`、`false`、`null`、`0`、`NaN` 等。 179 | 180 | **糟糕的:** 181 | ```javascript 182 | function createMicrobrewery(name) { 183 | const breweryName = name || 'Hipster Brew Co.'; 184 | // ... 185 | } 186 | ``` 187 | 188 | **適當的:** 189 | ```javascript 190 | function createMicrobrewery(name = 'Hipster Brew Co.') { 191 | // ... 192 | } 193 | ``` 194 | 195 | **譯者附註** 196 | 197 | 預設參數非常好用,可以結合工廠模式做出很多應用。另外建議統一使用 `undefined` 代替 `null` 當作空值的回傳值。 198 | 199 | 此處原文為 **Arguments**,但是函數的參數定義應該為 **Parameter**,而呼叫函數時傳遞的引數才是 **Arguments**。 200 | 201 | **[⬆ 回到目錄](#目錄table-of-contents)** 202 | 203 | ## 函數(Functions) 204 | ### 參數(Parameter) (少於 2 個較佳) 205 | 限制函數的參數數量非常重要,因為能讓你更容易地測試。過多的參數代表著過多的組合,會導致你不得不編寫出大量測試。 206 | 207 | 一個至二個是最理想的,盡可能避免大於三個以上。如果你有超過兩個以上的參數,代表你的函數做太多事情。如果無法避免時,可以有效地使用物件替代大量的參數。 208 | 209 | 為了讓你可以清晰地表達,預期使用哪些物件的屬性(Properties),可以使用 ES2015/ES6 提供的解構(Destructuring)語法。使用這種語法有以下優點: 210 | 1. 函數需要物件的哪些屬性,可以像參數一樣清晰地表達。 211 | 2. 解構語法會複製來自物件的原始型態(Primitive),這能幫助你避免副作用(Side Effect)。注意!巢狀物件與陣列,並不會被解構語法複製。 212 | 3. 使用解構語法能讓物件屬性被程式碼檢查器(Linter)作用,提醒你哪些屬性未被使用到。 213 | 214 | **糟糕的:** 215 | ```javascript 216 | function createMenu(title, body, buttonText, cancellable) { 217 | // ... 218 | } 219 | ``` 220 | 221 | **適當的:** 222 | ```javascript 223 | function createMenu({ title, body, buttonText, cancellable }) { 224 | // ... 225 | } 226 | 227 | createMenu({ 228 | title: 'Foo', 229 | body: 'Bar', 230 | buttonText: 'Baz', 231 | cancellable: true 232 | }); 233 | ``` 234 | 235 | **譯者附註** 236 | 237 | 這種方法非常適合用於工廠模式,結合上一章的預設參數,譯者推薦的使用方式如下: 238 | ```javascript 239 | function createMenu({ 240 | title = 'Default Title', // 傳遞的物件不齊全,使用預設屬性 241 | body = '', 242 | buttonText = 'My Button', 243 | cancellable = true 244 | } = {}) { // 如未傳遞任何參數使用預設空物件,使用 `= {}` 可避免 TypeError: Cannot destructure property `...` of 'undefined' or 'null'. 245 | return { 246 | title, 247 | body, 248 | buttonText, 249 | cancellable 250 | } 251 | } 252 | 253 | const myMenu = createMenu({ 254 | title: 'Foo', 255 | body: 'Bar', 256 | buttonText: 'Baz', 257 | cancellable: true 258 | }); 259 | ``` 260 | 261 | 想了解更多工廠模式與 ES6 的結合,可參考連結文章:[JavaScript Factory Functions with ES6+](https://medium.com/javascript-scene/javascript-factory-functions-with-es6-4d224591a8b1)。 262 | 263 | **[⬆ 回到目錄](#目錄table-of-contents)** 264 | 265 | ### 一個函數只做一件事情(單一性) 266 | 這是個非常重要的原則,當你的函數做超過一件事情時,它會更難以被撰寫、測試與理解。當你隔離(Isolate)你的函數到只做一件事情時,它能更容易地被重構(Refactor)與清晰地閱讀。如果嚴格遵守此項原則,你將會領先許多開發者。 267 | 268 | **糟糕的:** 269 | ```javascript 270 | function emailClients(clients) { 271 | clients.forEach(client => { 272 | const clientRecord = database.lookup(client); 273 | if (clientRecord.isActive()) { 274 | email(client); 275 | } 276 | }); 277 | } 278 | ``` 279 | 280 | **適當的:** 281 | ```javascript 282 | function emailActiveClients(clients) { 283 | clients.filter(isActiveClient).forEach(email); 284 | } 285 | 286 | function isActiveClient(client) { 287 | const clientRecord = database.lookup(client); 288 | return clientRecord.isActive(); 289 | } 290 | ``` 291 | 292 | **[⬆ 回到目錄](#目錄table-of-contents)** 293 | 294 | ### 函數名稱應該說明它做的內容 295 | **糟糕的:** 296 | ```javascript 297 | function addToDate(date, month) { 298 | // ... 299 | } 300 | 301 | const date = new Date(); 302 | 303 | // 難以從函數名稱看出到底加入了什麼 304 | addToDate(date, 1); 305 | ``` 306 | 307 | **適當的:** 308 | ```javascript 309 | function addMonthToDate(month, date) { 310 | // ... 311 | } 312 | 313 | const date = new Date(); 314 | addMonthToDate(1, date); 315 | ``` 316 | 317 | **譯者附註** 318 | 319 | 建議函數命名以動詞開頭,像是 `doSomething()`、`setupUserProfile()`。 320 | 321 | **[⬆ 回到目錄](#目錄table-of-contents)** 322 | 323 | ### 函數應該只做一層抽象(Abstraction) 324 | 當你的函數需要的抽象多於一層時,代表你的函數做太多事情了。將其分解以利重用與測試。 325 | 326 | **糟糕的:** 327 | ```javascript 328 | function parseBetterJSAlternative(code) { 329 | const REGEXES = [ 330 | // ... 331 | ]; 332 | 333 | const statements = code.split(' '); 334 | const tokens = []; 335 | REGEXES.forEach(REGEX => { 336 | statements.forEach(statement => { 337 | // ... 338 | }); 339 | }); 340 | 341 | const ast = []; 342 | tokens.forEach(token => { 343 | // lex... 344 | }); 345 | 346 | ast.forEach(node => { 347 | // parse... 348 | }); 349 | } 350 | ``` 351 | 352 | **適當的:** 353 | ```javascript 354 | function parseBetterJSAlternative(code) { 355 | const tokens = tokenize(code); 356 | const syntaxTree = parse(tokens); 357 | syntaxTree.forEach(node => { 358 | // parse... 359 | }); 360 | } 361 | 362 | function tokenize(code) { 363 | const REGEXES = [ 364 | // ... 365 | ]; 366 | 367 | const statements = code.split(' '); 368 | const tokens = []; 369 | REGEXES.forEach(REGEX => { 370 | statements.forEach(statement => { 371 | tokens.push(/* ... */); 372 | }); 373 | }); 374 | 375 | return tokens; 376 | } 377 | 378 | function parse(tokens) { 379 | const syntaxTree = []; 380 | tokens.forEach(token => { 381 | syntaxTree.push(/* ... */); 382 | }); 383 | 384 | return syntaxTree; 385 | } 386 | ``` 387 | 388 | **譯者附註** 389 | 390 | 這個原則與前面提到的「一個函數只做一件事情(單一性)」概念相似。 391 | 392 | **[⬆ 回到目錄](#目錄table-of-contents)** 393 | 394 | ### 移除重複的(Duplicate)程式碼 395 | 絕對避免重複的程式碼,重複的程式碼代表著更動邏輯時,需要同時修改多處。 396 | 397 | 想像一下你經營著一家餐廳,需要持續追蹤存貨:番茄、洋蔥、大蒜與各種香料等。如果你有多份紀錄表,當使用蕃茄做完一道料理時,需要更新多份記錄表,可能會忘記更新其中一份。如果你只有一份記錄表,就不會有此問題! 398 | 399 | 通常重複的程式碼,是因為有兩個稍微不同的東西。它們之間絕大部分相同,但些微不同之處,迫使你使用多個函數處理相似的事情。如果出現重複的程式碼,可以使用函數、模組或是類別來抽象化處理。 400 | 401 | 正確的抽象化是非常關鍵的,這也是為什麼你應該遵循[類別(Classes)](#類別Classes)章節中,物件導向基本原則 SOLID 的原因。請小心,較差的抽象化會比重複的程式碼更糟!這麼說吧,如果你有把握做出好的抽象化,盡情放手去做。別讓程式碼出現重複的地方,不然你會需要修改更多的程式碼。 402 | 403 | **糟糕的:** 404 | ```javascript 405 | function showDeveloperList(developers) { 406 | developers.forEach(developer => { 407 | const expectedSalary = developer.calculateExpectedSalary(); 408 | const experience = developer.getExperience(); 409 | const githubLink = developer.getGithubLink(); 410 | const data = { 411 | expectedSalary, 412 | experience, 413 | githubLink 414 | }; 415 | 416 | render(data); 417 | }); 418 | } 419 | 420 | function showManagerList(managers) { 421 | managers.forEach(manager => { 422 | const expectedSalary = manager.calculateExpectedSalary(); 423 | const experience = manager.getExperience(); 424 | const portfolio = manager.getMBAProjects(); 425 | const data = { 426 | expectedSalary, 427 | experience, 428 | portfolio 429 | }; 430 | 431 | render(data); 432 | }); 433 | } 434 | ``` 435 | 436 | **適當的:** 437 | ```javascript 438 | function showEmployeeList(employees) { 439 | employees.forEach(employee => { 440 | const expectedSalary = employee.calculateExpectedSalary(); 441 | const experience = employee.getExperience(); 442 | 443 | const data = { 444 | expectedSalary, 445 | experience 446 | }; 447 | 448 | switch (employee.type) { 449 | case 'manager': 450 | data.portfolio = employee.getMBAProjects(); 451 | break; 452 | case 'developer': 453 | data.githubLink = employee.getGithubLink(); 454 | break; 455 | } 456 | 457 | render(data); 458 | }); 459 | } 460 | ``` 461 | 462 | **譯者附註** 463 | 464 | 剛讀完這個原則時,我非常遵守,但是個人龜毛的個性,造成了不少麻煩,我會在開發時不斷的思考是否會出現了重複的程式碼,甚至考慮到了之後的重用性。代價就是過度設計(Over Engineering)造成功能開發窒礙難行,設計了良好架構,但實際上並未被使用到。最後總結了一個建議作為附加原則:一開始撰寫程式碼先以功能開發優先,當你發現有兩個以上的地方重複時,再來考慮要不要重構。 465 | 466 | **[⬆ 回到目錄](#目錄table-of-contents)** 467 | 468 | ### 使用 `Object.assign` 設定 `Object` 的預設值 469 | 470 | **糟糕的:** 471 | ```javascript 472 | const menuConfig = { 473 | title: null, 474 | body: 'Bar', 475 | buttonText: null, 476 | cancellable: true 477 | }; 478 | 479 | function createMenu(config) { 480 | config.title = config.title || 'Foo'; 481 | config.body = config.body || 'Bar'; 482 | config.buttonText = config.buttonText || 'Baz'; 483 | config.cancellable = 484 | config.cancellable !== undefined ? config.cancellable : true; 485 | } 486 | 487 | createMenu(menuConfig); 488 | ``` 489 | 490 | **適當的:** 491 | ```javascript 492 | const menuConfig = { 493 | title: 'Order', 494 | // 使用者漏掉了 'body' 495 | buttonText: 'Send', 496 | cancellable: true 497 | }; 498 | 499 | function createMenu(config) { 500 | let finalConfig = Object.assign( 501 | { 502 | title: 'Foo', 503 | body: 'Bar', 504 | buttonText: 'Baz', 505 | cancellable: true 506 | }, 507 | config 508 | ); 509 | return finalConfig; 510 | // config 現在等同於: {title: 'Order', body: 'Bar', buttonText: 'Send', cancellable: true} 511 | // ... 512 | } 513 | 514 | createMenu(menuConfig); 515 | ``` 516 | 517 | **[⬆ 回到目錄](#目錄table-of-contents)** 518 | 519 | ### 不要使用旗標(Flag)作為參數 520 | 當你的函數使用了旗標當作參數時,代表函數做不只一件事情,依照不同旗標路徑切分你的函數。 521 | 522 | **糟糕的:** 523 | ```javascript 524 | function createFile(name, temp) { 525 | if (temp) { 526 | fs.create(`./temp/${name}`); 527 | } else { 528 | fs.create(name); 529 | } 530 | } 531 | ``` 532 | 533 | **適當的:** 534 | ```javascript 535 | function createFile(name) { 536 | fs.create(name); 537 | } 538 | 539 | function createTempFile(name) { 540 | createFile(`./temp/${name}`); 541 | } 542 | ``` 543 | 544 | **[⬆ 回到目錄](#目錄table-of-contents)** 545 | 546 | ### 避免副作用(Side Effects) 547 | 當函數作用在除了回傳值外的地方,像是讀寫文件、修改全域變數或是將你的錢轉帳到其他人帳戶,則稱為副作用。 548 | 549 | 程式在某些情況下是需要副作用的,像是上面所提到的例子。這時你應該將這些功能集中在一起,不要同時有多個函數或是類別同時操作資源,應該只用一個服務(Service)完成這些事情。 550 | 551 | 常見的問題像是: 552 | * 在沒有任何架構下,同時多個物件中分享共有狀態。 553 | * 可變的狀態,且可以被任何人寫入 554 | * 副作用發生的地方沒有被集中。 555 | 556 | 如果你能避免這些問題,你會比大多數的工程師快樂。 557 | 558 | **糟糕的:** 559 | ```javascript 560 | // 全域變數被以下函數使用 561 | // 假如有其他的函數使用了這個名稱,現在他變成了陣列,將會被破壞而出錯。 562 | let name = 'Ryan McDermott'; 563 | 564 | function splitIntoFirstAndLastName() { 565 | name = name.split(' '); 566 | } 567 | 568 | splitIntoFirstAndLastName(); 569 | 570 | console.log(name); // ['Ryan', 'McDermott']; 571 | ``` 572 | 573 | **適當的:** 574 | ```javascript 575 | function splitIntoFirstAndLastName(name) { 576 | return name.split(' '); 577 | } 578 | 579 | const name = 'Ryan McDermott'; 580 | const newName = splitIntoFirstAndLastName(name); 581 | 582 | console.log(name); // 'Ryan McDermott'; 583 | console.log(newName); // ['Ryan', 'McDermott']; 584 | ``` 585 | 586 | **[⬆ 回到目錄](#目錄table-of-contents)** 587 | 588 | ### 避免副作用(Side Effects)第二部分 589 | 在 JavaScript 中,原始資料類型傳遞數值(Value),物件/陣列傳遞參照(Reference)。在本案例中,你的函數改變了購物車清單 `cart` 中的陣列,像是你增加了一個商品,其他使用購物車清單的函數將會被影響。這做法有好有壞,讓我解釋一下問題所在: 590 | 591 | 使用者按下付款按鈕後,將會呼叫 `purchase` 函數,產生一個網路請求傳送購物車清單陣列到伺服器。因為較差的網路連線,必須多嘗試幾次。此時使用者不小心又按下加入購物車按鈕,因為是參考的關係,請求將會送出新加入的商品。 592 | 593 | 較好的解決辦法是 `addItemToCart` 函數,執行前複製新的一份購物車清單,修改複製的資料後再回傳。這能確保其他的函數的購物車清單沒有任何機會被被參考所影響。 594 | 595 | 使用這方法前,有兩個警告要告知: 596 | 1. 當採用這種做法後,你會發現,需要修改輸入物件的情況非常少。大多數的程式碼可以在沒有副作用的情況下重構! 597 | 598 | 2. 複製大型物件,需要花費高昂的效能。幸好,我們有好的[函數庫](https://immutable-js.github.io/immutable-js/),可以提升複製物件與陣列的速度與減少記憶體使用。 599 | 600 | **糟糕的:** 601 | ```javascript 602 | const addItemToCart = (cart, item) => { 603 | cart.push({ item, date: Date.now() }); 604 | }; 605 | ``` 606 | 607 | **適當的:** 608 | ```javascript 609 | const addItemToCart = (cart, item) => { 610 | return [...cart, { item, date: Date.now() }]; 611 | }; 612 | ``` 613 | 614 | **譯者附註** 615 | 616 | 譯者這邊另外再提供一個案例,函數 `checkIs18Age` 用來檢查是否成年。第一個寫法中,引用了全域(global)變數 `minimum`,這功能看起來能正常運作沒問題,但是今天如果有其他函數 `setMinAge` 修改了全域變數 `minimum`,函數 `checkIs18Age` 將會因為副作用的關係變的無法預期,甚至失去它的作用。 617 | 618 | 較好的寫法是,採用純函數(pure function),使用一個可預期的變數,來避免副作用影響。 619 | 620 | ```javascript 621 | let minimum = 18 622 | 623 | // impure with side effect 624 | const checkIs18Age = age => age >= minimum 625 | const setMinAge = age => minimum = age 626 | 627 | // pure 628 | const checkIs18Age = age => { 629 | let minimum = 18 630 | return age >= minimum 631 | } 632 | ``` 633 | 634 | 另外使用解構的方式複製資料,只會複製第一層而已。 635 | 636 | ```javascript 637 | // 巢狀解構只能複製第一層 638 | const obj1 = { subObj: { message: 'Hey' } } 639 | const obj2 = { ...obj1 } 640 | 641 | obj2.subObj.message = 'Yo' 642 | console.log(obj1.subObj.message) // 'Yo' 643 | ``` 644 | 645 | 如果要複製巢狀結構,你需要深度複製。可以使用一些函數庫 [loadsh](https://lodash.com/) 的 `_.CloneDeep` 或是 [ramda](https://ramdajs.com/) 的 `clone`。或是使用 `JSON.parse(JSON.stringify(object))` 來實現。不過使用 JSON 的話,會失去 Function 的複製。 646 | 647 | ```javascript 648 | // 使用 JSON 來深度複製 649 | const obj1 = { subObj: { message: 'Hey' } } 650 | const obj2 = JSON.parse(JSON.stringify(obj1)) 651 | 652 | obj2.subObj.message = 'Yo' 653 | console.log(obj1.subObj.message) // 'Hey' 654 | ``` 655 | 656 | **[⬆ 回到目錄](#目錄table-of-contents)** 657 | 658 | ### 別寫全域函數(Global Function) 659 | 在 JavaScript 中弄髒全域是個不好的做法,因為你可能會影響到其他函數庫或是 API。舉個例子,如果妳想要在 JavaScript 的原生陣列方法,擴展 `diff` 方法,用 B 陣列來去除 A 陣列中的元素(Element)。常見做法你可能會在 `Array.prototype` 中增加一個全新的函數,如果其他函數庫也有自己的 `diff` 實現的話將會互相影響。這就是為什麼我們需要使用 ES2015/ES6 的類別,來擴展的原因。 660 | 661 | **糟糕的:** 662 | ```javascript 663 | Array.prototype.diff = function diff(comparisonArray) { 664 | const hash = new Set(comparisonArray); 665 | return this.filter(elem => !hash.has(elem)); 666 | }; 667 | ``` 668 | 669 | **適當的:** 670 | ```javascript 671 | class SuperArray extends Array { 672 | diff(comparisonArray) { 673 | const hash = new Set(comparisonArray); 674 | return this.filter(elem => !hash.has(elem)); 675 | } 676 | } 677 | ``` 678 | 679 | **[⬆ 回到目錄](#目錄table-of-contents)** 680 | 681 | ### 偏好使用函數式程式(Functional Programming)設計代替命令式程式設計(Imperative Programming) 682 | JavaScript 不是像 Haskell 一樣的函數式語言,但它具有類似特性。函數式程式設計更加乾淨且容易被測試。當你在寫程式時,盡量選擇此設計方式。 683 | 684 | **糟糕的:** 685 | ```javascript 686 | const programmerOutput = [ 687 | { 688 | name: 'Uncle Bobby', 689 | linesOfCode: 500 690 | }, 691 | { 692 | name: 'Suzie Q', 693 | linesOfCode: 1500 694 | }, 695 | { 696 | name: 'Jimmy Gosling', 697 | linesOfCode: 150 698 | }, 699 | { 700 | name: 'Gracie Hopper', 701 | linesOfCode: 1000 702 | } 703 | ]; 704 | 705 | let totalOutput = 0; 706 | 707 | for (let i = 0; i < programmerOutput.length; i++) { 708 | totalOutput += programmerOutput[i].linesOfCode; 709 | } 710 | ``` 711 | 712 | **適當的:** 713 | ```javascript 714 | const programmerOutput = [ 715 | { 716 | name: 'Uncle Bobby', 717 | linesOfCode: 500 718 | }, 719 | { 720 | name: 'Suzie Q', 721 | linesOfCode: 1500 722 | }, 723 | { 724 | name: 'Jimmy Gosling', 725 | linesOfCode: 150 726 | }, 727 | { 728 | name: 'Gracie Hopper', 729 | linesOfCode: 1000 730 | } 731 | ]; 732 | 733 | const totalOutput = programmerOutput.reduce( 734 | (totalLines, output) => totalLines + output.linesOfCode, 735 | 0 736 | ); 737 | ``` 738 | 739 | **[⬆ 回到目錄](#目錄table-of-contents)** 740 | 741 | ### 封裝狀態(Encapsulate Conditionals) 742 | 743 | **糟糕的:** 744 | ```javascript 745 | if (fsm.state === 'fetching' && isEmpty(listNode)) { 746 | // ... 747 | } 748 | ``` 749 | 750 | **適當的:** 751 | ```javascript 752 | function shouldShowSpinner(fsm, listNode) { 753 | return fsm.state === 'fetching' && isEmpty(listNode); 754 | } 755 | 756 | if (shouldShowSpinner(fsmInstance, listNodeInstance)) { 757 | // ... 758 | } 759 | ``` 760 | 761 | **[⬆ 回到目錄](#目錄table-of-contents)** 762 | 763 | ### 避免負面狀態(Negative Conditionals) 764 | 765 | **糟糕的:** 766 | ```javascript 767 | function isDOMNodeNotPresent(node) { 768 | // ... 769 | } 770 | 771 | if (!isDOMNodeNotPresent(node)) { 772 | // ... 773 | } 774 | ``` 775 | 776 | **適當的:** 777 | ```javascript 778 | function isDOMNodePresent(node) { 779 | // ... 780 | } 781 | 782 | if (isDOMNodePresent(node)) { 783 | // ... 784 | } 785 | ``` 786 | 787 | **[⬆ 回到目錄](#目錄table-of-contents)** 788 | 789 | ### 避免狀態 790 | 當你第一次聽到時,這聽起來是不可能的任務。大部分的人會說:「怎麼可能不使用 `if` 語法?」事實上你可以使用多態性(Polymorphism) 達到相同的效果。第二個問題來了,「為什麼我們需要這樣做呢?」依據前面概念,為了保持程式碼的乾淨,當類別或是函數出現 `if` 語法,代表你的函數做了超過一件事情。記住,一個函數只做一件事情! 791 | 792 | **糟糕的:** 793 | ```javascript 794 | class Airplane { 795 | // ... 796 | getCruisingAltitude() { 797 | switch (this.type) { 798 | case '777': 799 | return this.getMaxAltitude() - this.getPassengerCount(); 800 | case 'Air Force One': 801 | return this.getMaxAltitude(); 802 | case 'Cessna': 803 | return this.getMaxAltitude() - this.getFuelExpenditure(); 804 | } 805 | } 806 | } 807 | ``` 808 | 809 | **適當的:** 810 | ```javascript 811 | class Airplane { 812 | // ... 813 | } 814 | 815 | class Boeing777 extends Airplane { 816 | // ... 817 | getCruisingAltitude() { 818 | return this.getMaxAltitude() - this.getPassengerCount(); 819 | } 820 | } 821 | 822 | class AirForceOne extends Airplane { 823 | // ... 824 | getCruisingAltitude() { 825 | return this.getMaxAltitude(); 826 | } 827 | } 828 | 829 | class Cessna extends Airplane { 830 | // ... 831 | getCruisingAltitude() { 832 | return this.getMaxAltitude() - this.getFuelExpenditure(); 833 | } 834 | } 835 | ``` 836 | 837 | **[⬆ 回到目錄](#目錄table-of-contents)** 838 | 839 | ### 避免型別(Type)檢查:第一部分 840 | JavaScript 為弱型別語言,代表函數應能處理任何型別的引數(argument)。有時這會帶給你一些麻煩,讓你需要做型別檢查。有很多方法可以避免這種問題發生,第一種就是統一所有的 API。 841 | 842 | **糟糕的:** 843 | ```javascript 844 | function travelToTexas(vehicle) { 845 | if (vehicle instanceof Bicycle) { 846 | vehicle.pedal(this.currentLocation, new Location('texas')); 847 | } else if (vehicle instanceof Car) { 848 | vehicle.drive(this.currentLocation, new Location('texas')); 849 | } 850 | } 851 | ``` 852 | 853 | **適當的:** 854 | ```javascript 855 | function travelToTexas(vehicle) { 856 | vehicle.move(this.currentLocation, new Location('texas')); 857 | } 858 | ``` 859 | 860 | **譯者附註** 861 | 862 | 此範例統一了所有的車輛移動的參數、方法與實作,所以不再需要區分不同的類別的車輛呼叫不同的方法。 863 | 864 | **[⬆ 回到目錄](#目錄table-of-contents)** 865 | 866 | ### 避免型別檢查:第二部分 867 | 假設你需要型別檢查原始數值,像是字串與整數且你無法使用多態性處理,考慮使用 TypeScript 吧。他是提供標準 JavaScript 靜態類型的的最佳選擇。手動型別檢查需要很多額外處理,你得到的是虛假的型別安全,且失去的可讀性。保持你的 JavaScript 程式碼的整潔、寫好測試與足夠的程式碼審查(Code Review)。如果加上使用 TypeScript 會是更好的選擇。 868 | 869 | **糟糕的:** 870 | ```javascript 871 | function combine(val1, val2) { 872 | if ( 873 | (typeof val1 === 'number' && typeof val2 === 'number') || 874 | (typeof val1 === 'string' && typeof val2 === 'string') 875 | ) { 876 | return val1 + val2; 877 | } 878 | 879 | throw new Error('Must be of type String or Number'); 880 | } 881 | ``` 882 | 883 | **適當的:** 884 | ```javascript 885 | function combine(val1, val2) { 886 | return val1 + val2; 887 | } 888 | ``` 889 | 890 | **[⬆ 回到目錄](#目錄table-of-contents)** 891 | 892 | ### 別過度優化 893 | 現代瀏覽器在運行時幫你做了很多優化。大多數的情況,你所做的優化都是在浪費你的時間。這裏有些很好的[資源](https://github.com/petkaantonov/bluebird/wiki/Optimization-killers),去了解哪些優化是無用的。 894 | 895 | **糟糕的:** 896 | ```javascript 897 | // 在舊的瀏覽器中並不會快取(cache)`list.length`,每次迭代(iteration)時的重新計算相當損耗效能。 898 | // 這在新瀏覽器中已被優化,你不用手動去快取。 899 | for (let i = 0, len = list.length; i < len; i++) { 900 | // ... 901 | } 902 | ``` 903 | 904 | **適當的:** 905 | ```javascript 906 | for (let i = 0; i < list.length; i++) { 907 | // ... 908 | } 909 | ``` 910 | 911 | **譯者附註** 912 | 913 | 簡單來說,你不用在意程式語言層面上優化,因為這部分會因為版本更新而得到優化。但不要因此放棄所有的優化,演算法的優化才是你該注意的地方! 914 | 915 | **[⬆ 回到目錄](#目錄table-of-contents)** 916 | 917 | ### 移除無用的程式碼(Dead Code) 918 | 沒有任何理由保留無用的程式碼,如果他們沒有被使用到,移除它!讓它們被保留在版本歷史中。 919 | 920 | **糟糕的:** 921 | ```javascript 922 | function oldRequestModule(url) { 923 | // ... 924 | } 925 | 926 | function newRequestModule(url) { 927 | // ... 928 | } 929 | 930 | const req = newRequestModule; 931 | inventoryTracker('apples', req, 'www.inventory-awesome.io'); 932 | ``` 933 | 934 | **適當的:** 935 | ```javascript 936 | function newRequestModule(url) { 937 | // ... 938 | } 939 | 940 | const req = newRequestModule; 941 | inventoryTracker('apples', req, 'www.inventory-awesome.io'); 942 | ``` 943 | 944 | **[⬆ 回到目錄](#目錄table-of-contents)** 945 | 946 | ## 物件(Objects)與資料結構(Data Structures) 947 | ### 使用 getters 與 setters 948 | 使用 `getters` 與 `setters` 來存取物件中資料,會比單純使用屬性(property)來的好。因為: 949 | * 當你想要在取得物件屬性時做更多事情,你不用找出所有的程式碼修改。 950 | * 透過 `set` 可以建立規則進行資料校驗。 951 | * 封裝內部邏輯。 952 | * 存取時增加日誌(logging)與錯誤處理(error handling)。 953 | * 你可以延遲載入你的物件屬性,像是來自伺服器的資料。 954 | 955 | **糟糕的:** 956 | ```javascript 957 | function makeBankAccount() { 958 | // ... 959 | 960 | return { 961 | balance: 0 962 | // ... 963 | }; 964 | } 965 | 966 | const account = makeBankAccount(); 967 | account.balance = 100; 968 | ``` 969 | 970 | **適當的:** 971 | ```javascript 972 | function makeBankAccount() { 973 | // 私有變數 974 | let balance = 0; 975 | 976 | // 'getter',經由下方的返回物件對外公開 977 | function getBalance() { 978 | return balance; 979 | } 980 | 981 | // 'setter',經由下方的返回物件對外公開 982 | function setBalance(amount) { 983 | // ... 更新前先進行驗證 984 | balance = amount; 985 | } 986 | 987 | return { 988 | // ... 989 | getBalance, 990 | setBalance 991 | }; 992 | } 993 | 994 | const account = makeBankAccount(); 995 | account.setBalance(100); 996 | ``` 997 | 998 | **[⬆ 回到目錄](#目錄table-of-contents)** 999 | 1000 | ### 讓物件擁有私有成員(members) 1001 | 可以透過閉包(closures)來私有化參數(使用於 ES5 以下)。 1002 | 1003 | **糟糕的:** 1004 | ```javascript 1005 | const Employee = function(name) { 1006 | this.name = name; 1007 | }; 1008 | 1009 | Employee.prototype.getName = function getName() { 1010 | return this.name; 1011 | }; 1012 | 1013 | const employee = new Employee('John Doe'); 1014 | console.log(`Employee name: ${employee.getName()}`); // Employee name: John Doe 1015 | delete employee.name; 1016 | console.log(`Employee name: ${employee.getName()}`); // Employee name: undefined 1017 | ``` 1018 | 1019 | **適當的:** 1020 | ```javascript 1021 | function makeEmployee(name) { 1022 | return { 1023 | getName() { 1024 | return name; 1025 | } 1026 | }; 1027 | } 1028 | 1029 | const employee = makeEmployee('John Doe'); 1030 | console.log(`Employee name: ${employee.getName()}`); // Employee name: John Doe 1031 | delete employee.name; 1032 | console.log(`Employee name: ${employee.getName()}`); // Employee name: John Doe 1033 | ``` 1034 | 1035 | **[⬆ 回到目錄](#目錄table-of-contents)** 1036 | 1037 | ## 類別(Classes) 1038 | ### 類別語法偏好使用 ES2015/ES6 的類別更甚於 ES5 函數 1039 | ES5 的類別定義非常難以閱讀、繼承、建造與定義方法。假設你需要繼承(請注意你有可能不需要),偏好使用 ES2015/ES6 的類別。除非你需要大型且複雜的物件,不然使用小型函數會比類別更好。 1040 | 1041 | **糟糕的:** 1042 | ```javascript 1043 | const Animal = function(age) { 1044 | if (!(this instanceof Animal)) { 1045 | throw new Error('Instantiate Animal with `new`'); 1046 | } 1047 | 1048 | this.age = age; 1049 | }; 1050 | 1051 | Animal.prototype.move = function move() {}; 1052 | 1053 | const Mammal = function(age, furColor) { 1054 | if (!(this instanceof Mammal)) { 1055 | throw new Error('Instantiate Mammal with `new`'); 1056 | } 1057 | 1058 | Animal.call(this, age); 1059 | this.furColor = furColor; 1060 | }; 1061 | 1062 | Mammal.prototype = Object.create(Animal.prototype); 1063 | Mammal.prototype.constructor = Mammal; 1064 | Mammal.prototype.liveBirth = function liveBirth() {}; 1065 | 1066 | const Human = function(age, furColor, languageSpoken) { 1067 | if (!(this instanceof Human)) { 1068 | throw new Error('Instantiate Human with `new`'); 1069 | } 1070 | 1071 | Mammal.call(this, age, furColor); 1072 | this.languageSpoken = languageSpoken; 1073 | }; 1074 | 1075 | Human.prototype = Object.create(Mammal.prototype); 1076 | Human.prototype.constructor = Human; 1077 | Human.prototype.speak = function speak() {}; 1078 | ``` 1079 | 1080 | **適當的:** 1081 | ```javascript 1082 | class Animal { 1083 | constructor(age) { 1084 | this.age = age; 1085 | } 1086 | 1087 | move() { 1088 | // ... 1089 | } 1090 | } 1091 | 1092 | class Mammal extends Animal { 1093 | constructor(age, furColor) { 1094 | super(age); 1095 | this.furColor = furColor; 1096 | } 1097 | 1098 | liveBirth() { 1099 | // ... 1100 | } 1101 | } 1102 | 1103 | class Human extends Mammal { 1104 | constructor(age, furColor, languageSpoken) { 1105 | super(age, furColor); 1106 | this.languageSpoken = languageSpoken; 1107 | } 1108 | 1109 | speak() { 1110 | // ... 1111 | } 1112 | } 1113 | ``` 1114 | 1115 | **[⬆ 回到目錄](#目錄table-of-contents)** 1116 | 1117 | ### 使用方法鏈(method chaining) 1118 | 這個模式(pattern)在 JavaScript 中非常有用,你可以在很多函數庫中看到,像是 jQuery 與 Lodash。它可以讓你的程式碼表達的更好。 1119 | 1120 | 基於這個原因,方法鏈可以讓你的程式碼看起來更加簡潔。在你的類別函數中,只需要回傳 `this` 在每一個函數中,你就可以鏈結所有類別中的方法。 1121 | 1122 | **糟糕的:** 1123 | ```javascript 1124 | class Car { 1125 | constructor(make, model, color) { 1126 | this.make = make; 1127 | this.model = model; 1128 | this.color = color; 1129 | } 1130 | 1131 | setMake(make) { 1132 | this.make = make; 1133 | } 1134 | 1135 | setModel(model) { 1136 | this.model = model; 1137 | } 1138 | 1139 | setColor(color) { 1140 | this.color = color; 1141 | } 1142 | 1143 | save() { 1144 | console.log(this.make, this.model, this.color); 1145 | } 1146 | } 1147 | 1148 | const car = new Car('Ford', 'F-150', 'red'); 1149 | car.setColor('pink'); 1150 | car.save(); 1151 | ``` 1152 | 1153 | **適當的:** 1154 | ```javascript 1155 | class Car { 1156 | constructor(make, model, color) { 1157 | this.make = make; 1158 | this.model = model; 1159 | this.color = color; 1160 | } 1161 | 1162 | setMake(make) { 1163 | this.make = make; 1164 | // 注意:回傳 this 以鏈結 1165 | return this; 1166 | } 1167 | 1168 | setModel(model) { 1169 | this.model = model; 1170 | // 注意:回傳 this 以鏈結 1171 | return this; 1172 | } 1173 | 1174 | setColor(color) { 1175 | this.color = color; 1176 | // 注意:回傳 this 以鏈結 1177 | return this; 1178 | } 1179 | 1180 | save() { 1181 | console.log(this.make, this.model, this.color); 1182 | // 注意:回傳 this 以鏈結 1183 | return this; 1184 | } 1185 | } 1186 | 1187 | const car = new Car('Ford', 'F-150', 'red').setColor('pink').save(); 1188 | ``` 1189 | 1190 | **[⬆ 回到目錄](#目錄table-of-contents)** 1191 | 1192 | ### 偏好組合(composition)更甚於繼承(inheritance) 1193 | 正如四人幫的[設計模式](https://en.wikipedia.org/wiki/Design_Patterns),可以的話你應該優先使用組合而不是繼承。有許多好理由去使用繼承或是組合。重點是,如果你主觀認定是繼承,嘗試想一下組合能否替問題帶來更好的解法。你應該偏好使用組合更甚於繼承。 1194 | 1195 | 什麼時候使用繼承?這取決於你手上的問題,不過這有一些不錯的參考,說明什麼什麼時候繼承比組合更好用: 1196 | 1197 | 1. 你的繼承為是一種(is-a)的關係,而不是有一個(has-a)。例如「人類是一種動物 Human -> Animal」vs.「使用者有一個使用者資料 User -> UserDetails」。 1198 | 2. 你能重複使用基類(base classes)的程式碼。例如,人類能像動物一樣移動。 1199 | 3. 你希望能通過修改基類的程式碼來進行全域修改。例如,改變所有動物移動時的熱量消耗。 1200 | 1201 | **糟糕的:** 1202 | ```javascript 1203 | class Employee { 1204 | constructor(name, email) { 1205 | this.name = name; 1206 | this.email = email; 1207 | } 1208 | 1209 | // ... 1210 | } 1211 | 1212 | // 因為僱員有稅率金資料,而不是一種僱員 1213 | class EmployeeTaxData extends Employee { 1214 | constructor(ssn, salary) { 1215 | super(); 1216 | this.ssn = ssn; 1217 | this.salary = salary; 1218 | } 1219 | 1220 | // ... 1221 | } 1222 | ``` 1223 | 1224 | **適當的:** 1225 | ```javascript 1226 | class EmployeeTaxData { 1227 | constructor(ssn, salary) { 1228 | this.ssn = ssn; 1229 | this.salary = salary; 1230 | } 1231 | 1232 | // ... 1233 | } 1234 | 1235 | class Employee { 1236 | constructor(name, email) { 1237 | this.name = name; 1238 | this.email = email; 1239 | } 1240 | 1241 | setTaxData(ssn, salary) { 1242 | this.taxData = new EmployeeTaxData(ssn, salary); 1243 | } 1244 | // ... 1245 | } 1246 | ``` 1247 | 1248 | **[⬆ 回到目錄](#目錄table-of-contents)** 1249 | 1250 | ## 物件導向基本原則(SOLID) 1251 | ### 單一功能原則 Single Responsibility Principle (SRP) 1252 | 正如 Clean Code 所述:「永遠不要有超過一個理由來修改一個類型」。給一個類別塞滿許多功能,如同你在航班上只能帶一個行李箱一樣。這樣做的問題是,你的類別不會有理想的內聚性,將會有太多理由來對它進行修改。最小化需要修改一個類別的次數很重要,因為一個類別有太多的功能的話,一旦你修改一小部分,將會很難弄清楚它會對程式碼的其他模組造成什麼影響。 1253 | 1254 | **糟糕的:** 1255 | ```javascript 1256 | class UserSettings { 1257 | constructor(user) { 1258 | this.user = user; 1259 | } 1260 | 1261 | changeSettings(settings) { 1262 | if (this.verifyCredentials()) { 1263 | // ... 1264 | } 1265 | } 1266 | 1267 | verifyCredentials() { 1268 | // ... 1269 | } 1270 | } 1271 | ``` 1272 | 1273 | **適當的:** 1274 | ```javascript 1275 | class UserAuth { 1276 | constructor(user) { 1277 | this.user = user; 1278 | } 1279 | 1280 | verifyCredentials() { 1281 | // ... 1282 | } 1283 | } 1284 | 1285 | class UserSettings { 1286 | constructor(user) { 1287 | this.user = user; 1288 | this.auth = new UserAuth(user); 1289 | } 1290 | 1291 | changeSettings(settings) { 1292 | if (this.auth.verifyCredentials()) { 1293 | // ... 1294 | } 1295 | } 1296 | } 1297 | ``` 1298 | 1299 | **[⬆ 回到目錄](#目錄table-of-contents)** 1300 | 1301 | ### 開閉原則 Open/Closed Principle (OCP) 1302 | Bertrand Meyer 說過,「軟體實體(類別、模組、函數)應為開放擴展,但是關閉修改」。這原則基本上說明你應該同意使用者增加功能,而不用修改現有程式碼。 1303 | 1304 | **糟糕的:** 1305 | ```javascript 1306 | class AjaxAdapter extends Adapter { 1307 | constructor() { 1308 | super(); 1309 | this.name = 'ajaxAdapter'; 1310 | } 1311 | } 1312 | 1313 | class NodeAdapter extends Adapter { 1314 | constructor() { 1315 | super(); 1316 | this.name = 'nodeAdapter'; 1317 | } 1318 | } 1319 | 1320 | class HttpRequester { 1321 | constructor(adapter) { 1322 | this.adapter = adapter; 1323 | } 1324 | 1325 | fetch(url) { 1326 | if (this.adapter.name === 'ajaxAdapter') { 1327 | return makeAjaxCall(url).then(response => { 1328 | // 轉換回應並回傳 1329 | }); 1330 | } else if (this.adapter.name === 'nodeAdapter') { 1331 | return makeHttpCall(url).then(response => { 1332 | // 轉換回應並回傳 1333 | }); 1334 | } 1335 | } 1336 | } 1337 | 1338 | function makeAjaxCall(url) { 1339 | // 發送請求並回傳 promise 1340 | } 1341 | 1342 | function makeHttpCall(url) { 1343 | // 發送請求並回傳 promise 1344 | } 1345 | ``` 1346 | 1347 | **適當的:** 1348 | ```javascript 1349 | class AjaxAdapter extends Adapter { 1350 | constructor() { 1351 | super(); 1352 | this.name = 'ajaxAdapter'; 1353 | } 1354 | 1355 | request(url) { 1356 | // 發送請求並回傳 promise 1357 | } 1358 | } 1359 | 1360 | class NodeAdapter extends Adapter { 1361 | constructor() { 1362 | super(); 1363 | this.name = 'nodeAdapter'; 1364 | } 1365 | 1366 | request(url) { 1367 | // 發送請求並回傳 promise 1368 | } 1369 | } 1370 | 1371 | class HttpRequester { 1372 | constructor(adapter) { 1373 | this.adapter = adapter; 1374 | } 1375 | 1376 | fetch(url) { 1377 | return this.adapter.request(url).then(response => { 1378 | // transform response and return 1379 | }); 1380 | } 1381 | } 1382 | ``` 1383 | 1384 | **[⬆ 回到目錄](#目錄table-of-contents)** 1385 | 1386 | ### 里氏替換原則 Liskov Substitution Principle (LSP) 1387 | 這是一個驚人但簡單的概念,正式的定義為:「假如類別 S 是類別 T 的子類別,那麼類別 T 的物件(Object)可以被替換成類別 S 的物件(例如,類別 S 的物件可作為類別 T 的物件的替代品),而不需要改變任何程式的屬性(正確性、被執行的任務等)。 1388 | 1389 | 最好的解釋是這樣,假如你有父類別與子類別,父類別與子類別可以互換,而沒有問題發生。讓我們看看一個經典的正方形與長方形的案例。在數學上,正方形是長方形的一種,但如果你使用是一種(is-a)的關係用繼承來實現,你很快會發現問題。 1390 | 1391 | **糟糕的:** 1392 | ```javascript 1393 | class Rectangle { 1394 | constructor() { 1395 | this.width = 0; 1396 | this.height = 0; 1397 | } 1398 | 1399 | setColor(color) { 1400 | // ... 1401 | } 1402 | 1403 | render(area) { 1404 | // ... 1405 | } 1406 | 1407 | setWidth(width) { 1408 | this.width = width; 1409 | } 1410 | 1411 | setHeight(height) { 1412 | this.height = height; 1413 | } 1414 | 1415 | getArea() { 1416 | return this.width * this.height; 1417 | } 1418 | } 1419 | 1420 | class Square extends Rectangle { 1421 | setWidth(width) { 1422 | this.width = width; 1423 | this.height = width; 1424 | } 1425 | 1426 | setHeight(height) { 1427 | this.width = height; 1428 | this.height = height; 1429 | } 1430 | } 1431 | 1432 | function renderLargeRectangles(rectangles) { 1433 | rectangles.forEach(rectangle => { 1434 | rectangle.setWidth(4); 1435 | rectangle.setHeight(5); 1436 | const area = rectangle.getArea(); // 糟糕:結果為 25,應該為 20 才正確 1437 | rectangle.render(area); 1438 | }); 1439 | } 1440 | 1441 | const rectangles = [new Rectangle(), new Rectangle(), new Square()]; 1442 | renderLargeRectangles(rectangles); 1443 | ``` 1444 | 1445 | **適當的:** 1446 | ```javascript 1447 | class Shape { 1448 | setColor(color) { 1449 | // ... 1450 | } 1451 | 1452 | render(area) { 1453 | // ... 1454 | } 1455 | } 1456 | 1457 | class Rectangle extends Shape { 1458 | constructor(width, height) { 1459 | super(); 1460 | this.width = width; 1461 | this.height = height; 1462 | } 1463 | 1464 | getArea() { 1465 | return this.width * this.height; 1466 | } 1467 | } 1468 | 1469 | class Square extends Shape { 1470 | constructor(length) { 1471 | super(); 1472 | this.length = length; 1473 | } 1474 | 1475 | getArea() { 1476 | return this.length * this.length; 1477 | } 1478 | } 1479 | 1480 | function renderLargeShapes(shapes) { 1481 | shapes.forEach(shape => { 1482 | const area = shape.getArea(); 1483 | shape.render(area); 1484 | }); 1485 | } 1486 | 1487 | const shapes = [new Rectangle(4, 5), new Rectangle(4, 5), new Square(5)]; 1488 | renderLargeShapes(shapes); 1489 | ``` 1490 | 1491 | **譯者附註** 1492 | 1493 | `setWidth` 與 `setHeight` 方法無法被繼承,使用上的不一致,更造成了結果錯誤。 1494 | 1495 | **[⬆ 回到目錄](#目錄table-of-contents)** 1496 | 1497 | ### 介面隔離原則 Interface Segregation Principle (ISP) 1498 | JavaScript 沒有介面(interfaces),所以這個原則比較不像其他語言一樣嚴格。不過它在 JavaScript 這種缺少類型的語言來說一樣重要。 1499 | 1500 | ISP 原則是「客戶端不應該被強制依賴他們不需要的介面。」因為 JavaScript 是一種弱型別的語言,所以介面是一種隱式(implicit)的協議。 1501 | 1502 | 巨大的設定物件(objects)是這個原則的好範例,不需要客戶去設定大量的選項是有好處的,因為多數的情況下,他們不需要全部的設定。讓他們可以被選擇,可以防止出現一個過胖的介面。 1503 | 1504 | **糟糕的:** 1505 | ```javascript 1506 | class DOMTraverser { 1507 | constructor(settings) { 1508 | this.settings = settings; 1509 | this.setup(); 1510 | } 1511 | 1512 | setup() { 1513 | this.rootNode = this.settings.rootNode; 1514 | this.animationModule.setup(); 1515 | } 1516 | 1517 | traverse() { 1518 | // ... 1519 | } 1520 | } 1521 | 1522 | const $ = new DOMTraverser({ 1523 | rootNode: document.getElementsByTagName('body'), 1524 | animationModule() {} // 大多數的情況下,執行 traverse 時,我們其實不需要使用 animation。 1525 | // ... 1526 | }); 1527 | ``` 1528 | 1529 | **適當的:** 1530 | ```javascript 1531 | class DOMTraverser { 1532 | constructor(settings) { 1533 | this.settings = settings; 1534 | this.options = settings.options; 1535 | this.setup(); 1536 | } 1537 | 1538 | setup() { 1539 | this.rootNode = this.settings.rootNode; 1540 | this.setupOptions(); 1541 | } 1542 | 1543 | setupOptions() { 1544 | if (this.options.animationModule) { 1545 | // ... 1546 | } 1547 | } 1548 | 1549 | traverse() { 1550 | // ... 1551 | } 1552 | } 1553 | 1554 | const $ = new DOMTraverser({ 1555 | rootNode: document.getElementsByTagName('body'), 1556 | options: { // animationModule 可被選擇要不要使用 1557 | animationModule() {} 1558 | } 1559 | }); 1560 | ``` 1561 | 1562 | **[⬆ 回到目錄](#目錄table-of-contents)** 1563 | 1564 | ### 依賴反轉原則 Dependency Inversion Principle (DIP) 1565 | 這原則說明兩個必要事情: 1566 | 1. 高層級的模組(modules)不應該依賴於低層級的模組。它們兩者必須依賴於抽象。 1567 | 2. 抽象(abstract)不應該依賴於具體實現(implement),具體實現則應依賴於抽象。 1568 | 1569 | 這原則一開始很難理解,但如果你使用過 AngularJS,你應該已經知道,使用依賴注入(Dependency Injection)來實現這個原則。雖然不是同一種概念,但透過依賴注入讓高層級模組遠離低層級模組的細節與設定。這樣做的巨大好處是,降低模組間的耦合。耦合是很糟的開發模式,因為會導致程式碼難以重構(refactor)。 1570 | 1571 | 如上所示,JavaScript 沒有介面,所以被依賴的抽象是隱式協議。也是就說,一個物件/類別的屬性直接暴露給另外一個。在以下的範例中,任何的請求模組(Request Module)的隱式協議 `InventoryTracker` 都會有一個 `requestItems` 的方法。 1572 | 1573 | **糟糕的:** 1574 | ```javascript 1575 | class InventoryRequester { 1576 | constructor() { 1577 | this.REQ_METHODS = ['HTTP']; 1578 | } 1579 | 1580 | requestItem(item) { 1581 | // ... 1582 | } 1583 | } 1584 | 1585 | class InventoryTracker { 1586 | constructor(items) { 1587 | this.items = items; 1588 | 1589 | // 糟糕的:我們建立了一種依賴,依賴於特定(InventoryRequester)請求模組的實現。 1590 | // 我們實際上只有 `requestItems` 方法依賴於名為 `request` 的請求方法。 1591 | this.requester = new InventoryRequester(); 1592 | } 1593 | 1594 | requestItems() { 1595 | this.items.forEach(item => { 1596 | this.requester.requestItem(item); 1597 | }); 1598 | } 1599 | } 1600 | 1601 | const inventoryTracker = new InventoryTracker(['apples', 'bananas']); 1602 | inventoryTracker.requestItems(); 1603 | ``` 1604 | 1605 | **適當的:** 1606 | ```javascript 1607 | class InventoryTracker { 1608 | constructor(items, requester) { 1609 | this.items = items; 1610 | this.requester = requester; 1611 | } 1612 | 1613 | requestItems() { 1614 | this.items.forEach(item => { 1615 | this.requester.requestItem(item); 1616 | }); 1617 | } 1618 | } 1619 | 1620 | class InventoryRequesterV1 { 1621 | constructor() { 1622 | this.REQ_METHODS = ['HTTP']; 1623 | } 1624 | 1625 | requestItem(item) { 1626 | // ... 1627 | } 1628 | } 1629 | 1630 | class InventoryRequesterV2 { 1631 | constructor() { 1632 | this.REQ_METHODS = ['WS']; 1633 | } 1634 | 1635 | requestItem(item) { 1636 | // ... 1637 | } 1638 | } 1639 | 1640 | // 通過外部創建時將依賴注入,我們可以輕鬆地用全新的 WebSockets 的請求模組替換。 1641 | const inventoryTracker = new InventoryTracker( 1642 | ['apples', 'bananas'], 1643 | new InventoryRequesterV2() 1644 | ); 1645 | inventoryTracker.requestItems(); 1646 | ``` 1647 | 1648 | **譯者附註** 1649 | 1650 | 在糟糕的案例中,`InventoryTracker` 沒有提供替換請求模組的可能,`InventoryTracker` 依賴於 `InventoryRequester`。造成耦合,測試將會被難以撰寫,修改程式碼時將會牽一髮而動全身。 1651 | 1652 | **[⬆ 回到目錄](#目錄table-of-contents)** 1653 | 1654 | ## 測試(Testing) 1655 | 測試比發布更加重要。當發布時,如果你沒有測試或是測試不夠充分,你會無法確認有沒有任何功能被破壞。測試的量,由團隊決定,但是擁有 100% 的測試覆蓋率(包含狀態與分支),是你為什麼能有高度自信與內心平靜的原因。所以你需要一個偉大的測試框架,也需要一個[好的覆蓋率(coverage)工具](http://gotwarlost.github.io/istanbul/)。 1656 | 1657 | 沒有任何藉口不寫測試。這裡有很多[好的 JS 測試框架](http://jstherightway.org/#testing-tools),選一個你的團隊喜歡的。選擇好之後,接下來的目標是為任何新功能或是模組撰寫測試。如果你喜好[測試驅動開發(Test Driven Development)](https://en.wikipedia.org/wiki/Test-driven_development)的方式,那就太棒了,重點是確保上線前或是重構之前,達到足夠的覆蓋率。 1658 | 1659 | **譯者附註** 1660 | 1661 | 測試是一種保障,當你趕著修正錯誤時,測試會告訴你會不會改了 A 壞了 B。確保每次上線前的功能皆可正常運作。另外測試有分種類,詳情見連結[測試的種類](https://en.wikipedia.org/wiki/Software_testing)。 1662 | 1663 | ### 每個測試只測試一個概念 1664 | 1665 | **糟糕的:** 1666 | ```javascript 1667 | import assert from 'assert'; 1668 | 1669 | describe('MakeMomentJSGreatAgain', () => { 1670 | it('handles date boundaries', () => { 1671 | let date; 1672 | 1673 | date = new MakeMomentJSGreatAgain('1/1/2015'); 1674 | date.addDays(30); 1675 | assert.equal('1/31/2015', date); 1676 | 1677 | date = new MakeMomentJSGreatAgain('2/1/2016'); 1678 | date.addDays(28); 1679 | assert.equal('02/29/2016', date); 1680 | 1681 | date = new MakeMomentJSGreatAgain('2/1/2015'); 1682 | date.addDays(28); 1683 | assert.equal('03/01/2015', date); 1684 | }); 1685 | }); 1686 | ``` 1687 | 1688 | **適當的:** 1689 | ```javascript 1690 | import assert from 'assert'; 1691 | 1692 | describe('MakeMomentJSGreatAgain', () => { 1693 | it('handles 30-day months', () => { 1694 | const date = new MakeMomentJSGreatAgain('1/1/2015'); 1695 | date.addDays(30); 1696 | assert.equal('1/31/2015', date); 1697 | }); 1698 | 1699 | it('handles leap year', () => { 1700 | const date = new MakeMomentJSGreatAgain('2/1/2016'); 1701 | date.addDays(28); 1702 | assert.equal('02/29/2016', date); 1703 | }); 1704 | 1705 | it('handles non-leap year', () => { 1706 | const date = new MakeMomentJSGreatAgain('2/1/2015'); 1707 | date.addDays(28); 1708 | assert.equal('03/01/2015', date); 1709 | }); 1710 | }); 1711 | ``` 1712 | 1713 | **譯者附註** 1714 | 1715 | 如果你單個測試,測試過多的功能或是概念,當這個測試出錯的時候,你將會難以找到出錯的程式碼。 1716 | 1717 | **[⬆ 回到目錄](#目錄table-of-contents)** 1718 | 1719 | ## 並發(Concurrency) 1720 | ### 使用 Promises,不要使用回呼函式(callback) 1721 | 回呼函式不怎麼簡潔,他們會導致過多的巢狀。在 ES2016/ES6,Promises 已經是內建的全局類型(global type)。使用它們吧! 1722 | 1723 | **糟糕的:** 1724 | ```javascript 1725 | import { get } from 'request'; 1726 | import { writeFile } from 'fs'; 1727 | 1728 | get( 1729 | 'https://en.wikipedia.org/wiki/Robert_Cecil_Martin', 1730 | (requestErr, response) => { 1731 | if (requestErr) { 1732 | console.error(requestErr); 1733 | } else { 1734 | writeFile('article.html', response.body, writeErr => { 1735 | if (writeErr) { 1736 | console.error(writeErr); 1737 | } else { 1738 | console.log('File written'); 1739 | } 1740 | }); 1741 | } 1742 | } 1743 | ); 1744 | ``` 1745 | 1746 | **適當的:** 1747 | 1748 | ```javascript 1749 | import { get } from 'request'; 1750 | import { writeFile } from 'fs'; 1751 | 1752 | get('https://en.wikipedia.org/wiki/Robert_Cecil_Martin') 1753 | .then(response => { 1754 | return writeFile('article.html', response); 1755 | }) 1756 | .then(() => { 1757 | console.log('File written'); 1758 | }) 1759 | .catch(err => { 1760 | console.error(err); 1761 | }); 1762 | ``` 1763 | 1764 | **[⬆ 回到目錄](#目錄table-of-contents)** 1765 | 1766 | ### Async/Await 比 Promises 更加簡潔 1767 | Promises 是回呼函式的一種非常簡潔的替代品,但是 ES2017/ES8 帶來了 async 與 await,提供了一個更簡潔的方案。你需要的只是一個前綴為 `async` 關鍵字的函數,接下來你編寫邏輯時就不需要使用 `then` 函數鍊。如果你能使用 ES2017/ES8 的進階功能的話,今天就使用它吧! 1768 | 1769 | **糟糕的:** 1770 | ```javascript 1771 | import { get } from 'request-promise'; 1772 | import { writeFile } from 'fs-promise'; 1773 | 1774 | get('https://en.wikipedia.org/wiki/Robert_Cecil_Martin') 1775 | .then(response => { 1776 | return writeFile('article.html', response); 1777 | }) 1778 | .then(() => { 1779 | console.log('File written'); 1780 | }) 1781 | .catch(err => { 1782 | console.error(err); 1783 | }); 1784 | ``` 1785 | 1786 | **適當的:** 1787 | ```javascript 1788 | import { get } from 'request-promise'; 1789 | import { writeFile } from 'fs-promise'; 1790 | 1791 | async function getCleanCodeArticle() { 1792 | try { 1793 | const response = await get( 1794 | 'https://en.wikipedia.org/wiki/Robert_Cecil_Martin' 1795 | ); 1796 | await writeFile('article.html', response); 1797 | console.log('File written'); 1798 | } catch (err) { 1799 | console.error(err); 1800 | } 1801 | } 1802 | ``` 1803 | 1804 | **[⬆ 回到目錄](#目錄table-of-contents)** 1805 | 1806 | ## 錯誤處理(Error Handling) 1807 | 拋出錯誤是一件好事情!代表運行時可以成功辨識程式中的錯誤,通過停止執行目前堆疊(stack)上的執行函數,結束(Node.js 中的)目前程序(process),並在控制台中用一個堆疊追蹤(stack trace)提醒你。 1808 | 1809 | ### 不要忽略捕捉到的錯誤 1810 | 捕捉到一個錯誤時而不做任何處理,會讓你失去修復或是反應錯誤的能力。將錯誤紀錄於控制台(`console.log`)也不怎麼好,因為你往往會迷失在控制台大量的記錄之中。如果你使用 `try/catch` 包住程式碼,代表你預期這裡可能會出錯,因此當錯誤發生時你必須要有個處理方法。 1811 | 1812 | **糟糕的:** 1813 | ```javascript 1814 | try { 1815 | functionThatMightThrow(); 1816 | } catch (error) { 1817 | console.log(error); 1818 | } 1819 | ``` 1820 | 1821 | **適當的:** 1822 | ```javascript 1823 | try { 1824 | functionThatMightThrow(); 1825 | } catch (error) { 1826 | // 可以這樣(會比 console.log 更吵) 1827 | console.error(error); 1828 | // 或這種方法 1829 | notifyUserOfError(error); 1830 | // 另外一種方法 1831 | reportErrorToService(error); 1832 | // 或是全部都做! 1833 | } 1834 | ``` 1835 | 1836 | ### 不要忽略被拒絕的 Promises 1837 | 原因如上節所述,不要忽略任何捕捉到的錯誤。 1838 | 1839 | **糟糕的:** 1840 | ```javascript 1841 | getdata() 1842 | .then(data => { 1843 | functionThatMightThrow(data); 1844 | }) 1845 | .catch(error => { 1846 | console.log(error); 1847 | }); 1848 | ``` 1849 | 1850 | **適當的:** 1851 | ```javascript 1852 | getdata() 1853 | .then(data => { 1854 | functionThatMightThrow(data); 1855 | }) 1856 | .catch(error => { 1857 | // 可以這樣(會比 console.log 更吵) 1858 | console.error(error); 1859 | // 或這種方法 1860 | notifyUserOfError(error); 1861 | // 另外一種方法 1862 | reportErrorToService(error); 1863 | // 或是全部都做! 1864 | }); 1865 | ``` 1866 | 1867 | **[⬆ 回到目錄](#目錄table-of-contents)** 1868 | 1869 | ## 格式化(Formatting) 1870 | 格式化是很主觀的,就像其他規則一樣沒有硬性規定,沒有必要為了格式而爭論,這裡有[大量的自動化格式工具](https://standardjs.com/rules.html),選一個就是了!對工程師來說,爭論格式就是在浪費時間與金錢。 1871 | 1872 | 針對自動格式化工具不能涵蓋的問題,這裡有一些指南。 1873 | 1874 | ### 使用一致的大小寫 1875 | JavaScript 是動態型別的語言,所以從大小寫可以看出關於變數、函數等很多的事情。這些規則是很主觀的,所以你的團隊可以自由選擇。重點是,不管選了去什麼,就保持一致。 1876 | 1877 | **糟糕的:** 1878 | ```javascript 1879 | const DAYS_IN_WEEK = 7; 1880 | const daysInMonth = 30; 1881 | 1882 | const songs = ['Back In Black', 'Stairway to Heaven', 'Hey Jude']; 1883 | const Artists = ['ACDC', 'Led Zeppelin', 'The Beatles']; 1884 | 1885 | function eraseDatabase() {} 1886 | function restore_database() {} 1887 | 1888 | class animal {} 1889 | class Alpaca {} 1890 | ``` 1891 | 1892 | **適當的:** 1893 | ```javascript 1894 | const DAYS_IN_WEEK = 7; 1895 | const DAYS_IN_MONTH = 30; 1896 | 1897 | const SONGS = ['Back In Black', 'Stairway to Heaven', 'Hey Jude']; 1898 | const ARTISTS = ['ACDC', 'Led Zeppelin', 'The Beatles']; 1899 | 1900 | function eraseDatabase() {} 1901 | function restoreDatabase() {} 1902 | 1903 | class Animal {} 1904 | class Alpaca {} 1905 | ``` 1906 | 1907 | **[⬆ 回到目錄](#目錄table-of-contents)** 1908 | 1909 | ### 函數的呼叫者應該與被呼叫者靠近 1910 | 如果一個函數呼叫另外一個,在程式碼中兩個函數的垂直位置應該靠近。理想情況下,呼叫函數應於被呼叫函數的正上方。我們傾向於從上到下的閱讀方式,就像看報紙一樣。基於這個原因,讓你的程式碼可以依照這種方式閱讀。 1911 | 1912 | **糟糕的:** 1913 | ```javascript 1914 | class PerformanceReview { 1915 | constructor(employee) { 1916 | this.employee = employee; 1917 | } 1918 | 1919 | lookupPeers() { 1920 | return db.lookup(this.employee, 'peers'); 1921 | } 1922 | 1923 | lookupManager() { 1924 | return db.lookup(this.employee, 'manager'); 1925 | } 1926 | 1927 | getPeerReviews() { 1928 | const peers = this.lookupPeers(); 1929 | // ... 1930 | } 1931 | 1932 | perfReview() { 1933 | this.getPeerReviews(); 1934 | this.getManagerReview(); 1935 | this.getSelfReview(); 1936 | } 1937 | 1938 | getManagerReview() { 1939 | const manager = this.lookupManager(); 1940 | } 1941 | 1942 | getSelfReview() { 1943 | // ... 1944 | } 1945 | } 1946 | 1947 | const review = new PerformanceReview(employee); 1948 | review.perfReview(); 1949 | ``` 1950 | 1951 | **適當的:** 1952 | ```javascript 1953 | class PerformanceReview { 1954 | constructor(employee) { 1955 | this.employee = employee; 1956 | } 1957 | 1958 | perfReview() { 1959 | this.getPeerReviews(); 1960 | this.getManagerReview(); 1961 | this.getSelfReview(); 1962 | } 1963 | 1964 | getPeerReviews() { 1965 | const peers = this.lookupPeers(); 1966 | // ... 1967 | } 1968 | 1969 | lookupPeers() { 1970 | return db.lookup(this.employee, 'peers'); 1971 | } 1972 | 1973 | getManagerReview() { 1974 | const manager = this.lookupManager(); 1975 | } 1976 | 1977 | lookupManager() { 1978 | return db.lookup(this.employee, 'manager'); 1979 | } 1980 | 1981 | getSelfReview() { 1982 | // ... 1983 | } 1984 | } 1985 | 1986 | const review = new PerformanceReview(employee); 1987 | review.perfReview(); 1988 | ``` 1989 | 1990 | **[⬆ 回到目錄](#目錄table-of-contents)** 1991 | 1992 | ## 註解(Comments) 1993 | ### 只對商業邏輯複雜的部分撰寫註解 1994 | 註解是代表的辯解,而不是要求。多數情況下,好的程式碼本身就是文件。 1995 | 1996 | **糟糕的:** 1997 | ```javascript 1998 | function hashIt(data) { 1999 | // The hash 2000 | let hash = 0; 2001 | 2002 | // Length of string 2003 | const length = data.length; 2004 | 2005 | // Loop through every character in data 2006 | for (let i = 0; i < length; i++) { 2007 | // Get character code. 2008 | const char = data.charCodeAt(i); 2009 | // Make the hash 2010 | hash = (hash << 5) - hash + char; 2011 | // Convert to 32-bit integer 2012 | hash &= hash; 2013 | } 2014 | } 2015 | ``` 2016 | 2017 | **適當的:** 2018 | 2019 | ```javascript 2020 | function hashIt(data) { 2021 | let hash = 0; 2022 | const length = data.length; 2023 | 2024 | for (let i = 0; i < length; i++) { 2025 | const char = data.charCodeAt(i); 2026 | hash = (hash << 5) - hash + char; 2027 | 2028 | // Convert to 32-bit integer 2029 | hash &= hash; 2030 | } 2031 | } 2032 | ``` 2033 | 2034 | **[⬆ 回到目錄](#目錄table-of-contents)** 2035 | 2036 | ### 不要在程式碼中保留被註解掉的程式碼 2037 | 有了版本控制,舊的程式碼留在歷史紀錄中就好。 2038 | 2039 | **糟糕的:** 2040 | ```javascript 2041 | doStuff(); 2042 | // doOtherStuff(); 2043 | // doSomeMoreStuff(); 2044 | // doSoMuchStuff(); 2045 | ``` 2046 | 2047 | **適當的:** 2048 | ```javascript 2049 | doStuff(); 2050 | ``` 2051 | 2052 | **[⬆ 回到目錄](#目錄table-of-contents)** 2053 | 2054 | ### 不要留有日誌式的註解 2055 | 記住,使用版本控制!不需要無用的、註解掉的程式碼,尤其是日誌式的註解。使用 `git log` 來保存歷史紀錄。 2056 | 2057 | **糟糕的:** 2058 | ```javascript 2059 | /** 2060 | * 2016-12-20: Removed monads, didn't understand them (RM) 2061 | * 2016-10-01: Improved using special monads (JP) 2062 | * 2016-02-03: Removed type-checking (LI) 2063 | * 2015-03-14: Added combine with type-checking (JR) 2064 | */ 2065 | function combine(a, b) { 2066 | return a + b; 2067 | } 2068 | ``` 2069 | 2070 | **適當的:** 2071 | ```javascript 2072 | function combine(a, b) { 2073 | return a + b; 2074 | } 2075 | ``` 2076 | 2077 | **譯者附註** 2078 | 2079 | 在註解中寫歷史紀錄並沒有在版本控制中來得有效,另外補充有關歷史紀錄如何撰寫的規範([如何撰寫 Git Commit Message](https://chris.beams.io/posts/git-commit/))。 2080 | 2081 | **[⬆ 回到目錄](#目錄table-of-contents)** 2082 | 2083 | ### 避免位置標示 2084 | 它們只會增加干擾。讓函數與變數的名稱沿著合適的縮排與格式化,為你的程式碼帶來良好的視覺結構。 2085 | 2086 | **糟糕的:** 2087 | ```javascript 2088 | //////////////////////////////////////////////////////////////////////////////// 2089 | // Scope Model Instantiation 2090 | //////////////////////////////////////////////////////////////////////////////// 2091 | $scope.model = { 2092 | menu: 'foo', 2093 | nav: 'bar' 2094 | }; 2095 | 2096 | //////////////////////////////////////////////////////////////////////////////// 2097 | // Action setup 2098 | //////////////////////////////////////////////////////////////////////////////// 2099 | const actions = function() { 2100 | // ... 2101 | }; 2102 | ``` 2103 | 2104 | **適當的:** 2105 | ```javascript 2106 | $scope.model = { 2107 | menu: 'foo', 2108 | nav: 'bar' 2109 | }; 2110 | 2111 | const actions = function() { 2112 | // ... 2113 | }; 2114 | ``` 2115 | 2116 | **[⬆ 回到目錄](#目錄table-of-contents)** 2117 | 2118 | ## 翻譯(Translation) 2119 | 以下為所有的翻譯版本。 2120 | 2121 | - ![fr](https://raw.githubusercontent.com/gosquared/flags/master/flags/flags/shiny/24/France.png) **French**: 2122 | [GavBaros/clean-code-javascript-fr](https://github.com/GavBaros/clean-code-javascript-fr) 2123 | - ![br](https://raw.githubusercontent.com/gosquared/flags/master/flags/flags/shiny/24/Brazil.png) **Brazilian Portuguese**: [fesnt/clean-code-javascript](https://github.com/fesnt/clean-code-javascript) 2124 | - ![es](https://raw.githubusercontent.com/gosquared/flags/master/flags/flags/shiny/24/Uruguay.png) **Spanish**: [andersontr15/clean-code-javascript](https://github.com/andersontr15/clean-code-javascript-es) 2125 | - ![es](https://raw.githubusercontent.com/gosquared/flags/master/flags/flags/shiny/24/Spain.png) **Spanish**: [tureey/clean-code-javascript](https://github.com/tureey/clean-code-javascript) 2126 | - ![cn](https://raw.githubusercontent.com/gosquared/flags/master/flags/flags/shiny/24/China.png) **Simplified Chinese**: 2127 | - [alivebao/clean-code-js](https://github.com/alivebao/clean-code-js) 2128 | - [beginor/clean-code-javascript](https://github.com/beginor/clean-code-javascript) 2129 | - ![tw](https://raw.githubusercontent.com/gosquared/flags/master/flags/flags/shiny/24/Taiwan.png) **Traditional Chinese**: [AllJointTW/clean-code-javascript](https://github.com/AllJointTW/clean-code-javascript) 2130 | - ![de](https://raw.githubusercontent.com/gosquared/flags/master/flags/flags/shiny/24/Germany.png) **German**: [marcbruederlin/clean-code-javascript](https://github.com/marcbruederlin/clean-code-javascript) 2131 | - ![kr](https://raw.githubusercontent.com/gosquared/flags/master/flags/flags/shiny/24/South-Korea.png) **Korean**: [qkraudghgh/clean-code-javascript-ko](https://github.com/qkraudghgh/clean-code-javascript-ko) 2132 | - ![pl](https://raw.githubusercontent.com/gosquared/flags/master/flags/flags/shiny/24/Poland.png) **Polish**: [greg-dev/clean-code-javascript-pl](https://github.com/greg-dev/clean-code-javascript-pl) 2133 | - ![ru](https://raw.githubusercontent.com/gosquared/flags/master/flags/flags/shiny/24/Russia.png) **Russian**: 2134 | - [BoryaMogila/clean-code-javascript-ru/](https://github.com/BoryaMogila/clean-code-javascript-ru/) 2135 | - [maksugr/clean-code-javascript](https://github.com/maksugr/clean-code-javascript) 2136 | - ![vi](https://raw.githubusercontent.com/gosquared/flags/master/flags/flags/shiny/24/Vietnam.png) **Vietnamese**: [hienvd/clean-code-javascript/](https://github.com/hienvd/clean-code-javascript/) 2137 | - ![ja](https://raw.githubusercontent.com/gosquared/flags/master/flags/flags/shiny/24/Japan.png) **Japanese**: [mitsuruog/clean-code-javascript/](https://github.com/mitsuruog/clean-code-javascript/) 2138 | - ![id](https://raw.githubusercontent.com/gosquared/flags/master/flags/flags/shiny/24/Indonesia.png) **Indonesia**: 2139 | [andirkh/clean-code-javascript/](https://github.com/andirkh/clean-code-javascript/) 2140 | - ![it](https://raw.githubusercontent.com/gosquared/flags/master/flags/flags/shiny/24/Italy.png) **Italian**: 2141 | [frappacchio/clean-code-javascript/](https://github.com/frappacchio/clean-code-javascript/) 2142 | 2143 | **[⬆ 回到目錄](#目錄table-of-contents)** 2144 | 2145 | --------------------------------------------------------------------------------