1. 程式人生 > >如何編寫高質量的 JS 函式(2) -- 命名/註釋/魯棒篇

如何編寫高質量的 JS 函式(2) -- 命名/註釋/魯棒篇

本文首發於 vivo網際網路技術 微信公眾號 
連結:https://mp.weixin.qq.com/s/sd2oX0Z_cMY8_GvFg8pO4Q
作者:楊昆

上篇 《如何編寫高質量的 JS 函式(1) -- 敲山震虎篇 》介紹了函式的執行機制,此篇將會從函式的命名、註釋和魯棒性方面,闡述如何編寫高質量的 JS 函式。

(一)函式命名

一、目前前端的函式命名存在什麼問題

從上圖可以知道,命名和快取是電腦科學中的兩大難題。

本文要說的函式命名,雖然涉及到的範圍較窄,但思想都一樣,完全可以借鑑到其他的形式中。

之前閱讀過程式碼大全中變數的相關章節,也針對性的瞭解過一些原始碼,根據我的經驗總結,目前函式命名除了業界標準的問題外,還存在一些細節的問題,比如:

  • 中英語言的差異性
  • 不懂得從多維度去提升命名的準確性(如何從多維度去提升命名的準確性)
  • 不會使用輔助工具(如何使用輔助工具)

下面進行簡明扼要的分析。

二、漢語和英語的差異性

1、為什麼不能用漢語方式命名呢?

漢語拼音存在多義性;漢字翻譯的輔助工具還不夠普及,因此不能用漢語方式命名。

2、用英語時遇到的困難

最大的難點在於語法的正確使用場景。

舉個例子, react 的生命週期,如下:

  • componentDidMount
  • componentWillReceiveProps
  • shouldComponentUpdate

很多人都會有疑問,為什麼用 did 和 will 。

三、如何讓命名有英語 style

舉個例子:

  • componentDidMount 是 react 等生命週期的鉤子,但是為什麼要這樣命名?
  • componentWillReceiveProps 為什麼要這樣命名?

答案就在下圖:

注意上圖中的 did 代表一般過去時,will 代表一般將來時。

然後我們百科一般過去式和一般將來時,如圖所示。

(1)一般過去時:

(2)一般將來時:

看上圖的紅箭頭,did 表示一般過去時,時是指動作發生的時間,用在這裡,突出了鉤子的含義,一旦 mount 成功就執行此函式。will 同理。

四、通過函式返回結果來命名

這是個小特性,比如 shouldComponentUpdate , 為什麼 should 放在最前面。

因為這個函式返回的值是布林值。那麼我們可以理解為這是個問句,通過問句的形式來告訴我們,這裡具有不確定性,需要根據返回值來判斷是否更新。

五、藉助工具

1、藉助谷歌翻譯

2、藉助 codelf

這是一個神器,用來搜尋各種開源專案中的變數命名,以供參考。

地址:unbug.github.io/codelf/

對應名字的 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 ,程式碼中看不出這個函式的命名含義,只能從從原始碼裡面的函式註釋中找答案。

函式註釋如下圖:

由此知道,如果函式命名存在困難,可以通過註釋的方式,將函式的整體介紹和說明輸出文件來解決這個問題。

七、函式命名的分類

函式命名很普遍的一個現象就是帶各種字首的函式名,如下:

- $xxx()
- _xxx()

這種帶各種字首的函式名,看起來並不好看還彆扭。核心原因是 JS 語言不支援私有變數,導致只能使用 _ 或者 $ 來保證相應的對外不可****見。

所以我把前端的函式命名分為兩大類,如下:

第一類:不想暴露給外部訪問的函式(比如只給內部使用)

第二類:暴露給外部訪問的函式(各種功能方法)

而Symbol 初始化的函式命名是一種特例,程式碼如下:​​​​​​

const ADD = Symbol('add')

[ADD](a, b) {
console.log('a + b')
}

  

八、總結

總結一下最佳實踐:

