此篇我將會從函數的命名、註釋和魯棒性方面,闡述如何編寫高質量的函數。前端
PS:這一篇相對上一篇文章要簡單好懂多了。node
寫第二篇以前,先說個事情。針對前面我寫的 如何編寫高質量的函數 -- 敲山震虎篇 文章的評論區,小夥伴提出的一些問題,我也所有都看了,特此寫了答疑篇。react
答疑篇放在了 issues
上,點擊下面連接便可 TP
:git
如何編寫高質量的函數 -- 敲山震虎的答疑篇github
對個人回答有什麼疑問的話,能夠 issues
討論,這篇文章就不放在掘金了。web
PS: 提一下一些公衆號(掘金,奇舞週刊等)轉載的狀況,因爲文章一開始有錯誤,而後公衆號又沒有同步,而後我又不能幫大家改,因而點開公衆號文章,看着那些還在的錯誤,卻無能爲力的心情,內心默默
ob
: 感謝地主們的轉載,那就這樣吧😂。面試
好,胡謅很少說,直接開始吧。express
從文章開頭的圖片能夠知道,命名和緩存是計算機科學中的兩大難題。npm
而今天要說的是函數命名,雖然函數命名涉及到的範圍較窄,但思想都是同樣的,徹底 能夠借鑑到其餘的形式中。編程
我閱讀過代碼大全的變量一章,也針對性的閱讀過一些源碼,好比 lodash
, ramda
這些函數工具庫。如今根據我我的的一些感悟,總結了一些我我的認爲能幫助你完全解決命名這個事情的 best practice
。
PS: 雖然變量命名這個沒有所謂
best practice
,可是對於前端的函數命名來講,我我的認爲是能夠有一套完善的best pratice
的, 聽我娓娓道來。
存在什麼問題呢?要我說啊,那些業界標準,好比駝峯,首字母大寫的類和構造函數,下劃線,$
等都不是瓶頸。真正的瓶頸是一些你察覺不到的,或者察覺到可是無能無力的細節。好比:
下面進行簡明扼要的分析。
PS: 關於駝峯等耳熟能詳的業界標準我就再也不提了。
爲何一開始要說這個呢,由於我認爲這是目前命名中,存在的最大的問題。英語水平好的哥們沒多少,不少人不能掌握英語命名的那一套語法規則,說白了就是你英語水平達不到能像外國人那樣能寫出符合英語 style
的名字。
理由就三個:
最大的困難就是 不會 。
我舉個例子,都知道 react
的生命週期吧,以下:
componentDidMount
componentWillReceiveProps
shouldComponentUpdate
不少人都會有疑問,爲何用 did
,爲何用 will
。行吧,記住就完事了,而後過了一段時間,面試的被問到了,而後內心 ob:
是 componentMounted
仍是啥來...
多麼鮮活的例子,嗯,繼續往下讀吧,後面有驚(極)爲(爲)天(驚)人(悚)的答案。
黑人臉,怎麼讓啊?
老哥,多翻翻高中或者初中的英語語法知識吧。好比我舉個最簡單的例子,你就知道了。
componentDidMount
是react
等生命週期的鉤子,可是爲何要這樣命名?
componentWillReceiveProps
爲何要這樣命名?
答案就在下圖:
注意上圖中的 did
表明通常過去時,will
表明通常未來時。
而後咱們百科通常過去式和通常未來時,而後如圖所示:
通常過去時:
通常未來時
看上圖的紅箭頭,did
表示通常過去時,時是指動做發生的時間,用在這裏,突出了鉤子的含義,一旦 mount
成功就執行此函數。是否是瞬間明白了,好了,will
同理。
啥也別說了,趕忙去好好看看初高中英語語法吧。
這是個小特性,好比 shouldComponentUpdate
, 爲何 should
放在最前面。
由於這個函數返回的值是布爾值。那麼咱們能夠理解爲這是個問句,經過問句的形式來告訴咱們,這裏具備不肯定性,須要根據返回值來判斷是否更新。
關於問句的英語語法,老鐵們還要多翻翻語法書啊(淚)。
谷歌翻譯這個就不說了,你們都會
這是一個神器,用來搜索各類開源項目中的變量命名,來給你提供參考。
對應名字的 VSCODE
插件也有,具體怎麼用,小夥伴自行去了解吧。
可能你給一個函數命名了,其餘人看到這個函數時,一臉懵逼,徹底不知道這命名的啥子東西喲,只能靠猜。
好比我從 ramda
源碼中找了一個函數,代碼以下:
var forEachObjIndexed = _curry2(function forEachObjIndexed(fn, obj) {
var keyList = keys(obj);
var idx = 0;
while (idx < keyList.length) {
var key = keyList[idx];
fn(obj[key], key, obj);
idx += 1;
}
return obj;
});
export default forEachObjIndexed;
複製代碼
這個函數叫 forEachObjIndexed
,看到這個命名,是否是一臉懵逼,反正我第一次看到是懵逼了,這啥子嘛,什麼鬼東西,而後我就去趴了下源碼,從源碼裏面的函數註釋中才知道是幹啥的,函數註釋以下圖:
看到沒,多詳細,固然,這是爲了輸出文檔用的,可是給了咱們一塊兒很是好的解決方法。那就是:
若是你實在想不到如何去命名,或者你本身已經知道這個命名很爛了,好比太長,好比很難理解,那這個時候你就別掙扎了。寫一個你以爲還 ok
的命名,而後把剩下的時間留給你寫註釋吧。好比 forEachObjIndexed
的第一部分的註釋就是對整個函數的總體介紹和說明。
若是你的函數命名很爛,那這個時候,函數的總體介紹和說明就顯得很是重要了。這部分你必定要作好,英語水平很差的話,那就老老實實寫中文。這部分作好了,這個函數你哪怕用兩行文字命名的,或者用了火星文命名的,也不要緊,問題不大。
最後說一說命名的分類,這是我我的的一些見解。
爲何我會說函數命名的分類呢,是由於咱們常常會看到函數會這樣命名(源碼中很廣泛)。好比:
- $xxx()
- _xxx()
複製代碼
這種帶各類前綴的函數名,看起來並很差看。這樣命名,在我我的看起來是很是彆扭的,可是爲何要有這種命名呢,其實這是前端的無奈之舉。
核心緣由就是
JS
語言不支持私有變量,致使只能使用_
或者$
來保證相應的對外不可見,經過治標不治本的方法來解決這個問題。
因此我把前端的函數命名分爲兩大類,以下:
第一類:不想暴露給外部訪問的函數(好比只給內部使用)
第二類:暴露給外部訪問的函數(各類功能方法)
我我的目前的觀點,大體也就這兩大類了。
PS:這裏我沒把 Symbol 初始化的函數命名考慮在內,好比以下代碼:
const ADD = Symbol('add')
[ADD](a, b) {
console.log('a + b')
}
複製代碼
關於 Symbol
的用法,你們能夠自行了解,這種特例我就不考慮在內了。
PS:關於這個無奈之舉,在瞭解的更多的時候,會發如今前端,並無什麼方法(設計模式也好,
hack
方法也好)能絕對的解決上面的問題,因此有時候你不得不使用_
等,由於當都不能解決這個問題的時候,那越簡單的方式越受歡迎,這就是現實。
總結一下最佳實踐:
多學習初中高中英語語法,開源項目中的函數命名沒有那麼難理解,經過語法的學習和藉助工具,函數命名基本能夠解決,若是遇到沒法清晰描述所寫函數的目的的命名時,請務必給函數寫上良好註釋,無論函數名字有多長多難懂,只要有良好的註釋,那就是能夠接受的一件事情。畢竟你也不想命名的這麼難懂啊,可是能力有限,那就用漢語作好註釋吧,這樣的效果也是槓槓的。
如何經過良好的函數命名來提供函數的質量,我也說的差很少了,答案都在文字中,如何去借助工具,如何去理解英語中的命名語法,如何去經過多維度來增長命名含義的準確性和可讀性。簡單聊了下目前前端界函數命名的分類,你們自行體會和運用吧。
PS:一些都知道的點我就不說了,好比動詞+名詞,名詞+動詞,駝峯等,清晰描述函數的目的,這都不是痛點,痛點我都說了,最佳實踐也說了。
咱們來談函數的註釋,註釋一方面提升了可讀性,另外一方面也能夠經過註釋去作一些其它的事情,好比生成在線文檔。一個高質量的函數,註釋少不了的,可是這並不表明全部的函數都須要註釋。富有富的活法,窮有窮的瀟灑,重要或者說複雜的函數,那就給個好註釋,簡單或者不重要的函數,能夠不給註釋或者給一個簡單的註釋。說空話沒有意義,咱們來看看目前函數的註釋都有哪幾種方式。
PS:這裏要注意我上面的用詞,若是你以爲這個函數命名很爛,那你就應該給一個好的註釋。
就像大學裏面寫論文以前,都要閱讀不少文獻資料,咱們也同樣,咱們來看看幾個有名的 npm
包是怎麼玩註釋的。
從圖中,咱們看到 egg.js
的入口文件的註釋狀況,暫且不去判斷這是否是一種 doc
工具的註釋規則(不要在乎細節)。咱們就看一下其註釋特色,是否是發現和你腦海中的註釋風格又有區別了呢。這種入口文件的註釋特色,簡單整潔,這種思想是否是要吸取一波,之後你作開源項目的時候,這些思想均可以帶給你靈感。
繼續看下圖:
這是一個被抽象出來的基類,展現了做者 [Yiyu He
] 當時寫這個類的時候,其註釋的風格。從這張圖中,咱們能學到什麼呢?有如下幾點:
第一點:構造函數的註釋規則,表達式語句的註釋規則。
第二點:註釋的取捨,有一些變量能夠不用註釋,有些要註釋,不要有那種要註釋就要所有註釋的思想。
再看兩張有趣的圖片:
看上面兩張圖的箭頭,指向的都是同一個做者 [fengmk2
] , 咱們看他的函數註釋規則。體會一下不一樣,想一想爲何第一張圖沒有空格,第二種有空格,還有對返回的 this
的註釋,好比不少人習慣將 this
直接註釋成 Object
類型。
說到函數註釋,就不能不說到 lodash.js
。可是寫到這,我發現這塊要是加上去的話,第二篇的文字就又超了,那這裏就再也不說了,你們本身看看源碼分析一下吧(這操做真香)。
有人說註釋要很規範,方便給別人,好比用 jsdoc
等 。這裏我我的的見解是這樣的,對一些不須要開源的 web
項目,沒有必要用 jsdoc
, 理由以下:
jsdoc
規則來jsdoc
有入侵性,文檔規則須要寫在代碼中。這裏我認爲若是要寫註釋說明手冊,對於大型項目,我推薦使用 apidoc
, 由於 apidoc
入侵性不強,不要求把規則寫在代碼中,你能夠把全部規則寫到一個文件中。具體使用方法,我就不說了,自行搜索相關資料。
可是通常小項目,沒有必要單獨寫一份 api
文檔。若是是開源的大型項目,那你要考慮的事情就更多了,首先須要有開源的官方網站,你會看到網上的一些開源項目官網好像很酷,其實這個世界上不缺的就是輪子,你也能夠很快的作出這樣的網站,下面咱們來看看是如何作到的。
首先咱們看一下 taro
源碼,會發現以下圖:
這裏就是生成一個靜態網站的祕密,執行這個 npm run docs
就能夠了。用到的是 docusaurus
包,不知道的能夠自行搜索。
而後這裏你看下圖:
從圖中能夠知道,文檔的內容,來源於 docs
目錄,裏面都是 md
文件,開源項目的文檔說明都在這裏。
固然也有把對應的文檔直接放到對應的代碼目錄下的,好比 ant-design
以下圖:
就是直接把文檔放在組件目錄下了。
從這裏,咱們能夠知道,目前流行的開源項目的官方網站是怎麼實現的,以及文檔該怎麼寫。你能夠說這和函數註釋沒有什麼關係,可是想一想好像又有點關係,這裏就很少言了,本身體會吧。
下面說說我本人對函數註釋(只針對函數註釋)的一些我的風格或者意見。
VSCode
關於註釋的幾個工具Better Comments
給註釋上色Document This
自動生成註釋TODO Highlight
高亮 TODO
,並能夠搜尋全部 TODO
具體用法就不說了,下面是一張演示圖,自行去研究吧:
我我的的觀點是這樣的:
不影響可讀性,複雜度低,對外界沒有過分干涉的函數能夠不寫註釋。
函數內,表達式語句的註釋能夠簡單點,我通常以下圖所示,//
後面加簡要說明。
function add(a, b) {
// sum ....
let sum = a + b
}
複製代碼
function say() {
// TODO: 編寫 say 具體內容
console.log('say')
}
複製代碼
function fix() {
// FIXME: 刪除 console.log方法
console.log('fix')
}
複製代碼
通常我分爲普通函數和構造函數。
普通函數註釋:
/** * add * @param {Number} a - 數字 * @param {Number} b - 數字 * @returns {Number} result - 兩個整數之和 */
function add(a, b) {
// FIXME: 這裏要對 a, b 參數進行類型判斷
let result = a + b
return (result)
}
複製代碼
構造函數註釋:
class Kun {
/** * @constructor * @param {Object} opt - 配置對象 */
constructor(opt = {}) {
// 語句註釋
this.config = opt
}
}
複製代碼
從開源項目的代碼中能夠發現,註釋的風格多種多樣,有時候我本身不一樣項目的註釋風格也有點差異,可是我會盡量的去平衡註釋和不註釋,上面註釋的基本原則仍是要遵照的。
可是怎麼說呢,註釋這塊不存在銀彈。
你們都聽過防護性編程對吧,let it crash
。 咱們看一個段子,下圖:
看最後一句,測試測了那麼多場景,最後酒吧仍是炸了(哈哈哈哈哈哈怎麼回事?)。
因此,咱們能夠看出,防護性編程的核心就是:
把全部可能會出現的異常都考慮到,而且作相應處理。
可是我我的認爲,防護性的程度要看其重要的程度。通常來講,不可能去處理全部狀況的,可是提升代碼魯棒性的有效途徑就是進行防護性的編程。
我接手過一個需求,重寫(徹底重構)蘇寧易購微信小程序的登陸註冊綁定的功能,並將代碼同步到蘇寧其餘小程序(和其餘小程序的開發進行代碼交接並協助 coder
平穩完成版本過渡)。
這個項目重要性不言而喻,因爲用戶的基數很大,風險程度很高,須要考慮不少場景,好比:
支不支持線上版本回退,也就是須要有前端的 AB
版本方案(線上有任何問題,能夠快速切到舊登陸方案)
須要有各類驗證:圖形驗證碼、短信驗證碼、ip
、人機、設備指紋、風控、各類異常處理、異常埋點上報等。
代碼層面的考慮:經過代碼優化,縮短總的響應時間,提升用戶體驗。
如何確保單個節點出問題,不會影響整個登陸流程。
你會發現,須要考慮的點不少,如何去合理的完成這個需求仍是比較有難度的。
PS: 關於第四點的如何確保單個節點出問題,不會影響整個登陸流程,文末有答案。
下面我就關於函數魯棒性,說一說我我的的一些見解。
在 ES6
的到來後,函數的入參寫法已經獲得了質的提升和優化。看下面代碼
function print(obj = {}) {
console.log('name', obj.name)
console.log('age', obj.age)
}
複製代碼
print
函數,入參是 obj
經過 obj = {}
來給入參設置默認的參數值,從而提升入參的魯棒性。
可是你會發現,若是入參的默認值是 {}
,那函數裏面的 obj.name
就會是 undefined
,這也不夠魯棒,因此下面就要說說函數內表達式語句的魯棒性了。
繼續上個例子:
function print(obj = {}) {
console.log('name:', obj.name || '未知姓名')
console.log('age:', obj.age || '未知年齡')
}
複製代碼
若是這樣的話,那你會發現表達式語句就變得比較魯棒性了,可是還不夠好,這樣寫不夠抽象,咱們換種方式稍微把表達式語句給解耦一下,代碼以下:
function print(obj = {}) {
const { name = '未知姓名', age = '未知年齡' } = obj
console.log('name:', name)
console.log('age:', age)
}
複製代碼
這樣的話,看起來就感受好多了,其實還能夠再抽象,好比吧 console.log
封裝成 log
函數,經過調用 log(name)
,就能完成 console.log('name:', name)
的功能, 這裏就再也不說了,自行研究吧。
上面的那幾個點,我我的認爲能夠歸類到一個方案層面去,那就是:
防患於未然,從一開始就不要讓異常發生。
可是不要忘了,總會有萬一,還有一個方案層面要去考慮,那就是:
異常仍是出現了,該怎麼去處理出現的異常。
下面兩個層面已經肯定了,那如何去更好的處理各類異常,提升函數的魯棒性呢,我我的有如下幾點見解。
try/catch
的原理有不少人不清楚怎麼去用 try/catch
。這裏我來按照我我的的看法,來推一下其原理吧,首先 js
是運行在 node.js
提供的運行時環境中的,而 node.js
是用 C++
寫的。C++
是有本身的異常處理機制的,也是有 try/catch
的 。那就說明 js
的 try/catch
的底層實現是直接經過橋,調用 C++
的 try/catch
。
而 C++
的 try/catch
具備的一些特性,你們能夠自行去了解一下,好比其中就有一個特性是這樣的:
try/catch
只能捕捉當前線程的異常。
因此這也就很好的解釋了,爲何 JS
的 try/catch
只能捕捉到同步的異常,而對於異步的異常就無能爲力了(由於異步是放在另外一個線程中執行的)。
這裏是個人推導,不表明確切答案。
這裏我推薦一篇博客:
有興趣的能夠看看。
這裏有幾個方法:
第一個方法:若是是同步的操做,能夠用
throw
來傳遞異常
看下面代碼:
try {
throw new Error('hello godkun, i am an Error ')
console.log('throw 以後的處代碼不執行')
} catch (e) {
console.log(e.message)
}
複製代碼
首先咱們要知道 throw
是以同步的方式傳遞異常的,也就是 throw
要和使用 throw
傳遞錯誤的函數擁有相同的上下文環境。
若是上下文環境中,都沒有使用 try/catch
的話,可是又 throw
了異常,那麼程序大機率會崩潰。
若是是 nodejs
,這個時候就應該再加一個進程級的 uncaughtException
來捕捉這種沒有被捕捉的異常。一般還會加上 unhandledRejection
的異常處理。
第二個方法:若是是異步的操做
有三種方式:
使用callback
,好比 nodejs
的 error first
風格
對於複雜的狀況可使用基於 Event
的方式來作,調用者來監聽對象的 error
事件
使用 promise
和 async/await
來捕捉異常
如今的問題是,怎麼去選擇哪一個方式呢?有這幾個原則:
簡單的場景,直接使用 promise
和 async/await
來捕捉異常
複雜的場景,好比可能會產生多個錯誤,這個時候最好用 Event
的方式
第三個方法:若是既有異步操做又有同步操做
怎麼辦呢?這個時候,我我的認爲,最好的方式,就是使用最新的語法:async/await
來結合 promise
和 try/catch
來完成對既有同步操做又有異步操做的異常捕捉。
第四個方法:處理異常的一些抽象和封裝
對處理異常的函數進行抽象和封裝也是提升函數質量的一個途徑。如何對處理異常進行抽象和封裝呢?有幾個方式能夠搞定它:
第一個方式:對 nodejs
來講,一般將異常處理封裝成中間件,好比基於 express/koa
的異常中間件,一般狀況下,處理異常的中間件要做爲最後一箇中間件加載,目的是爲了捕獲以前的全部中間件可能出現的錯誤
第二個方式:對前端或者 nodejs
來講,能夠將異常處理封裝成模塊,相似 Event
的那種。
第三種方式:使用裝飾器模式,對函數裝飾異常處理模塊,好比經過裝飾器對當前函數包裹一層 try/catch
。
第四種方式:使用函數式編程中的函子( Monad
)等來對異常處理進行統一包裹,這裏 的 Monad
和 try/catch
在表現上都至關於一個容器,這是一個至關強大的方法。從 Monad
能夠擴展出不少異常處理的黑科技,可是我建議慎用,由於不是全部人都能看懂的,要考慮團隊的總體技術能力,固然一我的的話,那就隨便嗨了。
合理的處理異常,須要能肯定使用哪種方式來處理異常,我大體也說了具體的選擇狀況,這裏我推薦一篇博客:
目前我見到的講的最全的處理異常的博客,但我這裏說的都是我認爲比較重要的主要的點,二者仍是有明顯區別的,你們融合一下吸取吸取吧。
好比登陸流程須要4個安全驗證,按照一般的寫法,其中一個掛了,那就所有掛了,可是這不夠魯棒性,如何去解決這個問題呢。這裏我提一下,可能不少人都不會注意到。
主要方案就使用將 promise
的鏈式寫法換一種方式寫,之前的寫法是這樣的:
僞代碼以下:
auth().then(getIP).then(getToken).then(autoLogin).then(xxx).catch(function(){})
複製代碼
通過魯棒調整後,能夠改爲以下寫法:
僞代碼以下:
auth().catch(goAuthErrorHandle).then(getIP).catch(goIPErrorHandle).then(function(r){})
複製代碼
通過微調後的代碼,直接讓登陸流程的魯棒性提高了不少,就算出錯也能夠經過錯誤處理後,繼續傳遞到下一個方法中。
我我的認爲對異常的處理,仍是要根據實際狀況來分析的。大概有如下幾點見解:
要考慮項目可維護性,團隊技術水平
我曾在一個需求中,使用了諸如函子等較爲抽象的處理異常的方法,雖然秀了一把(做死),結果致使,後續這塊的需求改動,還得我本身來。嗯,就是這麼刺激,由於同事不熟悉函數式編程。
要提早預估好項目的複雜性和重要性。
好比在作一個比較重要的業務時,一開始沒有想到異常處理須要這麼細節,並且通常初版的時候,需求並無涉及到不少異常狀況處理,可是後續需求迭代優化的時候,發現異常狀況處理是如此的多,直接致使須要重寫異常處理相關的代碼。
因此之後在項目評估的時候,要學會嘗試根據項目的重要性,來提早預留好坑位。
這也算是一種面對將來的編程模式。
關於函數的魯棒性(防護性編程),我介紹了不少東西,基本上是前端或者是 nodejs
處理異常的常規方法吧。處理異常不是一個簡單的活,工做中還得結合業務去肯定合適的異常處理方式,總之,多多實踐出真知吧。
Jest
,按照官網文檔來,本着函數式編程的思想,問題不大。能夠這麼說,讀了這篇文章,你的 git
和 gerrit
就沒有什麼問題。
關於如何閱讀 npm
包源碼的故事。
最近剛寫的一篇文章,發現沒什麼閱讀量,可是我以爲我寫的很好啊,這篇文章絕對會對絕大多數前端工程師有所啓發和幫助。
宣傳一波,想啓發更多還在路上的前端小夥伴:
加上本篇,這個系列已經寫了兩篇了,我計劃三篇搞定,可是看這狀況,後面都是硬貨,很難 7000
字之內搞定呀,後續幾篇搞定,我再 ob
一下吧。
小夥伴們能夠關注個人掘金博客或者 github
來獲取後續的系列文章更新通知。
掘金系列技術文章在 github
上彙總以下,以爲不錯的話,點個 star 鼓勵一下,我將 使命必達 ,持續輸出精品文章。
我是源碼終結者,歡迎技術交流。
也能夠進 前端狂想錄羣 你們一塊兒頭腦風暴。有想加的,由於人滿了,能夠先加我好友,我來邀請你進羣。
最後:尊重原創,轉載請註明出處哈😋