譯者:墨白 校對:野草
本文已在前端早讀課公眾號首發(fā):【第952期】JavaScript代碼風(fēng)格要素

1920年,由威廉·斯特倫克(William Strunk jr .)撰寫的《英語寫作手冊:風(fēng)格的要素(The Elements of Style)》出版了,這本書列舉了7條英文寫作的準(zhǔn)則,過了一個世紀(jì),這些準(zhǔn)則并沒有過時。對于工程師來說,你可以在自己的編碼風(fēng)格中應(yīng)用類似的建議來指導(dǎo)日常的編碼,提高自己的編碼水平。
需要注意的是,這些準(zhǔn)則不是一成不變的法則。如果違背它們,能夠讓代碼可讀性更高,那么便沒有問題,但請?zhí)貏e小心并時刻反思。這些準(zhǔn)繩是經(jīng)受住了時間考驗的,有充分的理由說明:它們通常是正確的。如果要違背這些規(guī)則,一定要有充足的理由,而不要單憑一時的興趣或者個人的風(fēng)格偏好。
書中的寫作準(zhǔn)則如下:
- 以段落為基本單位:一段文字,一個主題。
- 刪減無用的語句。
- 使用主動語態(tài)。
- 避免一連串松散的句子。
- 相關(guān)的內(nèi)容寫在一起。
- 從正面利用肯定語句去發(fā)表陳述。
- 不同的概念采用不同的結(jié)構(gòu)去闡述。
我們可以應(yīng)用相似的理念到代碼編寫上面:
- 一個function只做一件事,讓function成為代碼組合的最小單元。
- 刪除不必要的代碼。
- 使用主動語態(tài)。
- 避免一連串結(jié)構(gòu)松散的,不知所云的代碼。
- 將相關(guān)的代碼寫在一起。
- 利用判斷true值的方式來編寫代碼。
- 不同的技術(shù)方案利用不同的代碼組織結(jié)構(gòu)來實現(xiàn)。
1.一個function只做一件事,讓function成為代碼組合的最小單元
軟件開發(fā)的本質(zhì)是“組合”。 我們通過組合模塊,函數(shù)和數(shù)據(jù)結(jié)構(gòu)來構(gòu)建軟件。理解如果編寫以及組合方法是軟件開發(fā)人員的基本技能。
模塊是一個或多個function和數(shù)據(jù)結(jié)構(gòu)的簡單集合,我們用數(shù)據(jù)結(jié)構(gòu)來表示程序狀態(tài),只有在函數(shù)執(zhí)行之后,程序狀態(tài)才會發(fā)生一些有趣的變化。
JavaScript中,可以將函數(shù)分為3種:
- I/O 型函數(shù) (Communicating Functions):函數(shù)用來執(zhí)行I/O。
- 過程型函數(shù) (Procedural Functions):對一系列的指令序列進(jìn)行分組。
- 映射型函數(shù) (Mapping Functions):給定一些輸入,返回對應(yīng)的輸出。
有效的應(yīng)用程序都需要I/O,并且很多程序都遵循一定的程序執(zhí)行順序,這種情況下,程序中的大部分函數(shù)都會是映射型函數(shù):給定一些輸入,返回相應(yīng)的輸出。
每個函數(shù)只做一件事情:如果你的函數(shù)主要用于I/O,就不要在其中混入映射型代碼,反之亦然。嚴(yán)格根據(jù)定義來說,過程型函數(shù)違反了這一指導(dǎo)準(zhǔn)則,同時也違反了另一個指導(dǎo)準(zhǔn)則:避免一連串結(jié)構(gòu)松散,不知所云的代碼。
理想中的函數(shù)是一個簡單的、明確的純函數(shù):
- 同樣的輸入,總是返回同樣的輸出。
- 無副作用。
也可以查看,“什么是純函數(shù)?”
2. 刪除不必要的代碼
簡潔的代碼對于軟件而言至關(guān)重要。更多的代碼意味更多的bug隱藏空間。更少的代碼 = 更少的bug隱藏空間 = 更少的bug
簡潔的代碼讀起來更清晰,因為它擁有更高的“信噪比”:閱讀代碼時更容易從較少的語法噪音中篩選出真正有意義的部分。可以說,更少的代碼 = 更少的語法噪聲 = 更強(qiáng)的代碼含義信息傳達(dá)
借用《風(fēng)格的元素》這本書里面的一句話就是:簡潔的代碼更健壯。
function secret (message) {
return function () {
return message;
}
};
可以簡化成:
const secret = msg => () => msg;
對于那些熟悉簡潔箭頭函數(shù)寫法的開發(fā)來說,可讀性更好。它省略了不必要的語法:大括號,function
關(guān)鍵字以及return
語句。
而簡化前的代碼包含的語法要素對于傳達(dá)代碼意義本身作用并不大。它存在的唯一意義只是讓那些不熟悉ES6語法的開發(fā)者更好的理解代碼。
ES6自2015年已經(jīng)成為語言標(biāo)準(zhǔn),是時候去學(xué)習(xí)它了。
刪除不必要的代碼
有時候,我們試圖為不必要的事物命名。問題是人類的大腦在工作中可用的記憶資源有限,每個名稱都必須作為一個單獨(dú)的變量存儲,占據(jù)工作記憶的存儲空間。
由于這個原因,有經(jīng)驗的開發(fā)者會盡可能地刪除不必要的變量。
例如,大多數(shù)情況下,你應(yīng)該省略僅僅用來當(dāng)做返回值的變量。你的函數(shù)名應(yīng)該已經(jīng)說明了關(guān)于函數(shù)返回值的信息。看看下面的:
const getFullName = ({firstName, lastName}) => {
const fullName = firstName + ' ' + lastName;
return fullName;
};
對比
const getFullName = ({firstName, lastName}) => (
firstName + ' ' + lastName
);
另一個開發(fā)者通常用來減少變量名的做法是,利用函數(shù)組合以及point-free-style
。
Point-free-style
是一種定義函數(shù)方式,定義成一種與參數(shù)無關(guān)的合成運(yùn)算。實現(xiàn)point-free
風(fēng)格常用的方式包括函數(shù)科里化以及函數(shù)組合。
讓我們來看一個函數(shù)科里化的例子:
const add2 = a => b => a + b;
// Now we can define a point-free inc()
// that adds 1 to any number.
const inc = add2(1);
inc(3); // 4
看一下inc()
函數(shù)的定義方式。注意,它并未使用function
關(guān)鍵字,或者=>
語句。add2也沒有列出一系列的參數(shù),因為該函數(shù)不在其內(nèi)部處理一系列的參數(shù),相反,它返回了一個知道如何處理參數(shù)的新函數(shù)。
函數(shù)組合是將一個函數(shù)的輸出作為另一函數(shù)的輸入的過程。 也許你沒有意識到,你一直在使用函數(shù)組合。鏈?zhǔn)秸{(diào)用的代碼基本都是這個模式,比如數(shù)組操作時使用的.map()
,Promise 操作時的promise.then()
。函數(shù)組合在函數(shù)式語言中也被稱之為高階函數(shù),其基本形式為:f(g(x))。
當(dāng)兩個函數(shù)組合時,無須創(chuàng)建一個變量來保存兩個函數(shù)運(yùn)行時的中間值。我們來看看函數(shù)組合是怎么減少代碼的:
const g = n => n + 1;
const f = n => n * 2;
// 需要操作參數(shù)、并且存儲中間結(jié)果
const incThenDoublePoints = n => {
const incremented = g(n);
return f(incremented);
};
incThenDoublePoints(20); // 42
// compose2 - 接受兩個函數(shù)作為參數(shù),直接返回組合
const compose2 = (f, g) => x => f(g(x));
const incThenDoublePointFree = compose2(f, g);
incThenDoublePointFree(20); // 42
你可以利用函子(functor)來做同樣的事情。在函子中把參數(shù)封裝成可遍歷的數(shù)組。讓我們利用函子來寫另一個版本的compose2
:
const compose2 = (f, g) => x => [x].map(g).map(f).pop();
const incThenDoublePointFree = compose2(f, g);
incThenDoublePointFree(20); // 42
當(dāng)每次使用promise鏈時,你就是在做這樣的事情。
幾乎每一個函數(shù)式編程類庫都提供至少兩種函數(shù)組合方法:從右到左依次運(yùn)行的compose()
;從左到右依次運(yùn)行的pipe()
。
Lodash中的compose()
以及flow()
分別對應(yīng)這兩個方法。下面是使用pipe
的例子:
import pipe from 'lodash/fp/flow';
pipe(g, f)(20); // 42
下面的代碼也做著同樣的事情,但代碼量并未增加太多:
const pipe = (...fns) => x => fns.reduce((acc, fn) => fn(acc), x);
pipe(g, f)(20); // 42
如果函數(shù)組合這個名詞聽起來很陌生,你不知道如何使用它,請仔細(xì)想一想:
軟件開發(fā)的本質(zhì)是組合,我們通過組合較小的模塊,方法以及數(shù)據(jù)結(jié)構(gòu)來構(gòu)建應(yīng)用程序。
不難推論,工程師理解函數(shù)和對象組合這一編程技巧就如同搞裝修需要理解鉆孔機(jī)以及氣槍一樣重要。
當(dāng)你利用“命令式”代碼將功能以及中間變量拼湊在一起時,就像瘋狂使用膠帶和膠水將這些部分胡亂粘貼起來一樣,而函數(shù)組合看上去更流暢。
記住:
- 用更少的代碼。
- 用更少的變量。
3. 使用主動語態(tài)
主動語態(tài)比被動語態(tài)更直接,跟有力量,盡量多直接命名事物:
-
myFunction.wasCalled()
優(yōu)于myFunction.hasBeenCalled()
-
createUser
優(yōu)于User.create()
-
notify()
優(yōu)于Notifier.doNotification()
命名布爾返回值時最好直接反應(yīng)其輸出的類型:
-
isActive(user)
優(yōu)于getActiveStatus(user)
-
isFirstRun = false;
優(yōu)于firstRun = false;
函數(shù)名采用動詞形式:
-
increment()
優(yōu)于plusOne()
-
unzip()
優(yōu)于filesFromZip()
-
filter(fn, array)
優(yōu)于matchingItemsFromArray(fn, array)
事件處理
事件處理以及生命周期函數(shù)由于是限定符,比較特殊,就不適用動詞形式這一規(guī)則;相比于“做什么”,它們主要用來表達(dá)“什么時候做”。對于它們,可以“<什么時候去做>,<動作>”這樣命名,朗朗上口。
-
element.onClick(handleClick)
優(yōu)于element.click(handleClick)
-
element.onDragStart(handleDragStart)
優(yōu)于component.startDrag(handleDragStart)
上面兩例的后半部分,它們讀起來更像是正在嘗試去觸發(fā)一個事件,而不是對其作出回應(yīng)。
生命周期函數(shù)
對于組件生命周期函數(shù)(組件更新之前調(diào)用的方法),考慮一下以下的命名:
componentWillBeUpdated(doSomething)
componentWillUpdate(doSomething)
beforeUpdate(doSomething)
第一個種我們使用了被動語態(tài)(將要被更新而不是將要更新)。這種方式很口語化,但含義表達(dá)并沒有比其它兩種方式更清晰。
第二種就好多了,但生命周期函數(shù)的重點(diǎn)在于觸發(fā)處理事件。componentWillUpdate(handler)
讀起來就好像它將立即觸發(fā)一個處理事件,但這不是我們想要表達(dá)的。我們想說,“在組件更新之前,觸發(fā)事件”。beforeComponentUpdate()
能更清楚的表達(dá)這一想法。
進(jìn)一步簡化,因為這些方法都是組件內(nèi)置的。在方法名中加入component是多余的。想一想如果你直接調(diào)用這些方法時:component.componentWillUpdate()
。這就好像在說,“吉米吉米在晚餐吃牛排。”你沒有必要聽到同一個對象的名字兩次。顯然,
-
component.beforeUpdate(doSomething)
優(yōu)于component.beforeComponentUpdate(doSomething)
函數(shù)混合是指將方法作為屬性添加到一個對象上面,它們就像裝配流水線給傳進(jìn)來的對象加上某些方法或者屬性。
我喜歡用形容詞來命名函數(shù)混合。你也可以經(jīng)常使用"ing"或者"able"后綴來找到有意義的形容詞。例如:
const duck = composeMixins(flying, quacking);
const box = composeMixins(iterable, mappable);
4.避免一連串結(jié)構(gòu)松散的,不知所云的代碼
開發(fā)人員經(jīng)常將一系列事件串聯(lián)在一個進(jìn)程中:一組松散的、相關(guān)度不高的代碼被設(shè)計依次運(yùn)行。從而很容易形成“意大利面條”代碼。
這種寫法經(jīng)常被重復(fù)調(diào)用,即使不是嚴(yán)格意義上的重復(fù),也只有細(xì)微的差別。例如,界面不同組件之間幾乎共享相同的核心需求。 其關(guān)注點(diǎn)可以分解成不同生命周期階段,并由單獨(dú)的函數(shù)方法進(jìn)行管理。
考慮以下的代碼:
const drawUserProfile = ({ userId }) => {
const userData = loadUserData(userId);
const dataToDisplay = calculateDisplayData(userData);
renderProfileData(dataToDisplay);
};
這個方法做了三件事:獲取數(shù)據(jù),根據(jù)獲取的數(shù)據(jù)計算view的狀態(tài),以及渲染。
在大部分現(xiàn)代前端應(yīng)用中,這些關(guān)注點(diǎn)中的每一個都應(yīng)該考慮分拆開。通過分拆這些關(guān)注點(diǎn),我們可以輕松地為每個問題提供不同的函數(shù)。
比如,我們可以完全替換渲染器,它不會影響程序的其他部分。例如,React的豐富的自定義渲染器:適用于原生iOS和Android應(yīng)用程序的ReactNative,WebVR的AFrame,用于服務(wù)器端渲染的ReactDOM/Server 等等...
drawUserProfile
的另一個問題就是你不能在沒有數(shù)據(jù)的情況下,簡單地計算要展示的數(shù)據(jù)并生成標(biāo)簽。如果數(shù)據(jù)已經(jīng)在其他地方加載過了會怎么樣,就會做很多重復(fù)和浪費(fèi)的事情。
分拆關(guān)注點(diǎn)也使得它們更容易進(jìn)行測試。我喜歡對我的應(yīng)用程序進(jìn)行單元測試,并在每次修改代碼時查看測試結(jié)果。但是,如果我們將渲染代碼和數(shù)據(jù)加載代碼寫在一起,我不能簡單地將一些假數(shù)據(jù)傳遞給渲染代碼進(jìn)行測試。我必須從端到端測試整個組件。而這個過程中,由于瀏覽器加載,異步I/O請求等等會耗費(fèi)時間。
上面的drawUserProfile
代碼不能從單元測試測試中得到即時反饋。而分拆功能點(diǎn)允許你進(jìn)行單獨(dú)的單元測試,得到測試結(jié)果。
上文已經(jīng)已經(jīng)分析出單獨(dú)的功能點(diǎn),我們可以在應(yīng)用程序中提供不同的生命周期鉤子給其調(diào)用。 當(dāng)應(yīng)用程序開始裝載組件時,可以觸發(fā)數(shù)據(jù)加載。可以根據(jù)響應(yīng)視圖狀態(tài)更新來觸發(fā)計算和渲染。
這么做的結(jié)果是軟件的職責(zé)進(jìn)一步明確:每個組件可以復(fù)用相同的結(jié)構(gòu)和生命周期鉤子,并且軟件性能更好。在后續(xù)開發(fā)中,我們不需要重復(fù)相同的事。
5.功能相連的代碼寫在一起
許多框架以及boilerplates規(guī)定了程序文件組織的方法,其中文件按照代碼類別分組。如果你正在構(gòu)建一個小的計算器,獲取一個待辦事宜的app,這樣做是很好的。但是對于較大的項目,通過業(yè)務(wù)功能特性將文件分組在一起是更好的方法。
按代碼類別分組:
.
├── components
│ ├── todos
│ └── user
├── reducers
│ ├── todos
│ └── user
└── tests
├── todos
└── user
按業(yè)務(wù)功能特性分組:
.
├── todos
│ ├── component
│ ├── reducer
│ └── test
└── user
├── component
├── reducer
└── test
當(dāng)你通過功能特性來將文件分組,你可以避免在文件列表上下滾動,查找編輯所需要的文件這種情況。
6.利用判斷true值的方式來編寫代碼
要做出確定的斷言,避免使用溫順、無色、猶豫的語句,必要時使用 not 來否定、拒絕。典型的
-
isFlying
優(yōu)于isNotFlying
-
late
優(yōu)于notOneTime
if語句
if (err) return reject(err);
// do something
優(yōu)于
if (!err) {
// ... do something
} else {
return reject(err);
}
三元判斷語句
{
[Symbol.iterator]: iterator ? iterator : defaultIterator
}
優(yōu)于
{
[Symbol.iterator]: (!iterator) ? defaultIterator : iterator
}
恰當(dāng)?shù)氖褂梅穸?/h4>
有時候我們只關(guān)心一個變量是否缺失,如果通過判斷true值的方式來命名,我們得用!
操作符來否定它。這種情況下使用 "not" 前綴和取反操作符不如使用否定語句直接。
-
if (missingValue)
優(yōu)于if (!hasValue)
-
if (anonymous)
優(yōu)于if (!user)
-
if (!isEmpty(thing))
優(yōu)于if (notDefined(thing))
函數(shù)調(diào)用時,避免用null以及undefined代替某一個參數(shù)
不要在函數(shù)調(diào)用時,傳入undefined
或者null
作為某個參數(shù)的值。如果某些參數(shù)可以缺失,更推薦傳入一個對象:
const createEvent = ({
title = 'Untitled',
timeStamp = Date.now(),
description = ''
}) => ({ title, description, timeStamp });
const birthdayParty = createEvent({
title: 'Birthday Party',
description: 'Best party ever!'
});
優(yōu)于
const createEvent = (
title = 'Untitled',
timeStamp = Date.now(),
description = ''
) => ({ title, description, timeStamp });
const birthdayParty = createEvent(
'Birthday Party',
undefined, // This was avoidable
'Best party ever!'
);
不同的技術(shù)方案利用不同的代碼組織結(jié)構(gòu)來實現(xiàn)
迄今為止,應(yīng)用程序中未解決的問題很少。最終,我們都會一次又一次地做著同樣的事情。當(dāng)這樣的場景發(fā)生時,意味著代碼重構(gòu)的機(jī)會來啦。分辨出類似的部分,然后抽取出能夠支持每個不同部分的公共方法。這正是類庫以及框架為我們做的事情。
UI組件就是一個很好的例子。10 年前,使用 jQuery 寫出把界面更新、應(yīng)用邏輯和數(shù)據(jù)加載混在一起的代碼是再常見不過的。漸漸地,人們開始意識到我們可以將MVC應(yīng)用到客戶端的網(wǎng)頁上面,隨后,人們開始將model與UI更新邏輯分拆。
最終,web應(yīng)用廣泛采用組件化這一方案,這使得我們可以使用JSX或HTML模板來聲明式的對組件進(jìn)行建模。
最終,我們就能用完全相同的方式去表達(dá)所有組件的更新邏輯、生命周期,而不用再寫一堆命令式的代碼
對于熟悉組件的人,很容易看懂每個組件的原理:利用標(biāo)簽來表示UI元素,事件處理器用來觸發(fā)行為,以及用于添加回調(diào)的生命周期鉤子函數(shù),這些鉤子函數(shù)將在必要時運(yùn)行。
當(dāng)我們對于類似的問題采用類似的模式解決時,熟悉這個解決模式的人很快就能理解代碼是用來做什么的。
結(jié)論:代碼應(yīng)該簡單而不是過于簡單化
盡管在2015,ES6已經(jīng)標(biāo)準(zhǔn)化,但在2017,很多開發(fā)者仍然拒絕使用ES6特性,例如箭頭函數(shù),隱式return,rest以及spread操作符等等。利用自己熟悉的方式編寫代碼其實是一個幌子,這個說法是錯誤的。只有不斷嘗試,才能夠漸漸熟悉,熟悉之后,你會發(fā)現(xiàn)簡潔的ES6特性明顯優(yōu)于ES5:與語法結(jié)構(gòu)偏重的ES5相比,簡潔的es6的代碼很簡單。
代碼應(yīng)該簡單,而不是過于簡單化。
簡潔的代碼有以下優(yōu)勢:
- 更少的bug可能性
- 更容易去debug
但也有如下弊端:
- 修復(fù)bug的成本更高
- 有可能引用更多的bug
- 打斷了正常開發(fā)的流程
簡潔的代碼同樣:
- 更易寫
- 更易讀
- 更好去維護(hù)
清楚自己的目標(biāo),不要毫無頭緒。毫無頭緒只會浪費(fèi)時間以及精力。投入精力去訓(xùn)練,讓自己熟悉,去學(xué)習(xí)更好的編程方式,以及更有更有活力的代碼風(fēng)格。
代碼應(yīng)該簡單,而不是簡單化。