可讀性友好的JavaScript:兩個專家的故事

每個人都想成為專家,但什麼才是專家呢?這些年來,我見過兩種被稱為「專家」的人。專家一是指對語言中的每一個工具都了如指掌的人,而且無論是否有幫助,都一定要用好每一點。專家二也知道每一個語法,但他們對採用什麼來解決問題比較挑剔,會考慮很多因素,包括與程式碼有關的和無關的。

你能猜猜我們想讓哪位專家加入我們的團隊嗎?如果你說是專家二,那你猜對了。他們是專註於編寫可讀性好的 JavaScript 程式碼的開發人員,其他人可以理解和維護。他們能把複雜的事情簡單化。但「可讀性」很少是確定的–事實上,它在很大程度上是基於主觀感受。那麼,專家們在編寫可讀性程式碼時應該以什麼為目標?是否有明確的正確和錯誤的選擇?有,這視情況而定。

顯而易見的選擇

為了提高開發者的體驗,TC39 近年來在 ECMAScript 中加入了很多新功能,包括很多從其他語言中借鑒的成熟模式。ES2019 年新增的一個功能是 Array.prototype.flat() 它接受一個表示深度或 Infinity 參數,並將一個數組扁平化。如果沒有給出參數,深度默認為 1。

在增加這個功能之前,我們需要用下面的語法將一個數組扁平化為單層。

let arr = [1, 2, [3, 4]]

;[].concat.apply([], arr)
// [1, 2, 3, 4]

當我們添加了 flat() 後,同樣的功能可以用一個單一的、描述性的函數來表達。

arr.flat()
// [1, 2, 3, 4]

第二行程式碼是否更具可讀性?答案是肯定的。事實上,兩位專家都會同意。

並不是每個開發人員都會知道 flat() 的存在。但他們不需要,因為 flat() 是一個描述性動詞,它可以傳達正在發生的意義。它比 concat.apply() 要直觀得多。

對於新語法是否比舊語法好這個問題,這是少有的有明確答案的情況。兩位專家,每個人都熟悉這兩種語法,都會選擇第二種。他們會選擇更短、更清晰、更容易維護的一行程式碼。

但選擇和權衡並不總是那麼具有決定性的。

狀況檢查

JavaScript 的神奇之處在於它的用途非常廣泛。它在網路上的廣泛使用是有原因的。至於你認為這是好事還是壞事,那就另當別論了。

但是,隨著這種多功能性的出現,也帶來了選擇的矛盾。你可以用很多不同的方式來編寫同樣的程式碼。你如何確定哪種方式是「正確的」?除非你了解可用的選項和它們的局限性,否則你甚至無法開始做決定。

讓我們以函數式編程的 map() 為例。我將通過各種迭代來講解,這些迭代都會產生相同的結果。

這是我們 map() 例子的最簡潔版本。它使用了最少的字元,只有一行程式碼。這是我們的基準線。

const arr = [1, 2, 3]
let multipliedByTwo = arr.map(el => el * 2)
// multipliedByTwo is [2, 4, 6]

接下來這個例子只增加了兩個字元:括弧。有什麼損失嗎?又得到了什麼呢?一個有多個參數的函數總是需要使用括弧,這有什麼不同嗎?我認為是的。在這裡加入它們並沒有什麼壞處,而且當你不可避免地寫一個有多個參數的函數時,它提高了一致性。事實上,當我寫這個的時候,Prettier 執行了這個約束,它不希望我創建一個沒有括弧的箭頭函數。

let multipliedByTwo = arr.map((el) => el * 2)

讓我們再進一步。我們添加了大括弧和回車。現在,這開始看起來更像一個傳統的函數定義。如果有一個和函數邏輯一樣長的關鍵字,可能會顯得有些矯枉過正。然而,如果函數超過一行,這個額外的語法又是必須的。我們是否假定我們不會有任何其他超過一行的函數?這似乎很值得懷疑。

let multipliedByTwo = arr.map((el) => {
  return el * 2
})

接下來我們不使用箭頭函數。我們使用與上面相同的語法,但我們換成了 function 關鍵字。這很有意思,因為任何情況下這種語法都能用;任何數量的參數或行數都不會導致什麼問題,所以這更具有一致性。它比我們最初的定義更啰嗦,但這是一件壞事嗎?這對一個新的程式設計師,或者一個精通 JavaScript 以外語言的人來說,會有怎樣的衝擊?相比之下,一個精通 JavaScript 的人是否會因為這個語法而感到沮喪?

let multipliedByTwo = arr.map(function (el) {
  return el * 2
})

最後我們到了最後一個選項:只傳遞函數。而 timesTwo 可以使用我們喜歡的任何語法來寫。同樣,沒有任何情況傳遞函數名會造成問題。但是退一步想一想,這是否會讓人感到困惑。如果你是這個程式碼庫的新手,是否清楚 timesTwo 是一個函數而不是一個對象?當然,map() 是為了給你一個提示,但錯過這個細節也不是沒有道理的。timesTwo 被聲明和初始化的位置呢?它容易找到嗎?是否清楚它在做什麼,以及它是如何影響這個結果的?這些都是重要的考慮因素。

