當前位置:
首頁 > 科技 > JavaScript代碼風格要素

JavaScript代碼風格要素

前言

類似規範,有一定的個人風格。今日早讀文章由前端早讀課專欄作者@墨白翻譯分享。

本文由前端早讀課專欄作者@ 野草校對

正文從這開始~

1920年,由威廉·斯特倫克(William Strunk jr .)撰寫的《英語寫作手冊:風格的要素(The Elements of Style)》出版了,這本書列舉了7條英文寫作的準則,過了一個世紀,這些準則並沒有過時。對於工程師來說,你可以在自己的編碼風格中應用類似的建議來指導日常的編碼,提高自己的編碼水平。

需要注意的是,這些準則不是一成不變的法則。如果違背它們,能夠讓代碼可讀性更高,那麼便沒有問題,但請特別小心並時刻反思。這些準繩是經受住了時間考驗的,有充分的理由說明:它們通常是正確的。如果要違背這些規則,一定要有充足的理由,而不要單憑一時的興趣或者個人的風格偏好。

書中的寫作準則如下:

以段落為基本單位:一段文字,一個主題。

刪減無用的語句。

使用主動語態。

避免一連串鬆散的句子。

相關的內容寫在一起。

從正面利用肯定語句去發表陳述。

不同的概念採用不同的結構去闡述。

我們可以應用相似的理念到代碼編寫上面:

一個function只做一件事,讓function成為代碼組合的最小單元。

刪除不必要的代碼。

使用主動語態。

避免一連串結構鬆散的,不知所云的代碼。

將相關的代碼寫在一起。

利用判斷true值的方式來編寫代碼。

不同的技術方案利用不同的代碼組織結構來實現。

1.一個function只做一件事,讓function成為代碼組合的最小單元

軟體開發的本質是「組合」。 我們通過組合模塊,函數和數據結構來構建軟體。

理解如果編寫以及組合方法是軟體開發人員的基本技能。

模塊是一個或多個function和數據結構的簡單集合,我們用數據結構來表示程序狀態,只有在函數執行之後,程序狀態才會發生一些有趣的變化。

JavaScript中,可以將函數分為3種:

I/O 型函數 (Communicating Functions):函數用來執行I/O。

過程型函數 (Procedural Functions):對一系列的指令序列進行分組。

映射型函數 (Mapping Functions):給定一些輸入,返回對應的輸出。

有效的應用程序都需要I/O,並且很多程序都遵循一定的程序執行順序,這種情況下,程序中的大部分函數都會是映射型函數:給定一些輸入,返回相應的輸出。

每個函數只做一件事情:如果你的函數主要用於I/O,就不要在其中混入映射型代碼,反之亦然。嚴格根據定義來說,過程型函數違反了這一指導準則,同時也違反了另一個指導準則:避免一連串結構鬆散,不知所云的代碼。

理想中的函數是一個簡單的、明確的純函數:

同樣的輸入,總是返回同樣的輸出。

也可以查看,「什麼是純函數?」

2. 刪除不必要的代碼

簡潔的代碼對於軟體而言至關重要。更多的代碼意味更多的bug隱藏空間。更少的代碼 = 更少的bug隱藏空間 = 更少的bug

簡潔的代碼讀起來更清晰,因為它擁有更高的「信噪比」:閱讀代碼時更容易從較少的語法噪音中篩選出真正有意義的部分。可以說,更少的代碼 = 更少的語法雜訊 = 更強的代碼含義信息傳達

借用《風格的元素》這本書裡面的一句話就是:簡潔的代碼更健壯。

functionsecret(message){

returnfunction(){

returnmessage;

}

};

可以簡化成:

constsecret=msg=>()=>msg;

對於那些熟悉簡潔箭頭函數寫法的開發來說,可讀性更好。它省略了不必要的語法:大括弧,function關鍵字以及return語句。