多學習初中高中英語語法,開源專案中的函式命名沒有那麼難理解,通過語法的學習和藉助工具,函式命名基本可以解決,如果遇到無法清晰描述所寫函式的目的的命名時,請務必給函式寫上良好註釋,不管函式名字有多長多難懂,只要有良好的註釋,那就是可以接受的一件事情。

(二)函式的註釋

一、有名的npm包的一些註釋風格

函式註釋,一方面提高了可讀性,另一方面還可以生成線上文件。

一個高質量的函式,註釋少不了,但是這並不代表所有的函式都需要註釋。富有富的活法,窮有窮的瀟灑,重要或者複雜的函式,可以寫個好註釋;簡單或者不重要的函式,可以不寫註釋或者寫一個簡單的註釋。

那麼,目前函式的註釋都有哪幾種方式呢?

1、egg.js 的註釋風格

從圖中可以看到 egg.js 的入口檔案的註釋特點是簡單整潔。

繼續看下圖:

這是一個被抽象出來的基類,展示了作者 [Yiyu He] 當時寫這個類的時候,其註釋的風格有以下幾點:

第一點:建構函式的註釋規則,表示式語句的註釋規則。

第二點:註釋的取捨,有一些變數可以不用註釋,有些要註釋,不要有那種要註釋就要全部註釋的思想。

再看兩張有趣的圖片:

看上面兩張圖的箭頭,指向的都是同一個作者 [fengmk2] 。他的函式註釋規則,第一張圖沒有空格,第二種有空格,還有對返回的 this 的註釋,比如很多人習慣將 this 直接註釋成 Object 型別。

2、lodash.js

說到函式註釋,就不能不說到 lodash.js 。由於篇幅有限,本文就不做相應介紹了,大家自行按照上面的方式去了解。

二、通過註釋生成線上文件的思考

有人說註釋要很規範,方便給別人,比如用 jsdoc 等 。我的觀點是,對一些不需要開源的 web 專案,沒有必要用 jsdoc , 理由如下:

1.繁瑣,需要按照 jsdoc 規則來。

2.個人認為,jsdoc 有入侵性,文件規則需要寫在程式碼中。

如果要寫註釋說明手冊,對於大型專案,我推薦使用 apidoc , 因為 apidoc 入侵性不強,不要求把規則寫在程式碼中,可以把所有規則寫到一個檔案中。

但是一般小專案,沒有必要單獨寫一份 api 文件。如果是開源的大型專案,首先需要考慮是否有開源的官方網站,你會看到網上的一些開源專案官網好像很酷,其實這個世界上不缺的就是輪子,你也可以很快的做出這樣的網站,

下面我們來看看是如何做到的。

首先看一下 taro 原始碼,如下圖:

這裡就是生成一個靜態網站的祕密,執行 npm run docs 就可以。用的是 docusaurus 包。

從上圖中可以知道,文件的內容,來源於 docs 目錄,裡面都是 md 檔案,開源專案的文件說明都在這裡。

當然也有把對應的文件直接放到對應的程式碼目錄下的,比如 ant-design 如下圖:

就是直接把文件放在元件目錄下了。

從這裡我們可以知道,目前流行的開源專案的官方網站是怎麼實現的,以及文件該怎麼寫。

三、我個人的註釋習慣

下面說說我本人對函式註釋(只針對函式註釋)的一些個人風格或者意見。

1、分享 VSCode 關於註釋的幾個工具

  • Better Comments 給註釋上色
  • Document This 自動生成註釋
  • TODO Highlight 高亮 TODO ,並可以搜尋所有 TODO

下面是一張演示圖:

2、寫和不寫註釋的平衡

我的觀點是不影響可讀性,複雜度低,對外界沒有過度干涉的函式可以不寫註釋。

3、表示式語句的註釋

函式內,表示式語句的註釋可以簡單點。如下圖所示,// 後面加簡要說明。

function add(a, b) {
// sum ....
let sum = a + b
}

4、TODO 註釋

function say() {
// TODO: 編寫 say 具體內容
console.log('say')
}

