如何寫出更優雅的程式碼- JavaScript 篇

語言: CN / TW / HK

有人說好的程式碼像一首詩,優雅而有內涵。

在日常開發中,維護別人老程式碼的時候是不是總感覺邏輯混亂,無法入手。今天,我們從日常開發的角度上面談談如何讓自己的程式碼更清晰,更易於維護,讓別人看起來更有逼格。

變數命名

要寫出好程式碼,變數命名至關重要。我們儘量採用富有表現力的詞,英文不好多用翻譯軟體,保證不出現錯誤單詞。編輯器可以安裝相關的拼寫檢查、翻譯外掛。

  1. 不要縮寫/簡寫單詞,除非這些單詞已經公認可以被這樣縮寫/簡寫。這樣做導致可讀性下降,意義表達不明確。反例: Association assStringBuilder sb
  2. 普通變數命名則使用名詞及名詞短語。比如 valueoptionsfileTextcolumnName
  3. boolean 命名,如果表示“是不是”用 is...,表示“有沒有”用 has...,表示“能不能”用 can...,表示“能不能怎麼樣”用 ...able
  4. function 命名採用動詞/賓語順序。比如 getUserInfoinsertRowsclearValue
  5. 避免使用 _ 開頭、tempmy 之類命名臨時變數,臨時變數也是有意義的,這些都會增加閱讀程式碼時的噪點
  6. 避免無意義的命名,你起的每一個名字都要能表明意思。比如 userInfoclickCount 反例 infocount

程式碼結構

好的程式碼結構有助於我們保持理性的思路,降低心智負擔。

使用 const 定義

如果沒有複雜的邏輯,用 const 就足夠了,這樣不用擔心變數被重新賦值而引起意外情況。當這種模式寫多之後,你會發現在專案中幾乎找不到幾個用 let 的地方。

```typescript // Bad let result = false; if (userInfo.age > 30) {   result = true; }

// Good const result = userInfo.age > 30; ```

邏輯歸類

在複雜的邏輯中,相關的邏輯儘量放在一起,並插入空行分開。使程式碼結構看起來清晰明瞭。

提前返回

function 中經常會遇到變數值為 undefined 的情況,這個時候則需要提前判斷並阻止執行,避免一些不必要的分支(無 else),讓程式碼更精煉。

```typescript if (!userInfo) {   return; }

if (!hasMoney) {   return; }

// 執行業務邏輯 ```

優雅的條件判斷

簡單的判斷 if + return 就提前返回了。複雜邏輯 if else if 麵條式程式碼不夠優雅,想用 switch case? 實際情況看來 if elseswitch case 用法區別不大。

```typescript // if else if (status == 1) {   console.log('processing'); } else if (status == 2) {   console.log('fail'); } else if (status == 3) {   console.log('success'); } else if (status == 4) {   console.log('cancel'); } else {   console.log('other'); }

// switch case switch (status) {   case 1:     console.log('processing');     break;   case 2:     console.log('fail');     break;   case 3:     console.log('success');     break;   case 4:     console.log('cancel');     break;   default:     console.log('other');     break; } ```

在上面程式碼中可以看出 switch caseif else 程式碼行數還多,break 關鍵字也是必不可少,還不忘寫 default。這裡我們推薦用 ObjectMap 作為條件儲存。

```typescript const actions = {   1: 'processing',   2: 'fail',   3: 'success',   4: 'cancel',   default: 'other', };

console.log(actions[status] ?? actions.default); ```

Map 則更為強大,物件的鍵只能是一個字串或符號,但 Map 的鍵可以是物件或更多,可以作為條件聯合判斷。

```typescript const actions = new Map([   [/^sign_[1-3]$/, () => 'A'],   [/^sign_5$/, () => 'B'],   //... ]);

const action = [...actions].filter(([key, value]) => key.test(sign_${status})); action.forEach(([key, value]) => value()); ```

善用表示式

善用表示式,避免麵條式程式碼。簡單的條件判斷可以用三元運算子代替。 普通的 for 迴圈可以用 mapforEach 代替。

降低複雜度

一個邏輯的程式碼行數越多,維護起來越困難,這個時候我們就需要將相關的邏輯抽離到另一個 function 中,從而降低上下文的複雜度。這裡我們建議是一個 function 的程式碼量在 120 個字元的寬度下不超過一個螢幕。

值得注意的是, function 所定義的形參最好控制在 3 個以內,否則容易疏忽傳入的順序,從而變得不易維護。如果引數太多就需要將相關的引數聚合成物件傳遞。

