├── .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 | 
透過計算閱讀程式碼時的咒罵次數,來評估軟體品質
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 | -  **French**:
2122 | [GavBaros/clean-code-javascript-fr](https://github.com/GavBaros/clean-code-javascript-fr)
2123 | -  **Brazilian Portuguese**: [fesnt/clean-code-javascript](https://github.com/fesnt/clean-code-javascript)
2124 | -  **Spanish**: [andersontr15/clean-code-javascript](https://github.com/andersontr15/clean-code-javascript-es)
2125 | -  **Spanish**: [tureey/clean-code-javascript](https://github.com/tureey/clean-code-javascript)
2126 | -  **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 | -  **Traditional Chinese**: [AllJointTW/clean-code-javascript](https://github.com/AllJointTW/clean-code-javascript)
2130 | -  **German**: [marcbruederlin/clean-code-javascript](https://github.com/marcbruederlin/clean-code-javascript)
2131 | -  **Korean**: [qkraudghgh/clean-code-javascript-ko](https://github.com/qkraudghgh/clean-code-javascript-ko)
2132 | -  **Polish**: [greg-dev/clean-code-javascript-pl](https://github.com/greg-dev/clean-code-javascript-pl)
2133 | -  **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 | -  **Vietnamese**: [hienvd/clean-code-javascript/](https://github.com/hienvd/clean-code-javascript/)
2137 | -  **Japanese**: [mitsuruog/clean-code-javascript/](https://github.com/mitsuruog/clean-code-javascript/)
2138 | -  **Indonesia**:
2139 | [andirkh/clean-code-javascript/](https://github.com/andirkh/clean-code-javascript/)
2140 | -  **Italian**:
2141 | [frappacchio/clean-code-javascript/](https://github.com/frappacchio/clean-code-javascript/)
2142 |
2143 | **[⬆ 回到目錄](#目錄table-of-contents)**
2144 |
2145 |
--------------------------------------------------------------------------------