每個人都想成為專家,但什麼才是專家呢?這些年來,我見過兩種被稱為“專家”的人。專家一是指對語言中的每一個工具都了如指掌的人,而且無論是否有幫助,都一定要用好每一點。專家二也知道每一個文法,但他們對采用什麼來解決問題比較挑剔,會考慮很多因素,包括與代碼有關的和無關的。
你能猜猜我們想讓哪位專家加入我們的團隊嗎?如果你說是專家二,那你猜對了。他們是專注于編寫可讀性好的 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)
而這隻是人為因素影響代碼選擇的一個例子。例如,一個由很多不同團隊接觸的代碼庫,可能必須持有更嚴格的标準,不一定能跟上最新最強的文法。然後,你站在源代碼以外的視角,考慮你的工具鍊中的其他因素,這些因素會讓在這些代碼上工作的人感到更輕松或者更困難。有一些代碼,可以以一種敵視測試的方式進行結構化。有一些代碼,讓你在未來的擴充或功能添加時陷入困境。有的代碼性能較差,不能處理不同的浏覽器。所有這些都會成為專家二提出建議的因素。
專家二還考慮了命名的影響。但說實話,即使是他們也不能在大多數時候把這一點做對。
結語
專家并不是通過使用每一個規範來證明自己;他們是通過對規範的充分了解來證明自己,進而明智地使用恰當的文法并做出合理的決定。這就是專家如何成為倍增器--這也是他們會造就新的專家的原因。
那麼,這對我們這些自認為是專家或有志于成為專家的人來說意味着什麼呢?這意味着編寫代碼需要問自己很多問題。它意味着要以一種真實的方式考慮你的開發者閱聽人。你能寫出的最好的代碼是完成一些複雜的業務,但本質上是那些檢查你的代碼庫的人所能了解的代碼。
不,這并不容易。而且往往沒有一個明确的答案。但這是你在寫每個函數時都應該考慮的問題。
英文原文:https://alistapart.com/article/human-readable-javascript