移除重複程式碼

重複程式碼在 Bad Smell 中排在第一位,所以,竭盡你的全力去避免重複程式碼。因為它意味著當你需要修改一些邏輯時會有多個地方需要修改。

引入順序

import 中,我們約定將 node_modules 中的包放在前面,然後是相對路徑的包。有時一個 cssimport 順序不同就會導致執行的優先順序不同。

使用宣告式

宣告式程式設計:告訴“機器”你想要的是什麼(what),讓機器想出如何去做(how)。指令式程式設計:命令“機器”如何去做事情(how),這樣不管你想要的是什麼(what),它都會按照你的命令實現。世界很美妙,遠離命令式,節省時間體驗生活。

```typescript // 宣告式:篩選我需要的結果 const result = dataSource.filter((dataItem) => dataItem.age > 10);

// 命令式:親力而為查詢/追加資料 let result = []; dataSource.forEach((dataItem) => {   if (dataItem.age > 10) {     result.push(dataItem);   } }); ```

這個時候有人就會說指令式程式設計效能好。其實我們寫程式碼無需做過早優化,那點效能損耗與可維護性比起來可以算是九牛一毛。

寫好業務註釋

優秀的程式碼命名無需註釋,程式碼即註釋,加上註釋就會冗餘。這時某個業務的邏輯就離不開準確的註釋,這樣可以幫助我們更加理解業務的詳細邏輯。需要要求的是程式碼改動註釋也要隨之更新。

函數語言程式設計

函數語言程式設計獲得越來越多的關注,包括 react 都遵循這個理念。

函式是"第一等公民"

變數可以一個函式,可以作為另一個函式的引數

```typescript function increaseOperator(user) {   return user.age + 1; }

userList.filter(Boolean).map(increaseOperator); ```

純函式

即相同輸入,永遠會得到相同輸出,而且沒有任何可觀察的副作用。如果使用了 setTimeoutPromise 或更多具有意外情況發生的操作。那麼這類操作被稱之為 "副作用" Effect

每一個函式都可以被看做獨立單元。純函式的好處:方便組合、可快取、可測試、引用透明、易於併發等等。

```typescript // 不純的, minimum 可能被其他操作改變 let minimum = 21;

function checkAge(age) {   return age >= minimum; }

// 純的 function checkAge(age) {   const minimum = 21;   return age >= minimum; } ```

比如 slicespliceslice 符合純函式的定義是因為對相同輸入它保證能返回相同輸出。而 splice 卻會改變呼叫它的陣列,這就會產生可觀察到的副作用,即這個原始陣列永久地改變了。

```typescript var countList = [1, 2, 3, 4, 5];

// 純的 countList.slice(0, 3); //=> [1, 2, 3]

countList.slice(0, 3); //=> [1, 2, 3]

// 不純的 countList.splice(0, 3); //=> [1, 2, 3]

countList.splice(0, 3); //=> [4, 5] ```

不可變資料

每次操作不修改原先的值,而是返回一個新的值,這與無副作用相呼應。不可變資料模型易於除錯,不用擔心當前資料被別的地方更改。

```typescript // Bad 修改了引數 function updateUser(user) {   user.age = 10; }

// Good 返回新的物件 function updateUser(user) {   return {     ...user,     age: 10,   }; } ```

這裡推薦 immer 作為函式式不可變資料操作。

完善的 TS 型別

typescript 擁有強大的型別系統,彌補了 javascript 在型別上的短板。我們在寫 typescript 程式碼的時候需要注意的是,不使用隱式/顯示的 any 型別,若有不確定型別的情況,首先考慮的是 泛型 去約束它,其次則用 unknown 加斷言,最後才是 any

```typescript // 典型型別完備的函式 function pick(obj: T, keys: K[]) {   return Object.fromEntries(keys.map((key) => [key, obj[key]])) as Pick; }

pick(userInfo, ['email', 'name']); ```

結合 prettier + eslint

在團隊協作中,統一的程式碼尤為重要,目前社群中的 eslint 規則層出不窮。其中 eslint-config-airbnb 限制最為嚴格,很多開源團隊都在用。這裡我們可以與 prettier 搭配用,並禁用其中互相沖突的規則。編碼指南 https://github.com/airbnb/javascript

結語

想寫出優雅的程式碼其實很簡單,千里之行,始於足下。我們需要不斷優化自己的程式碼,不要畏懼改善程式碼質量所需付出的努力。理解這些準則並實踐,假以時日,相信我們每個人寫程式碼都能做到像寫詩一樣行雲流水。