5、FIXME 註釋

function fix() {
// FIXME: 刪除 console.log方法
console.log('fix')
}

6、函式註釋

一般分為普通函式和建構函式。

(1)普通函式註釋:

/**
* add
* @param {Number} a - 數字
* @param {Number} b - 數字
* @returns {Number} result - 兩個整數之和
*/
function add(a, b) {
// FIXME: 這裡要對 a, b 引數進行型別判斷
let result = a + b
return (result)
}

(2)建構函式註釋:

class Kun {
/**
* @constructor
* @param {Object} opt - 配置物件 
*/
constructor(opt = {}) {
// 語句註釋
this.config = opt
}
}

7、總結

從開源專案的程式碼中可以發現,在遵守註釋的基本原則的基礎上,註釋的風格多種多樣;同一個作者不同專案的註釋風格也有所差別,但我會盡可能的去平衡註釋和不註釋。

(三)函式的魯棒性(防禦性程式設計)

下圖是一個段子:

最後一句,測試測了那麼多場景,最後酒吧還是炸了,怎麼回事?

從中我們可以看出,防禦性程式設計的核心是:

把所有可能會出現的異常都考慮到,並且做相應處理。

而我個人認為,防禦性的程度要看其重要的程度。一般來說,不可能去處理所有情況的,但是提高程式碼魯棒性的有效途徑就是進行防禦性的程式設計。

一、一個專案的思考

我曾經接手過一個需求,重寫微信小程式的登入註冊繫結功能,並將程式碼同步到其他小程式(和其他小程式的開發進行程式碼交接並協助 coder 平穩完成版本過渡)。

這個專案由於使用者的基數很大,風險程度很高,需要考慮很多場景,比如:

  1. 是否支援線上版本回退,也就是需要有前端的 AB 版本方案(線上有任何問題,可以快速切到舊登入方案)

  2. 需要有各種驗證:圖形驗證碼、簡訊驗證碼、ip 、人機、裝置指紋、風控、各種異常處理、異常埋點上報等。

  3. 程式碼層面的考慮:通過程式碼優化,縮短總的響應時間,提高使用者體驗。

  4. 如何確保單個節點出問題,不會影響整個登入流程。

如何去合理的完成這個需求還是比較有難度的。

PS: 關於第4點的如何確保單個節點出問題,不會影響整個登入流程,文末有答案。

下面我就關於函式魯棒性,說一說我個人的一些看法。

二、前端函式魯棒性的幾種方式

1、入參要魯棒性

在 ES6+ 到來後,函式的入參寫法已經得到了質的提高和優化。看下面程式碼:

function print(obj = {}) {
console.log('name', obj.name)
console.log('age', obj.age)
}
print 函式,入參是 obj 通過 obj = {} 來給入參設定預設的引數值,從而提高入參的魯棒性。

同時會發現,如果入參的預設值是 {} ,那函式裡面的 obj.name 就會是 undefined ,這也不夠魯棒,所以下面就要說說函式內表示式語句的魯棒性了。

2、函式內表示式語句要魯棒性

繼續上個例子:

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) 的功能。

3、函式異常處理的兩個層面

  • 防患於未然,從一開始就不要讓異常發生。
  • 異常如果出現了,該怎麼去處理出現的異常。

那如何去更好的處理各種異常,提高函式的魯棒性呢,我個人有以下幾點看法。

4、推導一下 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 只能捕捉到同步的異常,而對於非同步的異常就無能為力了(因為非同步是放在另一個執行緒中執行的)。

這裡是我的推導,不代表確切答案。

這裡我推薦一篇部落格:《C++中try、catch 異常處理機制》 ,有興趣的可以看看。

5、合理的處理異常

第一個方法:如果是同步操作,可以用 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 的異常處理。

第二個方法:如果是非同步的操作

有三種方式:

  1. 使用 callback ,比如 nodejs 的 error first 風格。

  2. 對於複雜的情況可以使用基於 Event 的方式來做,呼叫者來監聽物件的 error 事件。

  3. 使用 promise 和 async/await 來捕捉異常。

