問題種種
做底層接口兼容,無非就是利用if,判斷客戶端支持哪個接口的問題。最著名的例子就是事件:
復制代碼 代碼如下:
var addEvent = function(e, what, how) {
if (e.addEventListener) e.addEventListener(what, how, false)
else if (e.attachEvent) e.attachEvent('on' + what, how)
}
這裡考慮了給元素綁定事件時可能遇到的兩種狀況——標准的W3C DOM接口以及DHTML提供的接口。當然這個例子還很粗糙,但足夠說明問題了。
原先的方法是在兼容層調用有現場判斷並進入相應的if分支。很顯然,這種“現場判斷”的方法效率並不高。後來,人們采用這樣的辦法:
復制代碼 代碼如下:
if (MSIE) {
addEvent = function(e, what, how) {
e.attachEvent('on' + what, how);
}
} else {
addEvent = function(e, what, how) {
e.addEventListener(what, how);
}
}
在一次判斷後給addEvent綁定不同的代碼,從而免去了運行時的分支判斷。
很可惜,這個問題也不小。首先把“采用attachEvent”和“客戶端是MSIE”綁定在一起是個很過時的想法。假如微軟哪天良心發現了怎麼辦?這事情現在就發生了——IE9明確支持了DOM接口,甚至DOM3都支持。結果,就這個“良心發現”的舉動會毀掉許多前端庫,他們必須被迫修改代碼(如同IE8來時那樣)。況且這種做法沒有考慮“未知的客戶端”——據我所知,Google發布Chrome後也導致不少類庫重寫代碼。
特性檢測
那究竟該怎麼做?特性檢測就可以最大限度地避免“新客戶端”帶來的麻煩——通過一組在類庫初始化時定義的代碼來檢測客戶端擁有的特性,並利用這一組檢測值綁定類庫代碼:
復制代碼 代碼如下:
var supportsAddEventListener = !!(checkerElement.addEventListener);
if (supportsAddEventListener) {
addEvent = function(e, what, how) {
e.addEventListener(what, how);
}
} else if (supportsAttachEvent) {
addEvent = function(e, what, how) {
e.attachEvent('on' + what, how);
}
}
特性檢測實際上是將“使用某個客戶端”和“支持某個特性”進行解耦——讓if分支直接針對“特性有無”(接口是否一致)判斷,從而消除客戶端制造商“良心發現”造成的“好心辦壞事”。事實上這麼做也是符合歷史潮流之選——當標准接口逐漸普及,客戶端之間漸漸“表征一致”時,為什麼不做個一致的兼容層接口呢?
跌落
讓我們重新看看這些代碼。通常,一條利用特性檢測進行兼容的代碼往往是這樣:
復制代碼 代碼如下:
if (new_interface_detected) {
comp = function() {uses_new_interface};
} else if (old_interface_detected) {
comp = function() {uses_old_interface};
} else {
throw new Error('Unadaptable!')
}
換言之,過程是:
如果客戶端支持新接口,就將兼容層綁定到新接口上
否則,如果客戶端支持老接口/不一致接口,就將兼容層綁定到老接口上
否則,如果可以的話,給出錯誤回饋
亦即,兼容層程序是從高空“掉”下來,如果客戶端支持“高級”特性(新接口、標准接口)就將它“接住”——兼容層就有了歸宿;否則繼續向下掉——哦,老接口接住了,就用老接口;如果一直沒人接住,於是——啪——摔倒了地上,並且用最後一口氣喊一聲:“你用的客戶端太小眾,我拿你沒辦法了!”
這和什麼比較像?
事實上,如果你了解JavaScript對象系統的機理,你就可以類比:這不就是原型嘛!原型系統就是利用了這種跌落——尋找某個成員,如果它在這個對象裡定義了,就返回之;否則沿著原型鏈向上搜(沒錯,這次是向上的),如此重復,直到真的連原型鏈都到頭的時候,返回個undefined。
說做就做!這裡同樣用addEvent為例。首先,我們定義一個空驅動,它裡面什麼都不包含:
var nullDriver = {}
然後,就是創建個對象,並且把原型鏈指向它。在ECMA V5時代,我們可以用Object.create,可惜,現在還有N多老客戶端(否則做什麼兼容啊),所以自己craft個函數:
復制代碼 代碼如下:
var derive = Object.create ? Object.create: function() {
var T = function() {};
return function(obj) {
T.prototype = obj;
return new T
}
}()
這個用法你可能會覺得很詭異,但它工作起來一點問題沒有,速度也不慢——能達到Object.create的一半。我們就用這個derive開動:
復制代碼 代碼如下:
var dhtmlDriver = derive(nullDriver);
var dhtmlDriverBugfix = derive(dhtmlDriver);
這裡的bugfix是針對一些“bug”和特殊情況定義的特別Driver。這裡你可以忽略它。好了,DHTML裡面addEvent是什麼來著?
復制代碼 代碼如下:
if (supportsAttachEvent) {
dhtmlDriver.addEvent = function(e, what, how) {
e.attachEvent('on' + what, how)
}
}
然後呢?位於原型鏈最前端的應該是W3C的標准驅動啊,寫上!
復制代碼 代碼如下:
var w3cDriver = derive(dhtmlDriverBugfix);
var w3cDriverBugfix = derive(w3cDriver);
if (supportsAddEventListener) {
w3cDriver.addEvent = function(e, what, how) {
e.addEventListener(what, how)
}
}
最後,我們就放個東西上去做最後調用的接口。(因為w3cDriverBugfix太難看……)
var driver = derive(w3cDriverBugfix);
然後就調用好了。看,這就讓那些長得嚇人的分支判斷變得簡單有效,但不失fallback本色:在支持addEventListener上調用addEvent等價於調用w3cDriver.addEvent,而在不支持addEventListener的客戶端上就會跌落到底下,比如調用dhtmlDriver.addEvent。另外,進行bugfix也很容易——可以在專門的“bugfix”層進行hook,而原有層絲毫不受影響。
等等,繼承這麼多層
會很慢麼?誠然,那麼深的原型鏈肯定會慢,不過我有辦法。還記得給對象的屬性寫入時會發生什麼事情嗎?
復制代碼 代碼如下:
var ego = function(x) {return x}
for (var each in driver) {
if (! (each in nullDriver)) {
driver[each] = ego(driver[each])
}
}
沒錯,原來高企在原型鏈上面的方法會“嘩”的一下掉到最下面!這回不用沿著原型鏈向上搜了,直接從最底端獲取屬性即可。這裡用ego函數的原因是防止一些浏覽器“優化掉”這裡的代碼。
總結
雖然這裡談兼容,可是,它的精華卻在語言特性上——利用原型繼承,我們可以很優雅地完成這個令人頭疼的操作。是的,框架的美感不應該只在外表,其內部——即使是最最令人煩的內部——也同樣要優雅。
這裡的技術可以在dess中找到。
來自:typeof.net