如果我們認為模式代表一個最佳的實踐,那麼反模式將代表我們已經學到一個教訓。受啟發於Gof的《設計模式》,Andrew Koeing在1995年的11月的C++報告大會上首次提出反模式。在Koeing的報告中,反模式有著兩種觀念:
描述對於一個特殊的問題,提出了一個糟糕的解決方案,最終導致一個壞結果發生
描述如何擺脫上述解決方案並能提出一個好的解決方案
在如今這個前端發展如火如荼的時代,談及jq總是顯得非常的low,但實際上,在學校,在很多前端新人以及所謂“頁面仔 || 切圖工”之類的同行之間,jq的活力還是遠超各種框架的時候,之所以想寫這樣一篇文章,一是因為見到了身邊的jq爛程式碼,二是因為我在百度jQuery反模式的時候居然什麼有價值的相關結果都沒有,所以覺得還是有必要聊聊的。
先從些簡單的開始:
插入DOM節點:
// 反模式
$.each(reallyLongArray, function(count, item) {
var newLi = '<li>' + item + '</li>';
$('#ballers').append(newLi)
})
// 更好的實踐
var frag = document.createDocumentFragment()
$.each(reallyLongArray, function(count, item) {
var newLi = '<li>' + item + '</li>';
frag.appendChild(newLi[0])
})
$('#ballers')[0].appendChild(frag)
// 你也可以用字串
var myHTML = ''
$.each(reallyLongArray, function(count, item) {
myHTML += '<li>' + item + '</li>';
})
$('#ballers').html(myHTML)
DocumentFragment是瀏覽器為了減少DOM操作中的更新所使用的API,詳情請查閱MDN相關文件。
遵循DRY原則:
if ($a.data('current') != 'showing') {
$a.stop()
}
if ($b.data('current') != 'showing') {
$b.stop()
}
if ($c.data('current') != 'showing') {
$c.stop()
}
// 用陣列來儲存不同的主體
var elems = [$a, $b, $c]
$.each(elems, function(k, v) {
if (v.data('current') != 'showing') {
v.stop()
}
})
用陣列或物件來儲存重複片段的差異引數是一種很常見的方法。更多內容可以參考常見的JavaScript設計模式中的“九、策略模式”
地獄式回撥(callback hell):
$(document).ready(function() {
$('#button').click(function() {
$.get('http://api.github.com/repos/facebook/react/forks', function(data) {
alert(data[0].owner.login)
})
})
})
// 以前有這麼一種優化的方法,使用物件字面量儲存回撥使其扁平化
var cbContainer = {
initApp: function() {
$(document).ready(cbContainer.readCb)
},
readyCb: function() {
$('#button').click(cbContainer.clickCb)
},
clickCb: function() {
$.get('http://api.github.com/repos/facebook/react/forks', function(data) {
cbContainer.getCb(data)
})
},
getCb: function(data) {
alert(data[0].owner.login)
}
}
cbContainer.initApp()
// 不過現在流行Promise
var initApp = function() {
return new Promise(function(resolve, reject) {
$(document).ready(resolve)
})
}
var readyCb = function() {
return new Promise(function(resolve, reject) {
$('#button').click(resolve)
})
}
var clickCb = function() {
return new Promise(function(resolve, reject) {
$.get('http://api.github.com/repos/facebook/react/forks', function(data) {
resolve(data)
})
})
}
var getCb = function(data) {
alert(data[0].owner.login)
}
initApp()
.then(readyCb)
.then(clickCb)
.then(getCb)
用物件將回撥扁平還好,Promise是什麼鬼。不是比回撥還噁心,好吧,示例確實是這樣。其實之所以用Promise除了將回撥轉成鏈式呼叫以外,主要還是為了用它的reject函式獲取回撥中的錯誤。像示例這種一路resolve的,沒必要這麼用。這裡只是提一句。
如果希望瞭解更多關於回撥相關的知識,可以看看Promise, generator, async與ES6這篇文章。
重複查詢:
$(document.body).append('<div class="baaron"></div>')
$('.baaron').click(function() {})
// 更好的方式
$('<div class="baaron"></div>')
.appendTo(document.body)
.click(function() {})
選擇器:
對於jq的選擇器還有許多要注意的問題,因為jq的選擇器是從右向左查詢,所以請記住一個“左輕右重”的原則:
// 請看下面兩個選擇器
$('div.foo .bar')
$('.foo span.bar') //右邊更明確一點,會好不少
// 當左邊確實要比右邊明確的時候這麼幹
$('#foo .bar')
$('#foo').find('.bar')
// 尤其避免使用萬用字元
$('#foo > *')
$('#foo').children()
// 有些萬用字元是隱式的
$('.foo :radio')
$('.foo *:radio') //和上邊一樣的
$('.foo input:radio') //改成這樣
聊聊依賴:
接下來,讓我們從優化一段jq程式碼開始,聊聊js中的依賴
$('#button').click(function() {
$.get('http://xxxx', function(data) {
$('#page').html(data.abc)
})
})
這段程式碼有以下問題:
click事件繫結的匿名函式難以重複利用,也很難測試
click回撥的匿名函式中的$是全域性變數,ajax請求回撥的匿名函式中的$('#page')也是用到了$這一全域性變數,全域性變數應該是要避免的
回撥的問題前面也說過了,這裡的回撥還很清楚不至於說到地獄的程度
現在我們把程式碼這樣改寫:
var downJSON = function() {
$.get('http://xxxx', function(data) {
$('#page').html(data.abc)
})
}
$('#button').click(downJSON)
現在匿名函式被我們拿出來了,可以重用了,但還是難以測試,且涵蓋全域性變數。
繼續:
var downJSON = function($, $el) {
$.get('http://xxxx', function(data) {
$el.html(data.abc)
})
}
$('#button').click(function() {
downJSON($, $('#page'))
})
這樣改寫以後,沒有了全域性變數,函式已經獨立出去。換一種說法就是,我們去除了函式中的隱式依賴(前面例子中的函式要執行需要全域性變數$,但沒有從函式宣告中表現出來,我們稱其為隱式依賴),現在,函式執行所需要的依賴被顯示宣告,使其具有更好的可控性。前端的依賴管理如今是一個很流行的話題,不過在這裡就不廢話了。
奇技淫巧:
最後,對於幾種比較常見的寫法,我們也可以使用一些奇技淫巧,或能使程式碼更短,或能使程式碼更為易讀:
簡化條件語句:
// 常見的寫法
if(!data) {
data = {}
}
// 簡化
data = data || {}
你可能覺得這不值一提,但可能有些時候你寫著寫著就忽視了。比如,js陣列去重的4個方法中的第二個方法,就可以應用這個技巧:
// 原來的程式碼
Array.prototype.unique2 = function()
{
var hashTable = {},res=[]; //n為hash表,r為臨時陣列
for(var i = 0; i < this.length; i++) { //遍歷當前陣列
if (!hashTable[this[i]]) { //如果hash表中沒有當前項
res.push(this[i]); //把當前陣列的當前項push到臨時陣列裡面
hashTable[this[i]] = true; //存入hash表
}
}
return res;
}
// 應用此技巧
Array.prototype.unique2 = function()
{
var hashTable = {}, res = []
for(var i = 0; i < this.length; i++) {
!hashTable[this[i]] ? res.push(this[i]) : null
hashTable[this[i]] = hashTable[this[i]] || true
}
return res
}
寫成這樣也未必說是優化,目測判斷邏輯還多了一個哈哈,但是巢狀少了一層,怎麼說呢,自行決定吧。
下面展示的一個技巧和上面這個也差不多:
// 正常寫法
if(type === 'foo' || type === 'bar') {}
// 用物件有三種方法
// 方法1
if(({foo: 1, bar: 1})[type]) {} // type === 'toString'可以繞過驗證
// 方法2
if(type in ({foo: 1, bar: 1})) {} // 和上一種等價但是慢點
// 方法3
if(({foo: 1, bar: 1}).hasOwnProperty(type)) {} // 最嚴密,也最慢
// 用正則
if(/^(foo|bar)$/.test(type)) {}
這種技巧的話,使用與否同樣是自己決定。很多有意思的東西,都是自己想著玩的,有些情況,我們倒不如好好寫成switch - case,都看的懂,也挺清晰。
總結兩句,雖說jQuery庫和新式的框架相比老了,但我覺得它在DOM操作上真的做到了一個極致。我相信很長一段時間,前端開發人員入門,還是要從它開始的。個人認為jq不適應時代的原因,是因為它本身也僅僅限於DOM操作,沒有其他限制,以至於當應用複雜時,你完全控制不住你的頁面。當你用上流行的框架,按照他們的Best Practice去組織程式碼,我想你剛剛開始的時候,一定會懷念jQuery這個溺愛你的老朋友的。
參考連結:
反模式 - 學用 JavaScript 設計模式 - 極客學院
jQuery Anti-Patterns for Performance & Compression
Patterns of Large-Scale JavaScript Applications