怎麼去選擇哪個方式呢?依據以下原則:

  1. 簡單的場景,直接使用 promise 和 async/await來捕捉異常。

  2. 複雜的場景,比如可能會產生多個錯誤,這個時候最好用 Event 的方式。

第三個方法:如果既有非同步操作又有同步操作

最好的方式就是使用最新的語法:async/await 來結合 promise 和 try/catch 來完成對既有同步操作又有非同步操作的異常捕捉。

第四個方法:處理異常的一些抽象和封裝

對處理異常的函式進行抽象和封裝也是提高函式質量的一個途徑。如何對處理異常進行抽象和封裝呢?有幾個方式可以搞定它:

  1. 第一種方式:對 nodejs 來說,通常將異常處理封裝成中介軟體,比如基於 express/koa 的異常中介軟體,通常情況下,處理異常的中介軟體要作為最後一箇中間件載入,目的是為了捕獲之前的所有中介軟體可能出現的錯誤。

  2. 第二種方式:對前端或者 nodejs 來說,可以將異常處理封裝成模組,類似 Event 的那種。

  3. 第三種方式:使用裝飾器模式,對函式裝飾異常處理模組,比如通過裝飾器對當前函式包裹一層 try/catch 。

  4. 第四種方式:使用函數語言程式設計中的函子( Monad )等來對異常處理進行統一包裹,這裡 的 Monad 和 try/catch 在表現上都相當於一個容器,這是一個相當強大的方法。從 Monad 可以擴展出很多異常處理的黑科技,但是我建議慎用,因為不是所有人都能看懂的,要考慮團隊的整體技術能力,當然一個人的話,那就隨便嗨了。

合理的處理異常,根據具體情況來確定使用合理的方式處理異常

這裡推薦一篇部落格:《Callback Promise Generator Async-Await 和異常處理的演進》

三、如何確保單個節點出問題,不會影響整個登入流程

比如登入流程需要4個安全驗證,按照通常的寫法,其中一個掛了,那就全部掛了,但是這不夠魯棒性,如何去解決這個問題呢。

主要方案就使用將 promise 的鏈式寫法換一種方式寫,以前的寫法是這樣的:

虛擬碼如下:

auth().then(getIP).then(getToken).then(autoLogin).then(xxx).catch(function(){})
經過魯棒調整後,可以改成如下寫法:

虛擬碼如下:

auth().catch(goAuthErrorHandle).then(getIP).catch(goIPErrorHandle).then(function(r){})

經過微調後的程式碼,直接讓登入流程的魯棒性提升了很多,就算出錯也可以通過錯誤處理後,繼續傳遞到下一個方法中。

四、我個人對異常處理的看法

我個人認為對異常的處理,還是要根據實際情況來分析的。大概有以下幾點看法:

要考慮專案可維護性,團隊技術水平

我曾在一個需求中,使用了諸如函子等較為抽象的處理異常的方法,雖然秀了一把(作死),結果導致後續這塊的需求改動,還得我自己來。

要提前預估好專案的複雜性和重要性。

比如在做一個比較重要的業務時,一開始沒有想到異常處理需要這麼細節,而且一般第一版的時候,需求並沒有涉及到很多異常情況處理,但是後續需求迭代優化的時候,發現異常情況處理是如此的多,直接導致需要重寫異常處理相關的程式碼。

所以以後在專案評估的時候,要學會嘗試根據專案的重要性,來提前預留好坑位。

這也算是一種面對未來的程式設計模式。

五、總結

關於函式的魯棒性(防禦性程式設計),本文主要介紹了前端或者是 nodejs 處理異常的常規方法。

處理異常不是一個簡單的活,工作中還得結合業務去確定合適的異常處理方式,總之,多多實踐出真知。

更多內容敬請關注 vivo 網際網路技術 微信公眾號

注:轉載文章請先與微訊號:labs2020 聯