本文首發於 vivo互聯網技術 微信公衆號
連接:https://mp.weixin.qq.com/s/sd2oX0Z\_cMY8\_GvFg8pO4Q
做者:楊昆javascript
上篇 《如何編寫高質量的 JS 函數(1) -- 敲山震虎篇 》介紹了函數的執行機制,此篇將會從函數的命名、註釋和魯棒性方面,闡述如何編寫高質量的 JS 函數。前端
(一)函數命名java
從上圖能夠知道,命名和緩存是計算機科學中的兩大難題。node
本文要說的函數命名,雖然涉及到的範圍較窄,但思想都同樣,徹底能夠借鑑到其餘的形式中。react
以前閱讀過代碼大全中變量的相關章節,也針對性的瞭解過一些源碼,根據個人經驗總結,目前函數命名除了業界標準的問題外,還存在一些細節的問題,好比:git
中英語言的差別性github
不懂得從多維度去提高命名的準確性(如何從多維度去提高命名的準確性)web
下面進行簡明扼要的分析。express
漢語拼音存在多義性;漢字翻譯的輔助工具還不夠普及,所以不能用漢語方式命名。npm
最大的難點在於語法的正確使用場景。
舉個例子, react 的生命週期,以下:
componentDidMount
componentWillReceiveProps
不少人都會有疑問,爲何用 did 和 will 。
舉個例子:
componentDidMount 是 react 等生命週期的鉤子,可是爲何要這樣命名?
答案就在下圖:
注意上圖中的 did 表明通常過去時,will 表明通常未來時。
而後咱們百科通常過去式和通常未來時,如圖所示。
(1)通常過去時:
(2)通常未來時:
看上圖的紅箭頭,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 ,代碼中看不出這個函數的命名含義,只能從從源碼裏面的函數註釋中找答案。
函數註釋以下圖:
由此知道,若是函數命名存在困難,能夠經過註釋的方式,將函數的總體介紹和說明輸出文檔來解決這個問題。
函數命名很廣泛的一個現象就是帶各類前綴的函數名,以下:
- $xxx() - _xxx()
這種帶各類前綴的函數名,看起來並很差看還彆扭。核心緣由是 JS 語言不支持私有變量,致使只能使用 _ 或者 $ 來保證相應的對外不可**見。**
因此我把前端的函數命名分爲兩大類,以下:
第一類:不想暴露給外部訪問的函數(好比只給內部使用)
第二類:暴露給外部訪問的函數(各類功能方法)
而Symbol 初始化的函數命名是一種特例,代碼以下:
const ADD = Symbol('add') [ADD](a, b) { console.log('a + b') }
總結一下最佳實踐:
多學習初中高中英語語法,開源項目中的函數命名沒有那麼難理解,經過語法的學習和藉助工具,函數命名基本能夠解決,若是遇到沒法清晰描述所寫函數的目的的命名時,請務必給函數寫上良好註釋,無論函數名字有多長多難懂,只要有良好的註釋,那就是能夠接受的一件事情。
函數註釋,一方面提升了可讀性,另外一方面還能夠生成在線文檔。
一個高質量的函數,註釋少不了,可是這並不表明全部的函數都須要註釋。富有富的活法,窮有窮的瀟灑,重要或者複雜的函數,能夠寫個好註釋;簡單或者不重要的函數,能夠不寫註釋或者寫一個簡單的註釋。
那麼,目前函數的註釋都有哪幾種方式呢?
從圖中能夠看到 egg.js 的入口文件的註釋特色是簡單整潔。
繼續看下圖:
這是一個被抽象出來的基類,展現了做者 [Yiyu He] 當時寫這個類的時候,其註釋的風格有如下幾點:
第一點:構造函數的註釋規則,表達式語句的註釋規則。
第二點:註釋的取捨,有一些變量能夠不用註釋,有些要註釋,不要有那種要註釋就要所有註釋的思想。
再看兩張有趣的圖片:
看上面兩張圖的箭頭,指向的都是同一個做者 [fengmk2] 。他的函數註釋規則,第一張圖沒有空格,第二種有空格,還有對返回的 this 的註釋,好比不少人習慣將 this 直接註釋成 Object 類型。
說到函數註釋,就不能不說到 lodash.js 。因爲篇幅有限,本文就不作相應介紹了,你們自行按照上面的方式去了解。
有人說註釋要很規範,方便給別人,好比用 jsdoc 等 。個人觀點是,對一些不須要開源的 web 項目,沒有必要用 jsdoc , 理由以下:
1.繁瑣,須要按照 jsdoc 規則來。
2.我的認爲,jsdoc 有***性,文檔規則須要寫在代碼中。
若是要寫註釋說明手冊,對於大型項目,我推薦使用 apidoc , 由於 apidoc ***性不強,不要求把規則寫在代碼中,能夠把全部規則寫到一個文件中。
可是通常小項目,沒有必要單獨寫一份 api 文檔。若是是開源的大型項目,首先須要考慮是否有開源的官方網站,你會看到網上的一些開源項目官網好像很酷,其實這個世界上不缺的就是輪子,你也能夠很快的作出這樣的網站,
下面咱們來看看是如何作到的。
首先看一下 taro 源碼,以下圖:
這裏就是生成一個靜態網站的祕密,執行 npm run docs 就能夠。用的是 docusaurus 包。
從上圖中能夠知道,文檔的內容,來源於 docs 目錄,裏面都是 md 文件,開源項目的文檔說明都在這裏。
固然也有把對應的文檔直接放到對應的代碼目錄下的,好比 ant-design 以下圖:
就是直接把文檔放在組件目錄下了。
從這裏咱們能夠知道,目前流行的開源項目的官方網站是怎麼實現的,以及文檔該怎麼寫。
下面說說我本人對函數註釋(只針對函數註釋)的一些我的風格或者意見。
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') }
通常分爲普通函數和構造函數。
(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 } }
從開源項目的代碼中能夠發現,在遵照註釋的基本原則的基礎上,註釋的風格多種多樣;同一個做者不一樣項目的註釋風格也有所差異,但我會盡量的去平衡註釋和不註釋。
下圖是一個段子:
最後一句,測試測了那麼多場景,最後酒吧仍是炸了,怎麼回事?
從中咱們能夠看出,防護性編程的核心是:
把全部可能會出現的異常都考慮到,而且作相應處理。
而我我的認爲,防護性的程度要看其重要的程度。通常來講,不可能去處理全部狀況的,可是提升代碼魯棒性的有效途徑就是進行防護性的編程。
我曾經接手過一個需求,重寫微信小程序的登陸註冊綁定功能,並將代碼同步到其餘小程序(和其餘小程序的開發進行代碼交接並協助 coder 平穩完成版本過渡)。
這個項目因爲用戶的基數很大,風險程度很高,須要考慮不少場景,好比:
是否支持線上版本回退,也就是須要有前端的 AB 版本方案(線上有任何問題,能夠快速切到舊登陸方案)
須要有各類驗證:圖形驗證碼、短信驗證碼、ip 、人機、設備指紋、風控、各類異常處理、異常埋點上報等。
代碼層面的考慮:經過代碼優化,縮短總的響應時間,提升用戶體驗。
如何去合理的完成這個需求仍是比較有難度的。
PS: 關於第4點的如何確保單個節點出問題,不會影響整個登陸流程,文末有答案。
下面我就關於函數魯棒性,說一說我我的的一些見解。
在 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) 的功能。
防患於未然,從一開始就不要讓異常發生。
那如何去更好的處理各類異常,提升函數的魯棒性呢,我我的有如下幾點見解。
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 異常處理機制》 ,有興趣的能夠看看。
第一個方法:若是是同步操做,能夠用 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來捕捉異常。
第三個方法:若是既有異步操做又有同步操做
最好的方式就是使用最新的語法:async/await 來結合 promise 和 try/catch 來完成對既有同步操做又有異步操做的異常捕捉。
第四個方法:處理異常的一些抽象和封裝
對處理異常的函數進行抽象和封裝也是提升函數質量的一個途徑。如何對處理異常進行抽象和封裝呢?有幾個方式能夠搞定它:
第一種方式:對 nodejs 來講,一般將異常處理封裝成中間件,好比基於 express/koa 的異常中間件,一般狀況下,處理異常的中間件要做爲最後一箇中間件加載,目的是爲了捕獲以前的全部中間件可能出現的錯誤。
第二種方式:對前端或者 nodejs 來講,能夠將異常處理封裝成模塊,相似 Event 的那種。
第三種方式:使用裝飾器模式,對函數裝飾異常處理模塊,好比經過裝飾器對當前函數包裹一層 try/catch 。
合理的處理異常,根據具體狀況來肯定使用合理的方式處理異常
這裏推薦一篇博客:《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 處理異常的常規方法。
處理異常不是一個簡單的活,工做中還得結合業務去肯定合適的異常處理方式,總之,多多實踐出真知。