而簡化前的代碼包含的語法要素對於傳達代碼意義本身作用並不大。它存在的唯一意義只是讓那些不熟悉ES6語法的開發者更好的理解代碼。

ES6自2015年已經成為語言標準,是時候去學習它了。

刪除不必要的代碼

有時候,我們試圖為不必要的事物命名。問題是人類的大腦在工作中可用的記憶資源有限,每個名稱都必須作為一個單獨的變數存儲,佔據工作記憶的存儲空間。

由於這個原因,有經驗的開發者會儘可能地刪除不必要的變數。

例如,大多數情況下,你應該省略僅僅用來當做返回值的變數。你的函數名應該已經說明了關於函數返回值的信息。看看下面的:

constgetFullName=({firstName,lastName})=>{

constfullName=firstName+ +lastName;

returnfullName;

};

對比

constgetFullName=({firstName,lastName})=>(

firstName+ +lastName

);

另一個開發者通常用來減少變數名的做法是,利用函數組合以及point-free-style。

Point-free-style是一種定義函數方式,定義成一種與參數無關的合成運算。實現point-free風格常用的方式包括函數科里化以及函數組合。

讓我們來看一個函數科里化的例子:

constadd2=a=>b=>a+b;

// Now we can define a point-free inc()

// that adds 1 to any number.

constinc=add2(1);

inc(3);// 4

看一下inc()函數的定義方式。注意,它並未使用function關鍵字,或者=>語句。add2也沒有列出一系列的參數,因為該函數不在其內部處理一系列的參數,相反,它返回了一個知道如何處理參數的新函數。

函數組合是將一個函數的輸出作為另一函數的輸入的過程。 也許你沒有意識到,你一直在使用函數組合。鏈式調用的代碼基本都是這個模式,比如數組操作時使用的.map(),Promise 操作時的promise.then()。函數組合在函數式語言中也被稱之為高階函數,其基本形式為:f(g(x))。

當兩個函數組合時,無須創建一個變數來保存兩個函數運行時的中間值。我們來看看函數組合是怎麼減少代碼的:

constg=n=>n+1;

constf=n=>n*2;

// 需要操作參數、並且存儲中間結果

constincThenDoublePoints=n=>{

constincremented=g(n);

returnf(incremented);

};

incThenDoublePoints(20);// 42

// compose2 - 接受兩個函數作為參數,直接返回組合

constcompose2=(f,g)=>x=>f(g(x));

constincThenDoublePointFree=compose2(f,g);

incThenDoublePointFree(20);// 42

你可以利用函子(functor)來做同樣的事情。在函子中把參數封裝成可遍歷的數組。讓我們利用函子來寫另一個版本的compose2:

constcompose2=(f,g)=>x=>[x].map(g).map(f).pop();

constincThenDoublePointFree=compose2(f,g);

incThenDoublePointFree(20);// 42

當每次使用promise鏈時,你就是在做這樣的事情。

幾乎每一個函數式編程類庫都提供至少兩種函數組合方法:從右到左依次運行的compose();從左到右依次運行的pipe()。

Lodash中的compose()以及flow()分別對應這兩個方法。下面是使用pipe 的例子:

importpipefrom lodash/fp/flow ;

pipe(g,f)(20);// 42

下面的代碼也做著同樣的事情,但代碼量並未增加太多:

constpipe=(...fns)=>x=>fns.reduce((acc,fn)=>fn(acc),x);

pipe(g,f)(20);// 42

如果函數組合這個名詞聽起來很陌生,你不知道如何使用它,請仔細想一想:

軟體開發的本質是組合,我們通過組合較小的模塊,方法以及數據結構來構建應用程序。

不難推論,工程師理解函數和對象組合這一編程技巧就如同搞裝修需要理解鑽孔機以及氣槍一樣重要。

當你利用「命令式」代碼將功能以及中間變數拼湊在一起時,就像瘋狂使用膠帶和膠水將這些部分胡亂粘貼起來一樣,而函數組合看上去更流暢。

