好程序員web前端分享JavaScript中常見的反模式,前言:反模式 是指對反覆出現的設計問題的常見的無力而低效的設計模式,俗話說就是重蹈覆轍。 這篇文章描述了 JavaScript 中常見的一些反模式,以及避免它們的辦法。前端
硬編碼程序員
硬編碼(Hard-Coding)的字符串、數字、日期…… 全部能寫死的東西都會被人寫死。 這是一個婦孺皆知的反模式,同時也是最普遍使用的反模式。 硬編碼中最爲典型的大概是 平臺相關代碼(Platform-Related), 這是指特定的機器或環境下才能夠正常運行的代碼, 多是隻在你的機器上能夠運行,也多是隻在 Windows 下能夠運行。web
例如在 npm script 中寫死腳本路徑 /Users/harttle/bin/fis3, 緣由多是安裝一次很是困難,多是爲了不重複安裝,也可能僅僅是由於這樣好使。 無論怎樣,這會讓全部同事來找你問「爲何我這裏會報錯」。 解決辦法就是把它放到依賴管理,若是有特定的版本要求可使用 package-lock,若是實在搞不定能夠視爲外部依賴能夠放到本地配置文件並從版本控制(好比 Git) 移除。算法
例如在 cli 工具中寫死特殊文件夾 /tmp, ~/.cache,或者路徑分隔符 \ 或 /。 這類字符串通常能夠經過 Node.js 內置模塊(或其餘運行時 API)來獲得, 好比使用 os.homedir, os.tmpdir, path.sep 等。npm
重複代碼設計模式
重複代碼(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 相關的 等等。