您好,登錄后才能下訂單哦!
本文首發于 vivo互聯網技術 微信公眾號?
鏈接:https://mp.weixin.qq.com/s/sd2oX0Z\_cMY8\_GvFg8pO4Q
作者:楊昆
上篇 《如何編寫高質量的 JS 函數(1) -- 敲山震虎篇 》介紹了函數的執行機制,此篇將會從函數的命名、注釋和魯棒性方面,闡述如何編寫高質量的 JS 函數。
(一)函數命名
從上圖可以知道,命名和緩存是計算機科學中的兩大難題。
本文要說的函數命名,雖然涉及到的范圍較窄,但思想都一樣,完全可以借鑒到其他的形式中。
之前閱讀過代碼大全中變量的相關章節,也針對性的了解過一些源碼,根據我的經驗總結,目前函數命名除了業界標準的問題外,還存在一些細節的問題,比如:
中英語言的差異性
不懂得從多維度去提升命名的準確性(如何從多維度去提升命名的準確性)
下面進行簡明扼要的分析。
漢語拼音存在多義性;漢字翻譯的輔助工具還不夠普及,因此不能用漢語方式命名。
最大的難點在于語法的正確使用場景。
舉個例子, react 的生命周期,如下:
componentDidMount
componentWillReceiveProps
很多人都會有疑問,為什么用 did 和 will 。
舉個例子:
componentDidMount 是 react 等生命周期的鉤子,但是為什么要這樣命名?
答案就在下圖:
注意上圖中的 did 代表一般過去時,will 代表一般將來時。
然后我們百科一般過去式和一般將來時,如圖所示。
(1)一般過去時:
(2)一般將來時:
看上圖的紅箭頭,did 表示一般過去時,時是指動作發生的時間,用在這里,突出了鉤子的含義,一旦 mount 成功就執行此函數。will 同理。
這是個小特性,比如 shouldComponentUpdate , 為什么 should 放在最前面。
因為這個函數返回的值是布爾值。那么我們可以理解為這是個問句,通過問句的形式來告訴我們,這里具有不確定性,需要根據返回值來判斷是否更新。
這是一個神器,用來搜索各種開源項目中的變量命名,以供參考。
地址: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')
}
總結一下最佳實踐:
多學習初中高中英語語法,開源項目中的函數命名沒有那么難理解,通過語法的學習和借助工具,函數命名基本可以解決,如果遇到無法清晰描述所寫函數的目的的命名時,請務必給函數寫上良好注釋,不管函數名字有多長多難懂,只要有良好的注釋,那就是可以接受的一件事情。
函數注釋,一方面提高了可讀性,另一方面還可以生成在線文檔。
一個高質量的函數,注釋少不了,但是這并不代表所有的函數都需要注釋。富有富的活法,窮有窮的瀟灑,重要或者復雜的函數,可以寫個好注釋;簡單或者不重要的函數,可以不寫注釋或者寫一個簡單的注釋。
那么,目前函數的注釋都有哪幾種方式呢?
從圖中可以看到?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 處理異常的常規方法。
處理異常不是一個簡單的活,工作中還得結合業務去確定合適的異常處理方式,總之,多多實踐出真知。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。