記住:

用更少的代碼。

用更少的變數。

3. 使用主動語態

主動語態比被動語態更直接,跟有力量,盡量多直接命名事物:

myFunction.wasCalled()優於myFunction.hasBeenCalled()

createUser優於User.create()

notify()優於Notifier.doNotification()

命名布爾返回值時最好直接反應其輸出的類型:

isActive(user)優於getActiveStatus(user)

isFirstRun = false;優於firstRun = false;

函數名採用動詞形式:

increment()優於plusOne()

unzip()優於filesFromZip()

filter(fn, array)優於matchingItemsFromArray(fn, array)

事件處理

事件處理以及生命周期函數由於是限定符,比較特殊,就不適用動詞形式這一規則;相比於「做什麼」,它們主要用來表達「什麼時候做」。對於它們,可以「,」這樣命名,朗朗上口。

element.onClick(handleClick)優於element.click(handleClick)

element.onDragStart(handleDragStart)優於component.startDrag(handleDragStart)

上面兩例的後半部分,它們讀起來更像是正在嘗試去觸發一個事件,而不是對其作出回應。

生命周期函數

對於組件生命周期函數(組件更新之前調用的方法),考慮一下以下的命名:

componentWillBeUpdated(doSomething)

componentWillUpdate(doSomething)

beforeUpdate(doSomething)

第一個種我們使用了被動語態(將要被更新而不是將要更新)。這種方式很口語化,但含義表達並沒有比其它兩種方式更清晰。

第二種就好多了,但生命周期函數的重點在於觸發處理事件。componentWillUpdate(handler)讀起來就好像它將立即觸發一個處理事件,但這不是我們想要表達的。我們想說,「在組件更新之前,觸發事件」。beforeComponentUpdate()能更清楚的表達這一想法。

進一步簡化,因為這些方法都是組件內置的。在方法名中加入component是多餘的。想一想如果你直接調用這些方法時:component.componentWillUpdate()。這就好像在說,「吉米吉米在晚餐吃牛排。」你沒有必要聽到同一個對象的名字兩次。顯然,

component.beforeUpdate(doSomething)優於component.beforeComponentUpdate(doSomething)

函數混合是指將方法作為屬性添加到一個對象上面,它們就像裝配流水線給傳進來的對象加上某些方法或者屬性。

我喜歡用形容詞來命名函數混合。你也可以經常使用"ing"或者"able"後綴來找到有意義的形容詞。例如:

const duck = composeMixins(flying, quacking);

const box = composeMixins(iterable, mappable);

4.避免一連串結構鬆散的,不知所云的代碼

開發人員經常將一系列事件串聯在一個進程中:一組鬆散的、相關度不高的代碼被設計依次運行。從而很容易形成「義大利麵條」代碼。

這種寫法經常被重複調用,即使不是嚴格意義上的重複,也只有細微的差別。例如,界面不同組件之間幾乎共享相同的核心需求。 其關注點可以分解成不同生命周期階段,並由單獨的函數方法進行管理。

考慮以下的代碼:

constdrawUserProfile=({userId})=>{

constuserData=loadUserData(userId);

constdataToDisplay=calculateDisplayData(userData);

renderProfileData(dataToDisplay);

};

這個方法做了三件事:獲取數據,根據獲取的數據計算view的狀態,以及渲染。

在大部分現代前端應用中,這些關注點中的每一個都應該考慮分拆開。通過分拆這些關注點,我們可以輕鬆地為每個問題提供不同的函數。

比如,我們可以完全替換渲染器,它不會影響程序的其他部分。例如,React的豐富的自定義渲染器:適用於原生iOS和Android應用程序的ReactNative,WebVR的AFrame,用於伺服器端渲染的ReactDOM/Server 等等...