const timesTwo = (el) => el * 2
let multipliedByTwo = arr.map(timesTwo)

正如你所看到的,這裡沒有明確的答案。但為你的程式碼庫做出正確的選擇意味著了解所有的選項及其局限性。並且知道一致性需要括弧、大括弧和 return 關鍵字。

在編寫程式碼時,你必須問自己一些問題。性能的問題通常是最常見的。但是當你在看功能相同的程式碼時,你的判斷應該基於人–人如何消費程式碼。

新的並不總是更好

到目前為止,我們已經找到了一個明確的例子,說明兩位專家都會採用最新的語法,即使它並不為人所知。我們還看了一個例子,它提出了很多問題,但沒有那麼多答案。

現在是時候深入研究我以前寫過的程式碼了……但被刪除了。這是讓我第一次成為專家的程式碼,使用了一個鮮為人知的語法來解決問題,但對我的同事來說它破壞了我們程式碼庫的可維護性。

解構賦值可以讓你從對象(或數組)中解開值。它通常看起來像這樣。

const { node } = exampleObject

它在一行中初始化一個變數並給它賦值。但這並不是必須的。

let node
;({ node } = exampleObject)

最後一行程式碼使用解構給一個變數賦值,但變數聲明發生在它之前的一行。這並不是一件稀奇古怪的事情,但很多人並不知道你可以這樣做。

但仔細看看這段程式碼。它為那些不使用分號結束行的程式碼強行加上了一個尷尬的分號。它將命令用括弧包裹起來,並加上大括弧;完全不清楚這是在做什麼。它不容易閱讀,而且,作為專家,它不應該出現在我寫的程式碼中。

let node
node = exampleObject.node

這個程式碼解決了這個問題。它很好用,很清楚它的作用,我的同事們不用查就能明白。對於解構語法,我可以做並不代表我應該做。

程式碼不是一切

正如我們所看到的那樣,專家二的解決方案很少能單憑程式碼就能明顯地看出;但每個專家會寫哪些程式碼,還是有明顯的區別。這是因為程式碼是給機器看的,而人類要解釋它。所以還有一些非程式碼因素需要考慮!

你為一個 JavaScript 開發團隊所做的語法選擇,與你為一個不沉浸於細枝末節的多語言團隊應該做的選擇是不同的。

讓我們以擴展運算符(...) 與 concat() 為例。

擴展運算符是幾年前添加到 ECMAScript 中的,它得到了廣泛的應用。它是一種實用的語法,它可以做很多不同的事情。其中之一就是連接數組。

const arr1 = [1, 2, 3]
const arr2 = [9, 11, 13]
const nums = [...arr1, ...arr2]

雖然擴展運算符很強大,但它並不是一個很直觀的符號。所以除非你已經知道它的作用,否則它並沒有極大的幫助。雖然兩位專家可能會安全地假設一個 JavaScript 專家團隊熟悉這種語法,但專家二可能會質疑一個多語言程式設計師團隊是否如此。相反,專家二可能會選擇 concat() 方法來代替,因為它是一個描述性動詞,你可以從程式碼的上下文中理解。

這段程式碼給我們提供了和上面擴展運算符例子一樣的數字結果。

const arr1 = [1, 2, 3]
const arr2 = [9, 11, 13]
const nums = arr1.concat(arr2)

而這只是人為因素影響程式碼選擇的一個例子。例如,一個由很多不同團隊接觸的程式碼庫,可能必須持有更嚴格的標準,不一定能跟上最新最強的語法。然後,你站在源程式碼以外的視角,考慮你的工具鏈中的其他因素,這些因素會讓在這些程式碼上工作的人感到更輕鬆或者更困難。有一些程式碼,可以以一種敵視測試的方式進行結構化。有一些程式碼,讓你在未來的擴展或功能添加時陷入困境。有的程式碼性能較差,不能處理不同的瀏覽器。所有這些都會成為專家二提出建議的因素。

專家二還考慮了命名的影響。但說實話,即使是他們也不能在大多數時候把這一點做對。

結語

專家並不是通過使用每一個規範來證明自己;他們是通過對規範的充分了解來證明自己,從而明智地使用恰當的語法並做出合理的決定。這就是專家如何成為倍增器–這也是他們會造就新的專家的原因。

那麼,這對我們這些自認為是專家或有志於成為專家的人來說意味著什麼呢?這意味著編寫程式碼需要問自己很多問題。它意味著要以一種真實的方式考慮你的開發者受眾。你能寫出的最好的程式碼是完成一些複雜的業務,但本質上是那些檢查你的程式碼庫的人所能理解的程式碼。

不,這並不容易。而且往往沒有一個明確的答案。但這是你在寫每個函數時都應該考慮的問題。

英文原文://alistapart.com/article/human-readable-javascript