好程式設計師web前端分享JavaScript中常見的反模式

好程式設計師IT發表於2019-08-05

好程式設計師web前端分享JavaScript中常見的反模式,前言:反模式 是指對反覆出現的設計問題的常見的無力而低效的設計模式,俗話說就是重蹈覆轍。 這篇文章描述了 JavaScript 中常見的一些反模式,以及避免它們的辦法。

硬編碼

硬編碼(Hard-Coding)的字串、數字、日期…… 所有能寫死的東西都會被人寫死。 這是一個婦孺皆知的反模式,同時也是最廣泛使用的反模式。 硬編碼中最為典型的大概是 平臺相關程式碼(Platform-Related), 這是指特定的機器或環境下才可以正常執行的程式碼, 可能是隻在你的機器上可以執行,也可能是隻在 Windows 下可以執行。

例如在 npm script 中寫死指令碼路徑 /Users/harttle/bin/fis3, 原因可能是安裝一次非常困難,可能是為了避免重複安裝,也可能僅僅是因為這樣好使。 不管怎樣,這會讓所有同事來找你問“為什麼我這裡會報錯”。 解決辦法就是把它放到依賴管理,如果有特定的版本要求可以使用 package-lock,如果實在搞不定可以視為外部依賴可以放到本地配置檔案並從版本控制(比如 Git) 移除。

例如在 cli 工具中寫死特殊資料夾 /tmp, ~/.cache,或者路徑分隔符 \\ 或 /。 這類字串一般可以透過 Node.js 內建模組(或其他執行時 API)來得到, 比如使用 os.homedir, os.tmpdir, path.sep 等。

重複程式碼

重複程式碼(Duplication)在業務程式碼中尤為常見,初衷幾乎都是維護業務的穩定性。 舉個例子:在頁面 A 中需要一個漂亮的搜尋框,而頁面 B 中恰好有一個。 這時程式設計師小哥面臨一個艱難的選擇(如果直接複製還會有些許感到不安的話):

  • 把 B 複製一份,改成 A 想要的樣子。
  • 把 B 中的搜尋框重構到 C,B 和 A 引用這份程式碼。

由於時間緊迫希望早點下班,或者由於改壞 B 需要承擔責任 (PM:讓你做 A 為啥 B 壞了?回答這個問題比較複雜,這裡先跳過), 經過一番思考後決定採取方案 2。

至此整個故事進行地很自然也很順利,這大概就是重複程式碼被廣泛使用的原因。 這個故事中有幾點需要質疑:

  • B 這麼容易被改壞,說明 B 的作者 並未考慮複用。這時不應複用 B 的程式碼,除非決定接手維護它。
  • B 改壞的責任不止程式設計師小哥:B 的作者是否有 編寫測試,測試人員是否 迴歸測試 B 頁面?
  • 時間緊迫不必然導致反模式的出現,不可作為說服自己的原因。短期方案也存在優雅實現。

解決辦法就是:抽取 B 的程式碼重新開發形成搜尋框元件 C,在 A 頁面使用它。 同時提供給日後的小夥伴使用,包括敦促 B 的作者也遷移到 C 統一維護。

假 AMD

模組化本意是指把軟體的各功能分離到獨立的模組中,每個模組包含完整的一個細分功能。 在 JavaScript 中則是特指把指令碼切分為獨立上下文的,可複用的程式碼單元。

由於 JavaScript 最初作為頁面指令碼,存在很多引用全域性作用域的語法,以及不少基於全域性變數的實踐方式。 比如 jQuery 的 $, BOM 提供的 window,省略 var 來定義變數等。 AMD 是 JavaScript 社群較早的模組化規範。這是一個君子協定,問題就出在這裡。 有無數種方式寫出假的 AMD 模組:

  • 沒有返回值。對,要的就是副作用。
  • define 後直接 require。對,要的就是立即執行。
  • 產生副作用。修改 window 或其他共享變數,比如其他模組的靜態屬性。
  • 併發問題。依賴關係不明容易引發併發問題。

