此為摘自 ==淘寶前端團隊(FFD)==
編寫可讀的代碼:
對于以代碼為生的程序員而已珍德,編寫可讀的代碼摔竿,是一件極其重要的事情真慢。從某種角度來說,代碼最重要的功能是能夠被閱讀榕吼,其次才是能被正確執(zhí)行饿序。一段無法正確執(zhí)行的代碼,也許會使項目延誤幾天羹蚣,但造成的危害只是暫時和短暫的原探,而一段缺乏條理,難以閱讀的代碼顽素,造成的危害卻是深遠而長久的咽弦。這里總結了下,在工作和業(yè)余生活中胁出,我對如何編寫可讀代碼這個問題的具體體會型型。
- 變量命名
變量命名是編寫可讀代碼的基礎。只有變量被賦予一個合適的名字全蝶,才能表達它在當前環(huán)境中的意義闹蒜。
命名必須傳遞足夠的信息,形如getData
這個的函數(shù)命名就沒能夠提供足夠的信息裸诽,這讓讀者無法猜測這個函數(shù)會做出些什么事情嫂用。而fetchUserInfoAsync
也許就好很多型凳,讀者至少能猜到丈冬,這個函數(shù)大約是遠程獲取用戶信息。
- 命名的基礎
通常甘畅,我們會使用 名詞 來命名對象埂蕊,用 動詞 來命名函數(shù)往弓。比如:
monkey.eat(banana) // the monkey eats a banana
const apple = pick(tree) // pick an apple from the tree
有時候,我們需要表示某種集合的概念蓄氧,比如數(shù)組或者哈希對象函似。這是可以使用名詞的復數(shù)形式來表示,比如bananas
表示一個數(shù)組喉童。如果需要特別強調(diào)這種集合的形式撇寞,也可以加上 List
或 Map
后綴來表示,比如 bananaList
表示數(shù)組堂氯。 如果有些單詞的復數(shù)形式和單數(shù)形式相同蔑担,比如data
, information
等,這時就用 List
為后綴表示集合概念咽白。
- 命名的上下文
變量都是處在 上下文 (作用域)內(nèi)的啤握,變量的命名應該和上下文相契合,同一個變量晶框,在不同的上下文排抬,命名可以不同。
- 嚴格遵循一種命名規(guī)范的收益
如果你能夠時刻按照某種嚴格的規(guī)范來命名變量和函數(shù)授段,還能帶來一個潛在的好處蹲蒲,那就是你再也不用 記住 那些之前命名過或者其他人命名過的變量和函數(shù)了。比如畴蒲,【獲取用戶信息】這個概念悠鞍,就叫作fetchUserInfomation
,不管是在早晨還是晚上模燥,家里還是在公司里咖祭,它都是命名為 fetchUserInfomation
。
- 分支結構
分支是代碼里最常見的結構蔫骂,一段結構清晰的代碼單元么翰,應當是像二叉樹一樣,呈現(xiàn)下面的結構辽旋。
if(condition1){
if(condition2){
...
} else {
...
}
}else {
if(condition3){
...
}else {
...
}
}
這種優(yōu)美的結構能幫助我們在大腦中迅速繪制一張圖浩嫌,便于在腦海中模擬代碼的執(zhí)行。但是补胚,我們大多數(shù)人都不會遵循上面這樣的結構來寫分支代碼码耐。以下是一些常見的,可讀性比較差的分支語句的寫法:
不好的做法:在分支中return
function foo(){
if (condition){
// 分支1的邏輯
return
}
// 分支2的邏輯
}
這種代碼很常見溶其,而且往往分支2的邏輯是先寫的骚腥,也是函數(shù)的主要邏輯。這種致命的問題就是瓶逃,如果讀者沒有注意到分支1中的 return
束铭,就不會意識到后面一段代碼(分支2)是有可能不被執(zhí)行的廓块。建議是把分支2的代碼寫著else
模塊中,代碼就會清晰可讀:
function foo(){
if(condition){
// 分支1的邏輯
}else {
// 分支2的邏輯
}
}
如果某個分支為空契沫,最好留一個空行带猴,明確地告訴代碼的讀者,
如果走到這個 else 懈万,什么也不做拴清。這樣讀者就不會產(chǎn)生任何懷疑。
不好的做法:多個條件的復合
if(condition1 && condition2 && condition3 ){
// 分支1:做些事情
} else {
//分支2:做些事情
}
這種代碼也很常見:在若干條件同時滿足(或有任一滿足) 的時候做一些主要事情(分支1)会通,否則做一些次要事情(分支2)贷掖。這樣籠統(tǒng)的使用同一段代碼來處理多個分支,那么就增加了閱讀者閱讀分支2時的負擔(需要考慮多個情況)渴语。對于這種場景苹威,通常這樣寫:
if(condition1){
if(condition2){
// 分支1: 做一些事情
}else {
// 分支2:做一些事情
}
} else {
//分支3 :做一些事情
}
即使分支2和分支3完全一樣,我也認為有必要分開驾凶。不過對于那種多個復合條件聯(lián)系十分緊密牙甫,就沒必要分開寫,比如if(foo && foo.bar)
调违。
不好的做法:使用分支改變環(huán)境
let foo = someValue;
if (condition){
foo = doSomethingTofoo(foo)
}
// 繼續(xù)使用 foo 做一些事情
這種風格的代碼很容易出現(xiàn)在那些屢經(jīng)修改的代碼文件中窟哺,很可能一開始是沒有這個 if
代碼塊的,后來發(fā)現(xiàn)了一個bug,于是加上了這個if
代碼塊技肩。
事實上且轨,這樣的「補丁」積累起來,很快就會摧毀代碼的可讀性和可維護性虚婿。怎么說呢旋奢?當我們在寫下上面這段代碼中的 if 分支以試圖修復 bug 的時候,我們內(nèi)心存在這樣一個假設:我們是知道程序在執(zhí)行到這一行時然痊,foo 什么樣子的至朗;但事實是,我們根本不知道剧浸,因為在這一行之前锹引,foo 很可能已經(jīng)被另一個人所寫的嘗試修復另一個 bug 的另一個 if 分支所篡改了。所以唆香,當代碼出現(xiàn)問題的時候嫌变,我們應當完整地審視一段獨立的功能代碼(通常是一個函數(shù)),并且多花一點時間來修復他躬它,比如:
const foo = condition ? doSomethingToFoo(someValue) : someValue;
我們看到腾啥,很多風險都是在項目快速迭代的過程中積累下來的。為了「快速」迭代,在添加功能代碼的時候碑宴,我們有時候連函數(shù)這個最小單元的都不去了解,僅僅著眼于自己插入的那幾行桑谍,希望在那幾行中解決/hack掉所有問題延柠,這是十分不可取的。
我認為锣披,項目的迭代再快贞间,其代碼質(zhì)量和可讀性都應當有一個底線。這個底線是雹仿,當我們在修改代碼的時候增热,應當完整了解當前修改的這個函數(shù)的邏輯,然后修改這個函數(shù)胧辽,以達到添加功能的目的峻仇。注意,這里的「修改一個函數(shù)」和「在函數(shù)某個位置添加幾行代碼」是不同的邑商,在「修改一個函數(shù)」的時候摄咆,為了保證函數(shù)功能獨立,邏輯清晰人断,不應該畏懼在這個函數(shù)的任意位置增刪代碼吭从。
- 函數(shù)
一個函數(shù)只做一件事情
有時,我們會自作聰明的寫一些很【通用】 的函數(shù)恶迈。比如涩金,我們有可能寫出下面這樣一個獲取用戶信息的函數(shù) fetchUserInfo
:其邏輯是:
1、當傳入的參數(shù)是用戶ID(字符串)時暇仲,返回單個用戶的數(shù)據(jù)步做。
2、當傳入的參數(shù)是用戶ID的列表(數(shù)組)時奈附,返回一個數(shù)組辆床,其中每項包含一個用戶的數(shù)據(jù)
sync function fetchUserInfo (id){
const isSingle = typeof id ==="string";
const idList = isSingle ? [id] : id;
const result = await request.post('/api/userInfo',{idList});
return isSingle ? result[0] :result;
}
// 可以這樣調(diào)用
const userList = await fetchUserInfo(['1001','1013']);
// 也可以這樣調(diào)用
const user = await fetchUserInfo('1013')
這個函數(shù)能做兩件事:1)獲取多個用戶列表的數(shù)據(jù);2)獲取單個用戶數(shù)據(jù)桅狠。這樣讀者在某處讀到 fetchUserInfo(['1001','1013'])
這句調(diào)用代碼時讼载,會立刻對 fetchUserInfo
產(chǎn)生第一印象:這個函數(shù)是需要出入用戶ID數(shù)組的;而當讀到另一種調(diào)用方式時中跌,就會懷疑自己之前的判斷咨堤。
遵循一個函數(shù)只做一件事 的原則,我們可以將上述功能拆分成兩個函數(shù) fetchMultipleUser
和 fetchSingleUser
來實現(xiàn)漩符。
async function fetchMultipleUser(idList){
return await request.post('/api/users/',{idList})
}
async function fetchSingleUser(id){
return await fetchMultipleUser([id])[0]
}
改良后的代碼不僅改善了代碼的可讀性一喘,也改善了可維護性,當不需要獲取單一用戶信息時,就可以放心大膽的直接刪掉整個函數(shù)凸克。
如何界定某個函數(shù)做的是不是一件事情议蟆?
作者的經(jīng)驗是:如果一個函數(shù)的參數(shù)僅僅包含輸入數(shù)據(jù)(交給函數(shù)處理的數(shù)據(jù)),而沒有混雜或暗含有指令(以某種約定的方式告訴函數(shù)該怎么處理數(shù)據(jù))萎战,那么函數(shù)做的應當就是一件事情咐容。比如說,改良前的 fetchUserInfo
函數(shù)的參數(shù)是【多個用戶ID數(shù)組或單個用戶的ID】蚂维,這個【或】字其實就暗含了某種指令戳粒。
函數(shù)應適當?shù)靥幚懋惓?/strong>
有時候,我們會陷入一種很不好的習慣中虫啥,那就是蔚约,總是去嘗試寫出永遠不會報錯的函數(shù)。我們會給參數(shù)配上默認值涂籽,在很多地方使用 ||
或者 &&
來避免代碼運行出錯苹祟,仿佛如果你的函數(shù)報錯會成為某種恥辱似的。而且评雌,當我們嘗試去修復一個運行時報錯的函數(shù)時苔咪,我們往往傾向于在報錯的那一行添加一些兼容邏輯來避免報錯。
舉個例子柳骄,假設我們需要編寫一個獲取用戶詳情的函數(shù)团赏,它要返回一個完整的用戶信息對象:不僅包含ID,名字等基本信息耐薯,也包含諸如「收藏的書籍」等通過額外接口返回的信息舔清。這些額外的接口也許不太穩(wěn)定:
async function getUserDetail(id) {
const user = await fetchSingleUser(id);
user.favoriteBooks = (await fetchUserFavorits(id)).books;
// 上面這一行報錯了:Can not read property 'books' of undefined.
// ...
}
假設 fetchUserFavorites 會時不時地返回 undefined,那么讀取其 books 屬性自然就會報錯曲初。為了修復該問題体谒,我們很可能會這樣做:
const favorites = await fetchUserFavorits(id);
user.favoriteBooks = favorites && favorites.books;
// 這下不會報錯了
這樣做看似解決了問題:的確,getUserDetail 不會再報錯了臼婆,但同時埋下了更深的隱患抒痒。
當 fetchUserFavorites 返回 undefined 時,程序已經(jīng)處于一種異常狀態(tài)了颁褂,我們沒有任何理由放任程序繼續(xù)運行下去故响。試想,如果后面的某個時刻(比如用戶點擊「我收藏的書」選項卡)颁独,程序試圖遍歷 user.favoriteBooks 屬性(它被賦值成了undefined)彩届,那時也會報錯,而且那時排查起來會更加困難誓酒。
如何處理上述的情況呢樟蠕?我認為,如果被我們依賴的 fetchUserFavorits 屬于當前的項目,那么 getUserDetail 對此報錯真的沒什么責任寨辩,因為 fetchUserFavorits 就不應該返回 undefined吓懈,我們應該去修復 fetchUserFavorits,任務失敗時顯式地告知出來靡狞,或者直接拋出異常耻警。同時,getUserDetail 稍作修改:
// 情況1:顯式告知耍攘,此時應認為獲取不到收藏數(shù)據(jù)不算致命的錯誤
const result = await fetchUserFavorits(id);
if(result.success) {
user.favoriteBooks = result.data.books;
} else {
user.favoriteBooks = []
}
// 情況2:直接拋出異常
user.favoriteBooks = (await fetchUserFavorits(id)).books;
// 這時 `getUserDetail` 不需要改動,任由異常沿著調(diào)用棧向上冒泡
那么如果 fetchUserFavorits 不在當前項目中畔勤,而是依賴的外部模塊呢蕾各?我認為,這時你就該為選擇了這樣一個不可靠的模塊負責庆揪,在 getUserDetail 中增加一些「擦屁股」代碼式曲,來避免你的項目的其他部分受到侵害。
const favorites = await fetchUserFavorits(id);
if(favorites) {
user.favoriteBooks = favorites.books;
} else {
throw new Error('獲取用戶收藏失敗');
}
控制函數(shù)的副作用
無副作用的函數(shù)缸榛,是不依賴上下文吝羞,也不改變上下文的函數(shù)。長久依賴内颗,我們已經(jīng)習慣了去寫「有副作用的函數(shù)」钧排,畢竟 JavaScript 需要通過副作用去操作環(huán)境的 API 完成任務。這就導致了均澳,很多原本可以用純粹的恨溜、無副作用的函數(shù)完成任務的場合,我們也會不自覺地采取有副作用的方式找前。
雖然看上去有點可笑糟袁,但我們有時候就是會寫出下面這樣的代碼!
async function getUserDetail(id) {
const user = await fetchSingleUserInfo(id);
await addFavoritesToUser(user);
...
}
async function addFavoritesToUser(user) {
const result = await fetchUserFavorits(user.id);
user.favoriteBooks = result.books;
user.favoriteSongs = result.songs;
user.isMusicFan = result.songs.length > 100;
}
上面躺盛,addFavoritesToUser 函數(shù)就是一個「有副作用」的函數(shù)项戴,它改變了 users,給它新增了幾個個字段槽惫。問題在于周叮,僅僅閱讀 getUserData 函數(shù)的代碼完全無法知道,user 會發(fā)生怎樣的改變界斜。
一個無副作用的函數(shù)應該是這樣的:
async function getUserDetail(id) {
const user = await fetchSingleUserInfo(id);
const {books, songs, isMusicFan} = await getUserFavorites(id);
return Object.assign(user, {books, songs, isMusicFan})
}
async function getUserFavorites(id) {
const {books, songs} = await fetchUserFavorits(user.id);
return {
books, songs, isMusicFan: result.songs.length > 100
}
}
非侵入性地改造函數(shù)
函數(shù)是一段獨立和內(nèi)聚的邏輯则吟。在產(chǎn)品迭代的過程中,我們有時候不得不去修改函數(shù)的邏輯锄蹂,為其添加一些新特性氓仲。之前我們也說過,一個函數(shù)只應做一件事,如果我們需要添加的新特性敬扛,與原先函數(shù)中的邏輯沒有什么聯(lián)系晰洒,那么決定是否通過改造這個函數(shù)來添加新功能,應當格外謹慎啥箭。
仍然用「向服務器查詢用戶數(shù)據(jù)」為例谍珊,假設我們有如下這樣一個函數(shù)(為了讓它看上去復雜一些,假設我們使用了一個更基本的 request 庫):
const fetchUserInfo = (userId, callback) => {
const param = {
url: '/api/user',
method: 'post',
payload: {id: userId}
};
request(param, callback);
}
現(xiàn)在有了一個新需求:為 fetchUserInfo 函數(shù)增加一道本地緩存急侥,如果第二次請求同一個 userId 的用戶信息砌滞,就不再重新向服務器發(fā)起請求,而直接以第一次請求得到的數(shù)據(jù)返回坏怪。
按照如下快捷簡單的解決方案贝润,改造這個函數(shù)只需要五分鐘時間:
const userInfoMap = {};
const fetchUserInfo = (userId, callback) => {
if (userInfoMap[userId]) { // 新增代碼
callback(userInfoMap[userId]); // 新增代碼
} else { // 新增代碼
const param = {
// ... 參數(shù)
};
request(param, (result) => {
userInfoMap[userId] = result; // 新增代碼
callback(result);
});
}
}
不知你有沒有發(fā)現(xiàn),經(jīng)此改造铝宵,這個函數(shù)的可讀性已經(jīng)明顯降低了打掘。沒有緩存機制前,函數(shù)很清晰鹏秋,一眼就能明白尊蚁,加上新增的幾行代碼,已經(jīng)不能一眼就看明白了侣夷。
實際上横朋,「緩存」和「獲取用戶數(shù)據(jù)」完全是獨立的兩件事。我提出的方案是百拓,編寫一個通用的緩存包裝函數(shù)(類似裝飾器)memorizeThunk叶撒,對 fetchUserInfo 進行包裝,產(chǎn)出一個新的具有緩存功能的 fetchUserInfoCache耐版,在不破壞原有函數(shù)可讀性的基礎上祠够,提供緩存功能。
const memorizeThunk = (func, reducer) => {
const cache = {};
return (...args, callback) => {
const key = reducer(...args);
if (cache[key]) {
callback(...cache[key]);
} else {
func(...args, (...result) => {
cache[key] = result;
callback(...result);
});
}
}
}
const fetchUserInfo = (userInfo, callback) => {
// 原來的邏輯
}
const fetchUserInfoCache = memorize(fetchUserInfo, (userId) => userId);
也許實現(xiàn)這個方案需要十五分鐘粪牲,但是試想一下古瓤,如果將來的某個時候,我們又不需要緩存功能了(或者需要提供一個開關來打開/關閉緩存功能)腺阳,修改代碼的負擔是怎樣的落君?第一種簡單方案,我們需要精準(提心吊膽地)地刪掉新增的若干行代碼亭引,而我提出的這種方案绎速,是以函數(shù)為單位增刪的,負擔要輕很多焙蚓,不是嗎纹冤?
類的結構
避免濫用成員函數(shù)
總結
偉大的文學作品都是建立在廢紙堆上的洒宝,不斷刪改作品的過程有助于寫作者培養(yǎng)良好的「語感」。當然萌京,代碼畢竟不是藝術品雁歌,程序員沒有精力也不一定有必要像作家一樣反復打磨自己的代碼/作品。但是知残,如果我們能夠在編寫代碼時稍稍多考慮一下實現(xiàn)的合理性靠瞎,或者在添加新功能的時候稍稍回顧一下之前的實現(xiàn),我們就能夠培養(yǎng)出一些「代碼語感」求妹。這種「代碼語感」會非常有助于我們寫出高質(zhì)量的可讀的代碼乏盐。