drawUserProfile的另一個問題就是你不能在沒有數據的情況下,簡單地計算要展示的數據並生成標籤。如果數據已經在其他地方載入過了會怎麼樣,就會做很多重複和浪費的事情。

分拆關注點也使得它們更容易進行測試。我喜歡對我的應用程序進行單元測試,並在每次修改代碼時查看測試結果。但是,如果我們將渲染代碼和數據載入代碼寫在一起,我不能簡單地將一些假數據傳遞給渲染代碼進行測試。我必須從端到端測試整個組件。而這個過程中,由於瀏覽器載入,非同步I/O請求等等會耗費時間。

上面的drawUserProfile代碼不能從單元測試測試中得到即時反饋。而分拆功能點允許你進行單獨的單元測試,得到測試結果。

上文已經已經分析出單獨的功能點,我們可以在應用程序中提供不同的生命周期鉤子給其調用。 當應用程序開始裝載組件時,可以觸發數據載入。可以根據響應視圖狀態更新來觸發計算和渲染。

這麼做的結果是軟體的職責進一步明確:每個組件可以復用相同的結構和生命周期鉤子,並且軟體性能更好。在後續開發中,我們不需要重複相同的事。

5.功能相連的代碼寫在一起

許多框架以及boilerplates規定了程序文件組織的方法,其中文件按照代碼類別分組。如果你正在構建一個小的計算器,獲取一個待辦事宜的app,這樣做是很好的。但是對於較大的項目,通過業務功能特性將文件分組在一起是更好的方法。

按代碼類別分組:

.

├── components

│ ├── todos

│ └── user

├── reducers

│ ├── todos

│ └── user

└── tests

├── todos

└── user

按業務功能特性分組:

.

├── todos

│ ├── component

│ ├── reducer

│ └── test

└── user

├── component

├── reducer

└── test

當你通過功能特性來將文件分組,你可以避免在文件列表上下滾動,查找編輯所需要的文件這種情況。

6.利用判斷true值的方式來編寫代碼

要做出確定的斷言,避免使用溫順、無色、猶豫的語句,必要時使用 not 來否定、拒絕。典型的

isFlying優於isNotFlying

late優於notOneTime

if 語句

if(err)returnreject(err);

// do something

優於

if(!err){

// ... do something

}else{

returnreject(err);

}

三元判斷語句

{

[Symbol.iterator]:iterator?iterator:defaultIterator

}

優於

{

[Symbol.iterator]:(!iterator)?defaultIterator:iterator

}

恰當的使用否定

有時候我們只關心一個變數是否缺失,如果通過判斷true值的方式來命名,我們得用!操作符來否定它。這種情況下使用 "not" 前綴和取反操作符不如使用否定語句直接。

if (missingValue)優於if (!hasValue)

if (anonymous)優於if (!user)

if (!isEmpty(thing))優於if (notDefined(thing))

函數調用時,避免用null以及undefined代替某一個參數

不要在函數調用時,傳入undefined或者null作為某個參數的值。如果某些參數可以缺失,更推薦傳入一個對象:

constcreateEvent=({

title= Untitled ,

timeStamp=Date.now(),

description=

})=>({title,description,timeStamp});

constbirthdayParty=createEvent({

title: Birthday Party ,

description: Best party ever!

});

優於

constcreateEvent=(

title= Untitled ,

timeStamp=Date.now(),

description=

)=>({title,description,timeStamp});

constbirthdayParty=createEvent(

Birthday Party ,

undefined,// This was avoidable

Best party ever!

);

不同的技術方案利用不同的代碼組織結構來實現

迄今為止,應用程序中未解決的問題很少。最終,我們都會一次又一次地做著同樣的事情。當這樣的場景發生時,意味著代碼重構的機會來啦。分辨出類似的部分,然後抽取出能夠支持每個不同部分的公共方法。這正是類庫以及框架為我們做的事情。

