使用JSDoc提升代碼的可讀性

工做了四年多,基本上都在圍繞着 JavaScript 作事情。
寫的代碼多了,看的代碼也多了,由衷的以爲,寫出別人看不懂的代碼並非什麼能力,寫出全部人都能讀懂的代碼,纔是真的牛X。
衆所周知, JavaScript 是一個弱類型的腳本語言,這就意味着,從編輯器中並不能直觀的看出這段代碼的做用是什麼,有些事情只有等到代碼真正的運行起來纔可以肯定。
因此爲了解決大型項目中 JavaScript 維護成本高的問題,前段時間咱們團隊開始使用 TypeScript,可是由前幾年所積累下來的代碼,並非說改立馬都能所有改完的,因此這個重構將是一個漫長的過程。
在重構同時咱們仍是須要繼續維護原有的 JavaScript 項目的,而 JSDoc 剛好是一箇中間過渡的方案,可讓咱們以註釋的形式來下降 JavaScript 項目的維護難度,提高可讀性。

做用

本人使用的是 vs code 編輯器,內置了對 jsdoc 的各類支持,同時還會根據部分常量,語法來推測出對應的類型
能夠很方便的在編輯器中看到效果,因此下面全部示例都是基於 vscode 來作的。

首先,JSDoc 並不會對源碼產生任何的影響,全部的內容都是寫在註釋裏邊的。
因此並不須要擔憂 JSDoc 會對你的程序形成什麼負面影響。 javascript

能夠先來看一個普通的 JavaScript 文件在編輯器中的展現效果:
html

很顯而易見的,編輯器也不可以肯定這個函數到底是什麼含義,由於任何類型的兩個參數均可以進行相加。
因此編輯器就會使用一個在 TypeScript 中常常出現用來標識任意類型的 any 關鍵字來描述函數的參數以及返回值。 java

而這種狀況下咱們能夠很簡單的使用 JSDoc 來手動描述這個函數的做用:
異步

實際上有些函數是須要手動指定 @return {TYPE}來肯定函數返回值類型的,但由於咱們函數的做用就是經過兩個參數相加並返回,因此編輯器推算出了函數返回值的類型。

對比上下兩段代碼,代碼上並無什麼區別,也許有人會嗤之以鼻,認爲代碼已經足夠清晰,並不須要額外的添加註釋來講明。
這種盲目自信通常會在接手了其餘人更爛的代碼後被打破,而後再反思本身究竟作錯了什麼,須要去維護這樣的代碼。 async

亦或者咱們來放出一個稍微複雜一些的例子:編輯器

看似清晰、簡潔的一個示例,徹底看不出什麼毛病 _除了兩個異步await能夠合併成一個_。
確實,若是這段代碼就這麼一直躺在項目中,也不去改需求,那麼這段代碼能夠說是很完美的存在了。
若是這段代碼一直是寫下這段代碼的做者在維護,那麼這段代碼在維護上也不會有什麼風險。 函數

不過若是哪天這段代碼被交接了出去,換其餘的小夥伴來維護。
那麼他可能會有這麼幾個疑問:ui

  1. getUserInfo的返回值是什麼結構
  2. createOrder的返回值又是什麼結構
  3. notify中傳入的兩個變量又都是用來作什麼的

咱們也只可以從notify函數中找到一些線索,查看到前兩個函數所返回對象的部分屬性, _可是仍然不能知道這些屬性的類型是什麼_。
而想要維護這樣的一段代碼,就須要佔用不少腦容量去記憶,這其實是一個性價比很是低的事情,當這段代碼再轉給第三我的時,第三我的還須要再經歷完整的流程,一個個函數、一行行代碼去閱讀,去記憶。
若是你把這個看成是對程序的深刻了解程度、對業務的嫺熟掌握,那麼我以爲我也幫不了你了。
就像是如今超市結帳時,沒有櫃員會以可以記憶N多商品價格而感到驕傲,掃碼槍能作到的事情,爲何要佔用你的大腦呢。spa

基礎用法

如上文所說的,JSDoc 是寫在註釋中的一些特定格式內容。
在 JavaScript 文件中大部分的標記都是塊級形式的,也就是使用 /** XXX */ 來進行定義,不過若是你願意的話,也能夠寫到代碼裏邊去。code

JSDoc 提供了不少種標記,用於各類場景。
但並非全部的都是經常使用的(並且使用了 vscode 之後,不少須要手動指定的標記,編輯器都可以代替你完成),經常使用的無外乎如下幾個:

  • @type 標識變量類型
  • @param 標識函數參數類型及描述
  • @return 標識函數返回值類型及描述
完整的列表能夠在這裏找到 Block tags

基本上使用以上三種標記之後,已經可以解決絕大部分的問題。
JSDoc 在寫法上有着特定的要求,好比說行內也必需要是這樣的結構 /** XXX */,若是是 /* XXX */ 則會被忽略。
而多行的寫法是比較經常使用的,在 vscode 中能夠直接在函數上方鍵入 /** 而後回車,編輯器會自動填充不少的內容,包括參數類型、參數描述以及函數描述的預留位置,使用TAB鍵便可快速切換。

實際上@type的使用頻率相較於其餘兩個是很低的,由於大多數狀況下@type用於標識變量的類型。
而變量的來源基本上只有兩個 1. 基本類型賦值 2. 函數返回值
首先是第一個基本類型的賦值,這個基本上 vscode 就幫你作了,而不須要本身手動的去指定。
而另一個函數的返回值,若是咱們在函數上添加了@return後,那麼調用該函數並獲取返回值的變量類型也會被設置爲@return對應的類型。

type

不過由於其餘兩個標記中都有類型相關的指定,因此就拿 @type 來講明一下

首先,在 JSDoc 中是支持全部的基本類型的,包括數字、字符串、布爾值之類的。

/** @type {number} */
/** @type {string} */
/** @type {boolean} */
/** @type {RegExp} */