全域性副作用的影響完全等同於全域性變數,幾乎有全域性變數的所有缺點: 執行邏輯不容易理解;隱式的耦合關係;編寫測試困難。下面來一個具體的例子:

  // file: login.js

  define('login', function () {

  fetch('/account/login').then(x => {

  window.login = true

  })

  })

  require(['login'])

這個 AMD 模組與直接寫在一個 <script> 並無區別,準確地說是更不可控(requirejs 實現是非同步的)。 也無法被其他模組使用(比如要實現登出後再次登入),因為它沒返回任何介面。 此外這個模組存在併發問題(Race Condition):使用 window.login 判斷是否登入不靠譜。

解決辦法就是把它抽象為模組,由外部來控制它的執行並獲得登入結果。 在一個模組化良好的專案中,所有狀態最終由 APP 入口產生, 模組間共享的狀態都被抽取到最近的公共上級。

  define(function () {

  return fetch('/account/login')

  .then(() => true)

  .catch(e => {

  console.error(e)

  return false

  }

  })

註釋膨脹

註釋的初衷是讓讀者更好的理解程式碼意圖,但實踐中可能恰好相反。直接舉一個生活中的例子:

  // 判斷手機百度版本大於 15

  if (navigator.userAgent.match(/Chrome:(\d+))[1] < 15) {

  // ...

  }

哈哈當你讀到這一段時,相信上述註釋已經成功地消耗了你的時間。 如果你第一次看到這樣的註釋可能會不可思議,但真實的專案中多數註釋都是這個狀態。 因為維護程式碼不一定總是記得維護註釋,況且維護程式碼的通常不止一人。 C 語言課程的後遺症不止變數命名,“常寫註釋”也是一個很壞的教導。

解決辦法就是用清晰的邏輯來代替註釋,上述例子重新編寫後的程式碼如下:

  if (isHttpsSupported()) {

  // 透過函式抽取 + 命名,避免了新增註釋

  }

  function isHttpsSupported() {

  return navigator.userAgent.match(/Chrome:(\d+))[1] < 15

  }

函式體膨脹

“通常”認為函式體膨脹和全域性變數都是演算法課的後遺症。 但複雜的業務和演算法的場景確實不同,前者有更多的概念和操作需要解釋和整理。 整理業務邏輯最有效的手段莫過於變數命名和方法抽取(當然,還要有相應的閉包或物件)。

但在真實的業務維護中,保持理性並不容易。 當你幾十次進入同一個檔案新增業務邏輯後,你的函式一定會像懶婆娘的裹腳布一樣又臭又長:

  function submitForm() {

  var username = $('form input#username').val()

  if (username === 'harttle') {

  username = 'God'

  } else {

  username = 'Mortal'

  if ($('form input#words').val().indexOf('harttle')) {

  username = 'prophet'

  }

  }

  $('form input#username').val(username)

  $('form').submit()

  }

這只是用來示例,十幾行還遠遠沒有達到“又臭又長”的地步。 但已經可以看到各種目的的修改讓 submitForm() 的職責遠不止提交一個表單。 一個可能的重構方案是這樣的:

  function submitForm() {

  normalize()

  $('form').submit()

  }

  function normalize() {

  var username = parseUsername(

  $('form input#username').val(),

  $('form input#words').val()

  )

  $('form input#username').val(username)

  }

  function parseUsername(username, words)

  if (username === 'harttle') {

  return 'God'

  }

  return words.indexOf('harttle') ? 'prophet' : 'Mortal'

  }

在重構後的版本中,我們把原始輸入解析、資料歸一化等操作分離到了不同的函式, 這些抽離不僅讓 submitForm() 更容易理解,也讓進一步擴充套件業務更為方便。 比如在 normalize() 方法中對 input#password 欄位也進行檢查, 比如新增一個 parseWords() 方法對 input#words 欄位進行解析等等。

總結

常見的反模式還有許多,比如 == 和 != 的使用;擴充套件原生物件;還有 Promise 相關的 等等。


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69913892/viewspace-2652735/,如需轉載,請註明出處,否則將追究法律責任。

相關文章