UI組件就是一個很好的例子。10 年前,使用 jQuery 寫出把界面更新、應用邏輯和數據載入混在一起的代碼是再常見不過的。漸漸地,人們開始意識到我們可以將MVC應用到客戶端的網頁上面,隨後,人們開始將model與UI更新邏輯分拆。

最終,web應用廣泛採用組件化這一方案,這使得我們可以使用JSX或HTML模板來聲明式的對組件進行建模。

最終,我們就能用完全相同的方式去表達所有組件的更新邏輯、生命周期,而不用再寫一堆命令式的代碼

對於熟悉組件的人,很容易看懂每個組件的原理:利用標籤來表示UI元素,事件處理器用來觸發行為,以及用於添加回調的生命周期鉤子函數,這些鉤子函數將在必要時運行。

當我們對於類似的問題採用類似的模式解決時,熟悉這個解決模式的人很快就能理解代碼是用來做什麼的。

結論:代碼應該簡單而不是過於簡單化

儘管在2015,ES6已經標準化,但在2017,很多開發者仍然拒絕使用ES6特性,例如箭頭函數,隱式return,rest以及spread操作符等等。利用自己熟悉的方式編寫代碼其實是一個幌子,這個說法是錯誤的。只有不斷嘗試,才能夠漸漸熟悉,熟悉之後,你會發現簡潔的ES6特性明顯優於ES5:與語法結構偏重的ES5相比,簡潔的es6的代碼很簡單。

代碼應該簡單,而不是過於簡單化。

簡潔的代碼有以下優勢:

更少的bug可能性

更容易去debug

但也有如下弊端:

修復bug的成本更高

有可能引用更多的bug

打斷了正常開發的流程

簡潔的代碼同樣:

更易寫

更易讀

更好去維護

清楚自己的目標,不要毫無頭緒。毫無頭緒只會浪費時間以及精力。投入精力去訓練,讓自己熟悉,去學習更好的編程方式,以及更有更有活力的代碼風格。

代碼應該簡單,而不是簡單化。

關於本文

譯者:@墨白

校對:@野草

作者:@Eric Elliott

原文:https://medium.com/javascript-scene/elements-of-javascript-style-caa8821cb99f

點擊展開全文

喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 前端早讀課 的精彩文章:

二零一七之端午節
Angular組件間通信
JavaScript函數的多種寫法
讀前端架構設計——我眼中的前端架構
你不懂JS:ES6與未來 新增API

TAG:前端早讀課 |

您可能感興趣

新風格 Adidas Falcon W "Light Granite"復古拼色老爹鞋
Nvidia 圖片風格轉換工具——FastPhotoStyle
硬派科幻風格!adidas Futurepacer Boost 上腳效果如何?
nodejs+mongodb 編寫 restful 風格博客 api
黑腳趾風格!Air Jordan 14 GS 「Fuchsia Blast」 即將發售!
東瀛風格加持!Air Jordan 12 「International Flight」 正式登場!
Off-White風格的adidas Originals NMD CS1是你的菜么?
Apple Watch 也會有 AirPods 風格的充電盒?
疑似官方 Supreme + OW 定製風格 Air Jordan 1 釋出!
獨特風格,菲董 x adidas Originals Hu NMD 全新「Afro」系列
有種穿衣風格, 叫Victoria Beckham!
風格清新!Reebok Question 「Mint Glow」 下周發售!
除了現在大火的Balenciaga姥爺鞋,Gucci小白鞋,yeezy。最能hold住各種風格的還是converse
Illustrator繪製MEB風格的人像插圖
Google可能會將iPhone X風格的導航添加到Android P
機能風格再迎新品!這雙 Nike Air VaporMax Utility 下周發售
極簡解構風格!adidas Y-3 Ayero White 現已發售
Deep Learning-風格遷移
Nike Air VaporMax 氣墊跑鞋持續變裝「FK Utility」開啟機能風格
Bottega Veneta:當義大利風格在紐約