forked from ryanmcdermott/clean-code-javascript
-
Notifications
You must be signed in to change notification settings - Fork 184
/
README.md
2144 lines (1715 loc) · 60.6 KB
/
README.md
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# 無瑕的程式碼 JavaScript
> 原作: [https://github.com/ryanmcdermott/clean-code-javascript](https://github.com/ryanmcdermott/clean-code-javascript)<br>
> 原作者: [https://github.com/ryanmcdermott](https://github.com/ryanmcdermott) <br>
> 譯者: [https://github.com/trylovetom](https://github.com/trylovetom)
## 目錄(Table of Contents)
1. [介紹(Introduction)](#介紹Introduction)
2. [變數(Variables)](#變數Variables)
3. [函數(Functions)](#函數Functions)
4. [物件(Objects)與資料結構(Data Structures)](#物件Objects與資料結構Data-Structures)
5. [類別(Classes)](#類別Classes)
6. [物件導向基本原則(SOLID)](#物件導向基本原則SOLID)
7. [測試(Testing)](#測試Testing)
8. [並發(Concurrency)](#並發Concurrency)
9. [錯誤處理(Error Handling)](#錯誤處理Error-Handling)
10. [格式化(Formatting)](#格式化Formatting)
11. [註解(Comments)](#註解Comments)
12. [翻譯(Translation)](#翻譯Translation)
## 介紹(Introduction)
![透過計算閱讀程式碼時的咒罵次數,來評估軟體品質](http://www.osnews.com/images/comics/wtfm.jpg)<br>透過計算閱讀程式碼時的咒罵次數,來評估軟體品質<br><br>
文章作者根據 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 程式碼。
注意!你不必嚴格遵守每一項原則,有些甚至不被大眾所認同。雖然這只是份指南,卻是來自《無暇的程式碼》作者的多年結晶。
軟體工程只發展了五十年,仍然有很多地方值得去探討。當軟體與建築一樣古老時,也許會有一些墨守成規的原則。但現在,先讓這份指南當試金石,作為你和團隊的 JS 程式碼標準。
還有一件事情:知道這些原則,並不會立刻讓你成為出色的開發者,長期奉行它們,不代表你能高枕無憂不再犯錯。但是,千里之行,始於足下,時常與志同道合們進行討論(Code Review),改善不完備之處。不要因為自己寫出來的程式碼很糟糕而害怕分享,而是要畏懼自己居然寫出了這樣的程式碼!
**譯者序**
> 獻給[傲嬌文創](https://alljoint.tw/)的所有工程師夥伴,與熱愛 JavaScript 的各位。
《無暇的程式碼》是一本好書,是不可否認這個事實。我熱愛著 JS,當找到這份 JS 版本的後,我無比開心立刻著手翻譯,因此有了這份《無暇的程式碼 JavaScript》。對於 *Clean Code* 的書名採用博碩文化的翻譯,也是對原版譯者的尊敬。本翻譯中會穿插一些我對文章中註解,也請讀者原諒我的叨擾。另外專業術語的翻譯有可能會有出入,我會標示出英文原文,避免讀者誤解。如有翻譯或是理解上的錯誤,煩請聯絡我,謝謝。(聯絡方式在上方,我的 GitHub 中。)
## 變數(Variables)
### 使用具有意義且可閱讀的名稱
**糟糕的:**
```javascript
const yyyymmdstr = moment().format('YYYY/MM/DD');
```
**適當的:**
```javascript
const currentDate = moment().format('YYYY/MM/DD');
```
**[⬆ 回到目錄](#目錄table-of-contents)**
### 相同類型的變數使用相同的名稱
**糟糕的:**
```javascript
getUserInfo();
getClientData();
getCustomerRecord();
```
**適當的:**
```javascript
getUser();
```
**[⬆ 回到目錄](#目錄table-of-contents)**
### 使用可搜尋的名稱
使用易於閱讀與搜尋的名稱非常重要,因為我們要閱讀的程式碼遠比自己寫得多。使用沒有意義的名稱,會導致程式碼難以理解,對後續閱讀者是個糟糕的體驗。另外使用以下工具,可以協助你找出未命名的常數:
* [buddy.js](https://github.com/danielstjules/buddy.js)
* [ESLint](https://github.com/eslint/eslint/blob/660e0918933e6e7fede26bc675a0763a6b357c94/docs/rules/no-magic-numbers.md)
**糟糕的:**
```javascript
// 86400000 代表什麼意義?
setTimeout(blastOff, 86400000);
```
**適當的:**
```javascript
// 宣告(Declare)一個有意義的常數(constants)
const MILLISECONDS_IN_A_DAY = 86400000;
setTimeout(blastOff, MILLISECONDS_IN_A_DAY);
```
**[⬆ 回到目錄](#目錄table-of-contents)**
### 使用可解釋的變數
**糟糕的:**
```javascript
const address = 'One Infinite Loop, Cupertino 95014';
const cityZipCodeRegex = /^[^,\\]+[,\\\s]+(.+?)\s*(\d{5})?$/;
saveCityZipCode(
address.match(cityZipCodeRegex)[1],
address.match(cityZipCodeRegex)[2]
);
```
**適當的:**
```javascript
const address = 'One Infinite Loop, Cupertino 95014';
const cityZipCodeRegex = /^[^,\\]+[,\\\s]+(.+?)\s*(\d{5})?$/;
const [_, city, zipCode] = address.match(cityZipCodeRegex) || [];
saveCityZipCode(city, zipCode);
```
**譯者附註**
`address.match(cityZipCodeRegex)` 取出了字串中的 city 與 zipCode 並以陣列(Array)的方式輸出。在糟糕的範例中,你不會知道哪個是 city,哪個是 zipCode。在適當的範例中,則清楚地解釋了。
**[⬆ 回到目錄](#目錄table-of-contents)**
### 避免心理作用(Mental Mapping)
清晰(Explicit)的表達會比隱藏(Implicit)更好。
**糟糕的:**
```javascript
const locations = ['Austin', 'New York', 'San Francisco'];
locations.forEach(l => {
doStuff();
doSomeOtherStuff();
// ...
// ...
// ...
// 等等,這 `l` 是…?
dispatch(l);
});
```
**適當的:**
```javascript
const locations = ['Austin', 'New York', 'San Francisco'];
locations.forEach(location => {
doStuff();
doSomeOtherStuff();
// ...
// ...
// ...
dispatch(location);
});
```
**譯者附註**
在糟糕的範例中,程式碼的作者認為從 `locations` 取出的都是地址,所以選用縮減後的 `l` 作為名稱。不過這只有作者自己這麼認為,其他人可不一定知道。避免「我認為」、「我以為」、「我覺得」,這樣的心理作用。
**[⬆ 回到目錄](#目錄table-of-contents)**
### 避免使用不必要的描述(Context)
如果你的類別與物件名稱是有關聯意義的,就不用在內部變數上再次重複。
**糟糕的:**
```javascript
const Car = {
carMake: 'Honda',
carModel: 'Accord',
carColor: 'Blue'
};
function paintCar(car, color) {
car.carColor = color;
}
```
**適當的:**
```javascript
const Car = {
make: 'Honda',
model: 'Accord',
color: 'Blue'
};
function paintCar(color) {
car.color = color;
}
```
**[⬆ 回到目錄](#目錄table-of-contents)**
### 使用預設參數(Parameter)代替條件判斷(Conditionals)
使用預設參數較整潔,但請注意,當參數為 `undefined` 時才會作用,其他種類的虛值(Falsy)不會,像是 `''`、`false`、`null`、`0`、`NaN` 等。
**糟糕的:**
```javascript
function createMicrobrewery(name) {
const breweryName = name || 'Hipster Brew Co.';
// ...
}
```
**適當的:**
```javascript
function createMicrobrewery(name = 'Hipster Brew Co.') {
// ...
}
```
**譯者附註**
預設參數非常好用,可以結合工廠模式做出很多應用。另外建議統一使用 `undefined` 代替 `null` 當作空值的回傳值。
此處原文為 **Arguments**,但是函數的參數定義應該為 **Parameter**,而呼叫函數時傳遞的引數才是 **Arguments**。
**[⬆ 回到目錄](#目錄table-of-contents)**
## 函數(Functions)
### 參數(Parameter) (少於 2 個較佳)
限制函數的參數數量非常重要,因為能讓你更容易地測試。過多的參數代表著過多的組合,會導致你不得不編寫出大量測試。
一個至二個是最理想的,盡可能避免大於三個以上。如果你有超過兩個以上的參數,代表你的函數做太多事情。如果無法避免時,可以有效地使用物件替代大量的參數。
為了讓你可以清晰地表達,預期使用哪些物件的屬性(Properties),可以使用 ES2015/ES6 提供的解構(Destructuring)語法。使用這種語法有以下優點:
1. 函數需要物件的哪些屬性,可以像參數一樣清晰地表達。
2. 解構語法會複製來自物件的原始型態(Primitive),這能幫助你避免副作用(Side Effect)。注意!巢狀物件與陣列,並不會被解構語法複製。
3. 使用解構語法能讓物件屬性被程式碼檢查器(Linter)作用,提醒你哪些屬性未被使用到。
**糟糕的:**
```javascript
function createMenu(title, body, buttonText, cancellable) {
// ...
}
```
**適當的:**
```javascript
function createMenu({ title, body, buttonText, cancellable }) {
// ...
}
createMenu({
title: 'Foo',
body: 'Bar',
buttonText: 'Baz',
cancellable: true
});
```
**譯者附註**
這種方法非常適合用於工廠模式,結合上一章的預設參數,譯者推薦的使用方式如下:
```javascript
function createMenu({
title = 'Default Title', // 傳遞的物件不齊全,使用預設屬性
body = '',
buttonText = 'My Button',
cancellable = true
} = {}) { // 如未傳遞任何參數使用預設空物件,使用 `= {}` 可避免 TypeError: Cannot destructure property `...` of 'undefined' or 'null'.
return {
title,
body,
buttonText,
cancellable
}
}
const myMenu = createMenu({
title: 'Foo',
body: 'Bar',
buttonText: 'Baz',
cancellable: true
});
```
想了解更多工廠模式與 ES6 的結合,可參考連結文章:[JavaScript Factory Functions with ES6+](https://medium.com/javascript-scene/javascript-factory-functions-with-es6-4d224591a8b1)。
**[⬆ 回到目錄](#目錄table-of-contents)**
### 一個函數只做一件事情(單一性)
這是個非常重要的原則,當你的函數做超過一件事情時,它會更難以被撰寫、測試與理解。當你隔離(Isolate)你的函數到只做一件事情時,它能更容易地被重構(Refactor)與清晰地閱讀。如果嚴格遵守此項原則,你將會領先許多開發者。
**糟糕的:**
```javascript
function emailClients(clients) {
clients.forEach(client => {
const clientRecord = database.lookup(client);
if (clientRecord.isActive()) {
email(client);
}
});
}
```
**適當的:**
```javascript
function emailActiveClients(clients) {
clients.filter(isActiveClient).forEach(email);
}
function isActiveClient(client) {
const clientRecord = database.lookup(client);
return clientRecord.isActive();
}
```
**[⬆ 回到目錄](#目錄table-of-contents)**
### 函數名稱應該說明它做的內容
**糟糕的:**
```javascript
function addToDate(date, month) {
// ...
}
const date = new Date();
// 難以從函數名稱看出到底加入了什麼
addToDate(date, 1);
```
**適當的:**
```javascript
function addMonthToDate(month, date) {
// ...
}
const date = new Date();
addMonthToDate(1, date);
```
**譯者附註**
建議函數命名以動詞開頭,像是 `doSomething()`、`setupUserProfile()`。
**[⬆ 回到目錄](#目錄table-of-contents)**
### 函數應該只做一層抽象(Abstraction)
當你的函數需要的抽象多於一層時,代表你的函數做太多事情了。將其分解以利重用與測試。
**糟糕的:**
```javascript
function parseBetterJSAlternative(code) {
const REGEXES = [
// ...
];
const statements = code.split(' ');
const tokens = [];
REGEXES.forEach(REGEX => {
statements.forEach(statement => {
// ...
});
});
const ast = [];
tokens.forEach(token => {
// lex...
});
ast.forEach(node => {
// parse...
});
}
```
**適當的:**
```javascript
function parseBetterJSAlternative(code) {
const tokens = tokenize(code);
const syntaxTree = parse(tokens);
syntaxTree.forEach(node => {
// parse...
});
}
function tokenize(code) {
const REGEXES = [
// ...
];
const statements = code.split(' ');
const tokens = [];
REGEXES.forEach(REGEX => {
statements.forEach(statement => {
tokens.push(/* ... */);
});
});
return tokens;
}
function parse(tokens) {
const syntaxTree = [];
tokens.forEach(token => {
syntaxTree.push(/* ... */);
});
return syntaxTree;
}
```
**譯者附註**
這個原則與前面提到的「一個函數只做一件事情(單一性)」概念相似。
**[⬆ 回到目錄](#目錄table-of-contents)**
### 移除重複的(Duplicate)程式碼
絕對避免重複的程式碼,重複的程式碼代表著更動邏輯時,需要同時修改多處。
想像一下你經營著一家餐廳,需要持續追蹤存貨:番茄、洋蔥、大蒜與各種香料等。如果你有多份紀錄表,當使用蕃茄做完一道料理時,需要更新多份記錄表,可能會忘記更新其中一份。如果你只有一份記錄表,就不會有此問題!
通常重複的程式碼,是因為有兩個稍微不同的東西。它們之間絕大部分相同,但些微不同之處,迫使你使用多個函數處理相似的事情。如果出現重複的程式碼,可以使用函數、模組或是類別來抽象化處理。
正確的抽象化是非常關鍵的,這也是為什麼你應該遵循[類別(Classes)](#類別Classes)章節中,物件導向基本原則 SOLID 的原因。請小心,較差的抽象化會比重複的程式碼更糟!這麼說吧,如果你有把握做出好的抽象化,盡情放手去做。別讓程式碼出現重複的地方,不然你會需要修改更多的程式碼。
**糟糕的:**
```javascript
function showDeveloperList(developers) {
developers.forEach(developer => {
const expectedSalary = developer.calculateExpectedSalary();
const experience = developer.getExperience();
const githubLink = developer.getGithubLink();
const data = {
expectedSalary,
experience,
githubLink
};
render(data);
});
}
function showManagerList(managers) {
managers.forEach(manager => {
const expectedSalary = manager.calculateExpectedSalary();
const experience = manager.getExperience();
const portfolio = manager.getMBAProjects();
const data = {
expectedSalary,
experience,
portfolio
};
render(data);
});
}
```
**適當的:**
```javascript
function showEmployeeList(employees) {
employees.forEach(employee => {
const expectedSalary = employee.calculateExpectedSalary();
const experience = employee.getExperience();
const data = {
expectedSalary,
experience
};
switch (employee.type) {
case 'manager':
data.portfolio = employee.getMBAProjects();
break;
case 'developer':
data.githubLink = employee.getGithubLink();
break;
}
render(data);
});
}
```
**譯者附註**
剛讀完這個原則時,我非常遵守,但是個人龜毛的個性,造成了不少麻煩,我會在開發時不斷的思考是否會出現了重複的程式碼,甚至考慮到了之後的重用性。代價就是過度設計(Over Engineering)造成功能開發窒礙難行,設計了良好架構,但實際上並未被使用到。最後總結了一個建議作為附加原則:一開始撰寫程式碼先以功能開發優先,當你發現有兩個以上的地方重複時,再來考慮要不要重構。
**[⬆ 回到目錄](#目錄table-of-contents)**
### 使用 `Object.assign` 設定 `Object` 的預設值
**糟糕的:**
```javascript
const menuConfig = {
title: null,
body: 'Bar',
buttonText: null,
cancellable: true
};
function createMenu(config) {
config.title = config.title || 'Foo';
config.body = config.body || 'Bar';
config.buttonText = config.buttonText || 'Baz';
config.cancellable =
config.cancellable !== undefined ? config.cancellable : true;
}
createMenu(menuConfig);
```
**適當的:**
```javascript
const menuConfig = {
title: 'Order',
// 使用者漏掉了 'body'
buttonText: 'Send',
cancellable: true
};
function createMenu(config) {
let finalConfig = Object.assign(
{
title: 'Foo',
body: 'Bar',
buttonText: 'Baz',
cancellable: true
},
config
);
return finalConfig;
// config 現在等同於: {title: 'Order', body: 'Bar', buttonText: 'Send', cancellable: true}
// ...
}
createMenu(menuConfig);
```
**[⬆ 回到目錄](#目錄table-of-contents)**
### 不要使用旗標(Flag)作為參數
當你的函數使用了旗標當作參數時,代表函數做不只一件事情,依照不同旗標路徑切分你的函數。
**糟糕的:**
```javascript
function createFile(name, temp) {
if (temp) {
fs.create(`./temp/${name}`);
} else {
fs.create(name);
}
}
```
**適當的:**
```javascript
function createFile(name) {
fs.create(name);
}
function createTempFile(name) {
createFile(`./temp/${name}`);
}
```
**[⬆ 回到目錄](#目錄table-of-contents)**
### 避免副作用(Side Effects)
當函數作用在除了回傳值外的地方,像是讀寫文件、修改全域變數或是將你的錢轉帳到其他人帳戶,則稱為副作用。
程式在某些情況下是需要副作用的,像是上面所提到的例子。這時你應該將這些功能集中在一起,不要同時有多個函數或是類別同時操作資源,應該只用一個服務(Service)完成這些事情。
常見的問題像是:
* 在沒有任何架構下,同時多個物件中分享共有狀態。
* 可變的狀態,且可以被任何人寫入
* 副作用發生的地方沒有被集中。
如果你能避免這些問題,你會比大多數的工程師快樂。
**糟糕的:**
```javascript
// 全域變數被以下函數使用
// 假如有其他的函數使用了這個名稱,現在他變成了陣列,將會被破壞而出錯。
let name = 'Ryan McDermott';
function splitIntoFirstAndLastName() {
name = name.split(' ');
}
splitIntoFirstAndLastName();
console.log(name); // ['Ryan', 'McDermott'];
```
**適當的:**
```javascript
function splitIntoFirstAndLastName(name) {
return name.split(' ');
}
const name = 'Ryan McDermott';
const newName = splitIntoFirstAndLastName(name);
console.log(name); // 'Ryan McDermott';
console.log(newName); // ['Ryan', 'McDermott'];
```
**[⬆ 回到目錄](#目錄table-of-contents)**
### 避免副作用(Side Effects)第二部分
在 JavaScript 中,原始資料類型傳遞數值(Value),物件/陣列傳遞參照(Reference)。在本案例中,你的函數改變了購物車清單 `cart` 中的陣列,像是你增加了一個商品,其他使用購物車清單的函數將會被影響。這做法有好有壞,讓我解釋一下問題所在:
使用者按下付款按鈕後,將會呼叫 `purchase` 函數,產生一個網路請求傳送購物車清單陣列到伺服器。因為較差的網路連線,必須多嘗試幾次。此時使用者不小心又按下加入購物車按鈕,因為是參考的關係,請求將會送出新加入的商品。
較好的解決辦法是 `addItemToCart` 函數,執行前複製新的一份購物車清單,修改複製的資料後再回傳。這能確保其他的函數的購物車清單沒有任何機會被被參考所影響。
使用這方法前,有兩個警告要告知:
1. 當採用這種做法後,你會發現,需要修改輸入物件的情況非常少。大多數的程式碼可以在沒有副作用的情況下重構!
2. 複製大型物件,需要花費高昂的效能。幸好,我們有好的[函數庫](https://immutable-js.github.io/immutable-js/),可以提升複製物件與陣列的速度與減少記憶體使用。
**糟糕的:**
```javascript
const addItemToCart = (cart, item) => {
cart.push({ item, date: Date.now() });
};
```
**適當的:**
```javascript
const addItemToCart = (cart, item) => {
return [...cart, { item, date: Date.now() }];
};
```
**譯者附註**
譯者這邊另外再提供一個案例,函數 `checkIs18Age` 用來檢查是否成年。第一個寫法中,引用了全域(global)變數 `minimum`,這功能看起來能正常運作沒問題,但是今天如果有其他函數 `setMinAge` 修改了全域變數 `minimum`,函數 `checkIs18Age` 將會因為副作用的關係變的無法預期,甚至失去它的作用。
較好的寫法是,採用純函數(pure function),使用一個可預期的變數,來避免副作用影響。
```javascript
let minimum = 18
// impure with side effect
const checkIs18Age = age => age >= minimum
const setMinAge = age => minimum = age
// pure
const checkIs18Age = age => {
let minimum = 18
return age >= minimum
}
```
另外使用解構的方式複製資料,只會複製第一層而已。
```javascript
// 巢狀解構只能複製第一層
const obj1 = { subObj: { message: 'Hey' } }
const obj2 = { ...obj1 }
obj2.subObj.message = 'Yo'
console.log(obj1.subObj.message) // 'Yo'
```
如果要複製巢狀結構,你需要深度複製。可以使用一些函數庫 [loadsh](https://lodash.com/) 的 `_.CloneDeep` 或是 [ramda](https://ramdajs.com/) 的 `clone`。或是使用 `JSON.parse(JSON.stringify(object))` 來實現。不過使用 JSON 的話,會失去 Function 的複製。
```javascript
// 使用 JSON 來深度複製
const obj1 = { subObj: { message: 'Hey' } }
const obj2 = JSON.parse(JSON.stringify(obj1))
obj2.subObj.message = 'Yo'
console.log(obj1.subObj.message) // 'Hey'
```
**[⬆ 回到目錄](#目錄table-of-contents)**
### 別寫全域函數(Global Function)
在 JavaScript 中弄髒全域是個不好的做法,因為你可能會影響到其他函數庫或是 API。舉個例子,如果妳想要在 JavaScript 的原生陣列方法,擴展 `diff` 方法,用 B 陣列來去除 A 陣列中的元素(Element)。常見做法你可能會在 `Array.prototype` 中增加一個全新的函數,如果其他函數庫也有自己的 `diff` 實現的話將會互相影響。這就是為什麼我們需要使用 ES2015/ES6 的類別,來擴展的原因。
**糟糕的:**
```javascript
Array.prototype.diff = function diff(comparisonArray) {
const hash = new Set(comparisonArray);
return this.filter(elem => !hash.has(elem));
};
```
**適當的:**
```javascript
class SuperArray extends Array {
diff(comparisonArray) {
const hash = new Set(comparisonArray);
return this.filter(elem => !hash.has(elem));
}
}
```
**[⬆ 回到目錄](#目錄table-of-contents)**
### 偏好使用函數式程式(Functional Programming)設計代替命令式程式設計(Imperative Programming)
JavaScript 不是像 Haskell 一樣的函數式語言,但它具有類似特性。函數式程式設計更加乾淨且容易被測試。當你在寫程式時,盡量選擇此設計方式。
**糟糕的:**
```javascript
const programmerOutput = [
{
name: 'Uncle Bobby',
linesOfCode: 500
},
{
name: 'Suzie Q',
linesOfCode: 1500
},
{
name: 'Jimmy Gosling',
linesOfCode: 150
},
{
name: 'Gracie Hopper',
linesOfCode: 1000
}
];
let totalOutput = 0;
for (let i = 0; i < programmerOutput.length; i++) {
totalOutput += programmerOutput[i].linesOfCode;
}
```
**適當的:**
```javascript
const programmerOutput = [
{
name: 'Uncle Bobby',
linesOfCode: 500
},
{
name: 'Suzie Q',
linesOfCode: 1500
},
{
name: 'Jimmy Gosling',
linesOfCode: 150
},
{
name: 'Gracie Hopper',
linesOfCode: 1000
}
];
const totalOutput = programmerOutput.reduce(
(totalLines, output) => totalLines + output.linesOfCode,
0
);
```
**[⬆ 回到目錄](#目錄table-of-contents)**
### 封裝狀態(Encapsulate Conditionals)
**糟糕的:**
```javascript
if (fsm.state === 'fetching' && isEmpty(listNode)) {
// ...
}
```
**適當的:**
```javascript
function shouldShowSpinner(fsm, listNode) {
return fsm.state === 'fetching' && isEmpty(listNode);
}
if (shouldShowSpinner(fsmInstance, listNodeInstance)) {
// ...
}
```
**[⬆ 回到目錄](#目錄table-of-contents)**
### 避免負面狀態(Negative Conditionals)
**糟糕的:**
```javascript
function isDOMNodeNotPresent(node) {
// ...
}
if (!isDOMNodeNotPresent(node)) {
// ...
}
```
**適當的:**
```javascript
function isDOMNodePresent(node) {
// ...
}
if (isDOMNodePresent(node)) {
// ...
}
```
**[⬆ 回到目錄](#目錄table-of-contents)**
### 避免狀態
當你第一次聽到時,這聽起來是不可能的任務。大部分的人會說:「怎麼可能不使用 `if` 語法?」事實上你可以使用多態性(Polymorphism) 達到相同的效果。第二個問題來了,「為什麼我們需要這樣做呢?」依據前面概念,為了保持程式碼的乾淨,當類別或是函數出現 `if` 語法,代表你的函數做了超過一件事情。記住,一個函數只做一件事情!
**糟糕的:**
```javascript
class Airplane {
// ...
getCruisingAltitude() {
switch (this.type) {
case '777':
return this.getMaxAltitude() - this.getPassengerCount();
case 'Air Force One':
return this.getMaxAltitude();
case 'Cessna':
return this.getMaxAltitude() - this.getFuelExpenditure();
}
}
}
```
**適當的:**
```javascript
class Airplane {
// ...
}
class Boeing777 extends Airplane {
// ...
getCruisingAltitude() {
return this.getMaxAltitude() - this.getPassengerCount();
}
}
class AirForceOne extends Airplane {
// ...
getCruisingAltitude() {
return this.getMaxAltitude();
}
}
class Cessna extends Airplane {
// ...
getCruisingAltitude() {
return this.getMaxAltitude() - this.getFuelExpenditure();
}
}
```
**[⬆ 回到目錄](#目錄table-of-contents)**
### 避免型別(Type)檢查:第一部分
JavaScript 為弱型別語言,代表函數應能處理任何型別的引數(argument)。有時這會帶給你一些麻煩,讓你需要做型別檢查。有很多方法可以避免這種問題發生,第一種就是統一所有的 API。
**糟糕的:**
```javascript
function travelToTexas(vehicle) {
if (vehicle instanceof Bicycle) {
vehicle.pedal(this.currentLocation, new Location('texas'));
} else if (vehicle instanceof Car) {
vehicle.drive(this.currentLocation, new Location('texas'));
}
}
```
**適當的:**
```javascript
function travelToTexas(vehicle) {
vehicle.move(this.currentLocation, new Location('texas'));
}
```
**譯者附註**
此範例統一了所有的車輛移動的參數、方法與實作,所以不再需要區分不同的類別的車輛呼叫不同的方法。
**[⬆ 回到目錄](#目錄table-of-contents)**
### 避免型別檢查:第二部分
假設你需要型別檢查原始數值,像是字串與整數且你無法使用多態性處理,考慮使用 TypeScript 吧。他是提供標準 JavaScript 靜態類型的的最佳選擇。手動型別檢查需要很多額外處理,你得到的是虛假的型別安全,且失去的可讀性。保持你的 JavaScript 程式碼的整潔、寫好測試與足夠的程式碼審查(Code Review)。如果加上使用 TypeScript 會是更好的選擇。
**糟糕的:**
```javascript
function combine(val1, val2) {
if (
(typeof val1 === 'number' && typeof val2 === 'number') ||
(typeof val1 === 'string' && typeof val2 === 'string')
) {
return val1 + val2;
}
throw new Error('Must be of type String or Number');
}
```
**適當的:**
```javascript
function combine(val1, val2) {
return val1 + val2;
}
```
**[⬆ 回到目錄](#目錄table-of-contents)**
### 別過度優化
現代瀏覽器在運行時幫你做了很多優化。大多數的情況,你所做的優化都是在浪費你的時間。這裏有些很好的[資源](https://github.com/petkaantonov/bluebird/wiki/Optimization-killers),去了解哪些優化是無用的。
**糟糕的:**
```javascript
// 在舊的瀏覽器中並不會快取(cache)`list.length`,每次迭代(iteration)時的重新計算相當損耗效能。
// 這在新瀏覽器中已被優化,你不用手動去快取。
for (let i = 0, len = list.length; i < len; i++) {
// ...
}
```
**適當的:**
```javascript
for (let i = 0; i < list.length; i++) {
// ...
}
```
**譯者附註**
簡單來說,你不用在意程式語言層面上優化,因為這部分會因為版本更新而得到優化。但不要因此放棄所有的優化,演算法的優化才是你該注意的地方!
**[⬆ 回到目錄](#目錄table-of-contents)**
### 移除無用的程式碼(Dead Code)
沒有任何理由保留無用的程式碼,如果他們沒有被使用到,移除它!讓它們被保留在版本歷史中。
**糟糕的:**
```javascript
function oldRequestModule(url) {
// ...
}
function newRequestModule(url) {
// ...
}
const req = newRequestModule;
inventoryTracker('apples', req, 'www.inventory-awesome.io');
```
**適當的:**
```javascript
function newRequestModule(url) {
// ...
}
const req = newRequestModule;
inventoryTracker('apples', req, 'www.inventory-awesome.io');
```
**[⬆ 回到目錄](#目錄table-of-contents)**
## 物件(Objects)與資料結構(Data Structures)
### 使用 getters 與 setters
使用 `getters` 與 `setters` 來存取物件中資料,會比單純使用屬性(property)來的好。因為:
* 當你想要在取得物件屬性時做更多事情,你不用找出所有的程式碼修改。
* 透過 `set` 可以建立規則進行資料校驗。
* 封裝內部邏輯。
* 存取時增加日誌(logging)與錯誤處理(error handling)。
* 你可以延遲載入你的物件屬性,像是來自伺服器的資料。
**糟糕的:**
```javascript
function makeBankAccount() {
// ...
return {
balance: 0
// ...
};
}
const account = makeBankAccount();
account.balance = 100;
```
**適當的:**
```javascript
function makeBankAccount() {
// 私有變數
let balance = 0;
// 'getter',經由下方的返回物件對外公開
function getBalance() {
return balance;
}
// 'setter',經由下方的返回物件對外公開
function setBalance(amount) {
// ... 更新前先進行驗證
balance = amount;
}
return {
// ...
getBalance,
setBalance
};
}
const account = makeBankAccount();
account.setBalance(100);
```
**[⬆ 回到目錄](#目錄table-of-contents)**
### 讓物件擁有私有成員(members)