// 或者是一個函數
/** @type {function} */

// 一個包含參數的函數
/** @type {function(number, string)} */

// Object結構的參數
/** @type {function({ arg1: number, arg2: string })} */

// 一個包涵參數和返回值的函數
/** @type {function(number, string): boolean} */
在 vscode 中鍵入以上的註釋,均可以很方便的獲得動態提示。
固然了,關於函數的,仍是推薦使用 @param 和 @return 來實現,效果更好一些

擴展複雜類型

上邊的示例大可能是基於基本類型的描述,但實際開發過程當中不會說只有這麼些基本類型供你使用的。
必然會存在着大量的複雜結構類型的變量、參數或返回值。

關於函數參數,在 JSDoc 中兩種方式能夠描述複雜類型:

不過這個只能應用在@param中,並且複用性並不高,若是有好幾處一樣結構的定義,那咱們就須要把這樣的註釋拷貝多份,顯然不是一個優雅的寫法。
又或者咱們可使用另外兩個標記,@typedef@property,格式都與上邊提到的標記相似,能夠應用在全部須要指定類型的地方:

使用@typedef定義的類型能夠很輕鬆的複用,在須要的地方直接指定咱們定義好的類型便可。
同理,這樣的自定義類型能夠直接應用在@return中。

param

這個算是比較重要的一個標記了,用來標記函數參數的相關信息。
具體的格式是這樣的(切換到 TypeScript 後通常會移除類型的定義,改用代碼中的類型定義):

/**
 * @param {number} param 描述
 */
function test (param) { }

// 或者能夠結合着 @type 來寫(雖然說不多會這麼寫)

/**
 * @param param 描述
 */
function test (/** @type number */ param) { }

可選參數

若是咱們想要表示一個參數爲可選的參數,能夠的在參數名上包一個[]便可。

/**
 * @param {number} [param] 描述
 */
function test (param) { }

同事在文檔中還提到了關於默認值的寫法,實際上若是你的可選參數在參數位已經有了默認值的處理,那麼就再也不須要額外的添加[]來表示了,vscode 會幫助你標記。

// 文檔中提到的默認值寫法
/**
 * @param {number} [param=123] 描述
 */
function test (param = 123) { }

// 而實際上使用 vscode 之後就能夠簡化爲
/**
 * @param param 描述
 */
function test (param = 123) { }

二者效果是同樣的,而且因爲咱們手動指定了一個基礎類型的值,那麼咱們連類型的指定均可以省去了,簡單的定義一下參數的描述便可。

return

該標記就是用來指定函數的返回值,用法與@param類型,而且基本上這兩個都會同時出現,與@param的區別在於,由於@return只會有一個,因此不會像前者同樣還須要指定參數名。

/**
 * @return {number} 描述
 */
function test () { }

Promise 類型的返回值處理

如今這個年代,基本上Promise已經普及開來,因此不少函數的返回值可能並非結果,而是一個Promise
因此在vscode中,基於Promise去使用@return,有兩種寫法可使用:

// 函數返回 Promise 實例的狀況能夠這麼指定類型
/**
 * @return {Promise<number>}
 */
function test () {
  return new Promise((res) => {
    res(1)
  })
}

// 或者使用 async 函數定義的狀況下能夠省略 @return 的聲明
async function test () {
  return 1
}

  // 若是返回值是一個其餘定義了類型的函數 or 變量,那麼效果同樣
async function test () {
  return returnVal()
}

/** @return {string} */
function returnVal () {}

小結

再回到咱們最初的那個代碼片斷上,將其修改成添加了 JSDoc 版本的樣子:

/**
 * @typedef   {Object} UserInfo
 * @property  {number} uid  用戶UID
 * @property  {string} name 暱稱
 * 
 * @typedef   {Object} Order
 * @property  {number} orderId 訂單ID
 * @property  {number} price   訂單價格
 */
async function main () {
  const uid = 1

  const orders = await createOrder(uid)

  const userInfo = await getUserInfo(uid)

  await notify(userInfo, orders)
}

/**
 * 獲取用戶信息
 * @param   {number} uid 用戶UID
 * @return  {Promise<UserInfo>}
 */
async function getUserInfo (uid) { }

/**
 * 建立訂單
 * @param  {number} uid 用戶UID
 * @return {Promise<Order>}
 */
async function createOrder (uid) { }

/**
 * 發送通知
 * @param {UserInfo} userInfo 
 * @param {Order}    orders 
 */
async function notify (userInfo, orders) { }

實際上並無添加幾行文本,在切換到 TypeScript 以前,使用 JSDoc 可以在必定程度上下降維護成本,尤爲是使用 vscode 之後,要手動編寫的註釋其實是沒有多少的。
可是帶來的好處就是,維護者可以很清晰的看出函數的做用,變量的類型。代碼即文檔。
而且在進行平常開發時,結合編輯器的自動補全、動態提示功能,想必必定是可以提升開發體驗的。

上邊介紹的只是 JSDoc 經常使用的幾個標記,實際上還有更多的功能沒有提到,具體的文檔地址:jsdoc

參考資料

相關文章
相關標籤/搜索