【JS】1150- 如何寫出更優(yōu)雅的 JavaScript 代碼
有人說好的代碼像一首詩,優(yōu)雅而有內(nèi)涵。
在日常開發(fā)中,維護別人老代碼的時候是不是總感覺邏輯混亂,無法入手,就跟屎山一樣?改一個 BUG 就如同在這座屎山上面艱難地再拉一坨……今天,我們從日常開發(fā)的角度上面談?wù)勅绾巫屪约旱拇a更清晰,更易于維護,讓別人看起來更有逼格。
變量命名
要寫出好代碼,變量命名至關(guān)重要。我們盡量采用富有表現(xiàn)力的詞,英文不好多用翻譯軟件,保證不出現(xiàn)錯誤單詞。編輯器可以安裝相關(guān)的拼寫檢查、翻譯插件。
不要縮寫/簡寫單詞,除非這些單詞已經(jīng)公認可以被這樣縮寫/簡寫。這樣做導(dǎo)致可讀性下降,意義表達不明確。反例: Association ass、StringBuilder sb普通變量命名則使用名詞及名詞短語。比如 value、options、fileText、columnName等boolean 命名,如果表示“是不是”用 is...,表示“有沒有”用has...,表示“能不能”用can...,表示“能不能怎么樣”用...ablefunction 命名采用動詞/賓語順序。比如 getUserInfo、insertRows、clearValue等避免使用 _開頭、temp、my之類命名臨時變量,臨時變量也是有意義的,這些都會增加閱讀代碼時的噪點避免無意義的命名,你起的每一個名字都要能表明意思。比如 userInfo、clickCount反例info、count
代碼結(jié)構(gòu)
好的代碼結(jié)構(gòu)有助于我們保持理性的思路,降低心智負擔(dān)。
使用 const 定義
如果沒有復(fù)雜的邏輯,用 const 就足夠了,這樣不用擔(dān)心變量被重新賦值而引起意外情況。當這種模式寫多之后,你會發(fā)現(xiàn)在項目中幾乎找不到幾個用 let 的地方。
//?Bad
let?result?=?false;
if?(userInfo.age?>?30)?{
??result?=?true;
}
//?Good
const?result?=?userInfo.age?>?30;
邏輯歸類
在復(fù)雜的邏輯中,相關(guān)的邏輯盡量放在一起,并插入空行分開。使代碼結(jié)構(gòu)看起來清晰明了。
提前返回
在 function 中經(jīng)常會遇到變量值為 undefined 的情況,這個時候則需要提前判斷并阻止執(zhí)行,避免一些不必要的分支(無 else),讓代碼更精煉。
if?(!userInfo)?{
??return;
}
if?(!hasMoney)?{
??return;
}
//?執(zhí)行業(yè)務(wù)邏輯
優(yōu)雅的條件判斷
簡單的判斷 if + return 就提前返回了。復(fù)雜邏輯 if else if 面條式代碼不夠優(yōu)雅,想用 switch case?實際情況看來 if else 和 switch case 用法區(qū)別不大。
//?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 case 比 if else 代碼行數(shù)還多,break 關(guān)鍵字也是必不可少,還不忘寫 default。這里我們推薦用 Object 或 Map 作為條件存儲。
const?actions?=?{
??1:?'processing',
??2:?'fail',
??3:?'success',
??4:?'cancel',
??default:?'other',
};
console.log(actions[status]????actions.default);
Map 則更為強大,對象的鍵只能是一個字符串或符號,但 Map 的鍵可以是對象或更多,可以作為條件聯(lián)合判斷。
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 循環(huán)可以用 map、 forEach 代替。
降低復(fù)雜度
一個邏輯的代碼行數(shù)越多,維護起來越困難,這個時候我們就需要將相關(guān)的邏輯抽離到另一個 function 中,從而降低上下文的復(fù)雜度。這里我們建議是一個 function 的代碼量在 120 個字符的寬度下不超過一個屏幕。
值得注意的是, function 所定義的形參最好控制在 3 個以內(nèi),否則容易疏忽傳入的順序,從而變得不易維護。如果參數(shù)太多就需要將相關(guān)的參數(shù)聚合成對象傳遞。
移除重復(fù)代碼
重復(fù)代碼在 Bad Smell 中排在第一位,所以,竭盡你的全力去避免重復(fù)代碼。因為它意味著當你需要修改一些邏輯時會有多個地方需要修改。
引入順序
在 import 中,我們約定將 node_modules 中的包放在前面,然后是相對路徑的包。有時一個 css 的 import 順序不同就會導(dǎo)致執(zhí)行的優(yōu)先級不同。
使用聲明式
聲明式編程:告訴“機器”你想要的是什么(what),讓機器想出如何去做(how)。命令式編程:命令“機器”如何去做事情(how),這樣不管你想要的是什么(what),它都會按照你的命令實現(xiàn)。世界很美妙,遠離命令式,節(jié)省時間體驗生活。
//?聲明式:篩選我需要的結(jié)果
const?result?=?dataSource.filter((dataItem)?=>?dataItem.age?>?10);
//?命令式:親力而為查找/追加數(shù)據(jù)
let?result?=?[];
dataSource.forEach((dataItem)?=>?{
??if?(dataItem.age?>?10)?{
????result.push(dataItem);
??}
});
這個時候有人就會說命令式編程性能好。其實我們寫代碼無需做過早優(yōu)化,那點性能損耗與可維護性比起來可以算是九牛一毛。
寫好業(yè)務(wù)注釋
優(yōu)秀的代碼命名無需注釋,代碼即注釋,加上注釋就會冗余。這時某個業(yè)務(wù)的邏輯就離不開準確的注釋,這樣可以幫助我們更加理解業(yè)務(wù)的詳細邏輯。需要要求的是代碼改動注釋也要隨之更新。
函數(shù)式編程
函數(shù)式編程獲得越來越多的關(guān)注,包括 react 都遵循這個理念。
函數(shù)是"第一等公民"
變量可以一個函數(shù),可以作為另一個函數(shù)的參數(shù)
function?increaseOperator(user)?{
??return?user.age?+?1;
}
userList.filter(Boolean).map(increaseOperator);
純函數(shù)
即相同輸入,永遠會得到相同輸出,而且沒有任何可觀察的副作用。如果使用了 setTimeout 、Promise 或更多具有意外情況發(fā)生的操作。那么這類操作被稱之為 "副作用" Effect。
每一個函數(shù)都可以被看做獨立單元。純函數(shù)的好處:方便組合、可緩存、可測試、引用透明、易于并發(fā)等等。
//?不純的,?minimum?可能被其他操作改變
let?minimum?=?21;
function?checkAge(age)?{
??return?age?>=?minimum;
}
//?純的
function?checkAge(age)?{
??const?minimum?=?21;
??return?age?>=?minimum;
}
比如 slice 和 splice, slice 符合純函數(shù)的定義是因為對相同輸入它保證能返回相同輸出。而 splice 卻會改變調(diào)用它的數(shù)組,這就會產(chǎn)生可觀察到的副作用,即這個原始數(shù)組永久地改變了。
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]
不可變數(shù)據(jù)
每次操作不修改原先的值,而是返回一個新的值,這與無副作用相呼應(yīng)。不可變數(shù)據(jù)模型易于調(diào)試,不用擔(dān)心當前數(shù)據(jù)被別的地方更改。
//?Bad?修改了參數(shù)
function?updateUser(user)?{
??user.age?=?10;
}
//?Good?返回新的對象
function?updateUser(user)?{
??return?{
????...user,
????age:?10,
??};
}
這里推薦 immer 作為函數(shù)式不可變數(shù)據(jù)操作。
完善的 TS 類型
typescript 擁有強大的類型系統(tǒng),彌補了 javascript 在類型上的短板。我們在寫 typescript 代碼的時候需要注意的是,不使用隱式/顯示的 any 類型,若有不確定類型的情況,首先考慮的是 泛型 去約束它,其次則用 unknown 加斷言,最后才是 any。
//?典型類型完備的函數(shù)
function?pick<T,?K?extends?keyof?T>(obj:?T,?keys:?K[])?{
??return?Object.fromEntries(keys.map((key)?=>?[key,?obj[key]]))?as?Pick;
}
pick(userInfo,?['email',?'name']);
結(jié)合 prettier + eslint
在團隊協(xié)作中,統(tǒng)一的代碼尤為重要,目前社區(qū)中的 eslint 規(guī)則層出不窮。其中 eslint-config-airbnb 限制最為嚴格,很多開源團隊都在用。這里我們可以與 prettier 搭配用,并禁用其中互相沖突的規(guī)則。編碼指南 https://github.com/airbnb/javascript
結(jié)語
想寫出優(yōu)雅的代碼其實很簡單,千里之行,始于足下。我們需要不斷優(yōu)化自己的代碼,不要畏懼改善代碼質(zhì)量所需付出的努力。理解這些準則并實踐,假以時日,相信我們每個人寫代碼都能做到像寫詩一樣行云流水。

回復(fù)“加群”與大佬們一起交流學(xué)習(xí)~
點擊“閱讀原文”查看 130+ 篇原創(chuàng)文章
