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