英文原文:The seven rules of Unobtrusive JavaScript
原文網址: http://icant.co.uk/articles/seven-rules-of-unobtrusive-javascript/
原文作者:Chris Heilmann
譯文地址:http: //www.zhuoqun.net/html/y2008/1103.html
寫在前面:前一段時間kejun 給我們培訓JavaScript的時候,在幻燈片上推薦了很多特別經典的文章,其中就有這一篇。讀過之後感覺很不錯,不過我看文章往往理解不深入,恰好這篇文章我沒有發現中文版本,所以就萌生了把這個東西翻譯過來的想法,這樣既可以分享,又可以加深自己的理解。本文的作者Chris Heilmann是Yahoo! 英國的一位工程師(據kejun說是「教父」級的人物),本文的翻譯也是徵得了他本人的同意的。
這裡多說一句,以前我也翻譯過不少東西,不過那時候我更多是為了翻譯而翻譯,很多技術文章都沒領悟,所以到現在還是個。以後我還會繼續翻譯一些文章,不過應該只會翻譯那些需要仔細體會的經典文章。有時間還是要多寫程式碼,實踐才是王道。
術語的翻譯:關於「Unobtrusive JavaScript」一詞,我現在也沒想到一個特別貼切的譯法。在網路上搜了一下,發現有翻譯成「低調JavaScript」的,也有翻譯成「非侵入式JavaScript」的,台灣那邊有的翻譯成「不亂入JavaScript」…經過多方考證,我決定採用「不唐突的JavaScript」這種譯法(雖然這還是不太合我心意),具體請看這篇文章。其實「Unobtrusive JavaScript」包含了很多意思,也很難用一個字來概括,有興趣的可以看一下維基百科上面對「Unobtrusive JavaScript」的解釋。另外,我覺得翻譯就是要把作者的意思表達出來,而不一定非要逐字逐句翻譯,所以文章中我為了方便讀者理解,刪減了一些,增加了一些,不過這些都是在不傷害原文意思的基礎上進行的。
要說明的還有一點,那就是我翻譯程度很業餘,所以譯文中難免有紕漏,還請多多指正。
經過多年的開發、教學和編寫不唐突的JavaScript, 我發現了下面的一些準則。我希望它們可以幫助你對「為什麼這樣設計和執行JavaScript比較好」有一點理解。這些規則曾經幫助我更快地交付產品,並且產品的品質更高,也更容易維護。
1.不要做任何假設(JavaScript是一個不可靠的助手)
可能不唐突的JavaScript 的最重要的一個特性就是-你要停止任何假設:
不要假設JavaScript是可用的,你最好認為它很有可能是不可用的,而不是直接依賴它。
在你經過測試確認一些方法和屬性可以使用之前,不要假設瀏覽器支援它們。
不要假設HTML程式碼如你想像的那樣正確,每次都要檢查,並且當其不可用的時候就什麼也不要做。
讓JavaScript的功能獨立於輸入裝置要記住其他的腳本可能會影響你的JavaScript的功能,所以要確保你的腳本的作用域盡可能地安全。
在開始設計你的腳本之前,要考慮的第一件事就是檢查你要為其編寫腳本的HTML程式碼,看看有什麼東西可以幫助你達到目的。
2.找出鉤子和節點關係(HTML是腳本的基石)
在開始編寫腳本之前,先來看看你要為之編寫JavaScript的HTML。如果HTML是未經組織的或未知的,那麼你幾乎不可能有一個好的腳本編寫方案——很可能就會出現下面的情況:要么是會用JavaScript創建太多標記,要么就是應用太依賴於JavaScript。
在HTML中有一些東西需要考慮,那就是鉤子和節點關係。
<1>.HTML 鉤子
HTML最初的和最重要的鉤子就是ID,而且ID可以透過最快的DOM方法-getElementById 存取。如果在一個有效的HTML文件中所有的ID都是獨一無二的話(在IE中關於name 和ID 有一個bug,不過有些好的類別庫解決了這個問題),使用ID就是安全可靠的,並且易於測試。
其他一些鉤子就是是HTML元素和CSS類,HTML元素可以透過getElementsByTagName方法訪問,而在多數瀏覽器中都還不能透過原生的DOM方法來存取CSS類別。不過,有許多外部類別庫提供了可以存取CSS類別名稱(類似getElementsByClassName) 的方法。
<2>.HTML 節點關係
關於HTML的另外比較有趣的一點就是標記之間的關係,思考下面的問題:
要怎樣才可以最容易地、通過最少的DOM遍歷來到達目標節點?
透過修改什麼標記,可以盡可能存取需要修改的子節點?
一個給定的元素有什麼屬性或資訊可以用來到達另一個元素?
遍歷DOM很耗資源而且速度很慢,這就是為什麼要盡量使用瀏覽器中已經在使用的技術來做這件事情。
3.把遍歷交給專家來做(CSS,更快地遍歷DOM)
有關DOM的腳本和使用方法或屬性(getElementsByTagName, nextSibling, previousSibling, parentNode以及其它)來遍DOM似乎迷惑了很多人,這一點很有意思。而有趣的是,我們其實早已經透過另外一種技術—— CSS ——做了這些事情。
CSS 是這樣一種技術,它使用CSS選擇器,透過遍歷DOM來存取目標元素並改變它們的視覺屬性。一段複雜的使用DOM的JavaScript可以用一個CSS選擇器取代:
var n = document.getElementById('nav');
if(n){
var as = n.getElementsByTagName('a');
if(as.length > 0){
for(var i=0;as[i];i++){
as[i].style.color = '#369′;
as[i].style.textDecoration = 'none';
}
}
}
/* 下面的程式碼與上面功能一樣*/
#nav a{
color:#369;
text-decoration:none;
}
這是一個可以好好利用的很強大的技巧。你可以透過動態為DOM中高層的元素添加class 或更改元素ID來實現這一點。如果你使用DOM為文件的body新增了一個CSS類,那麼設計師就很可以輕鬆定義文件的靜態版本和動態版本。
JavaScript:
var dynamicClass = 'js';
var b = document.body;
b.className = b.className ? b.className + ' js' : 'js';
CSS:
/* 靜態版本*/
#nav {
....
}
/* 動態版本*/
body.js #nav {
....
}
4.理解瀏覽器和使用者(在既有的使用模式上創建你所需要的東西)
不唐突的JavaScript 中很重要的一部分就是理解瀏覽器是如何工作的(尤其是瀏覽器是如何崩潰的)以及用戶期望的是什麼。不考慮瀏覽器你也可以很容易地使用JavaScript創建一個完全不同的介面。拖曳介面,折疊區域,滾動條和滑動塊都可以使用JavaScript創建,但是這個問題並不是個簡單的技術問題,你需要思考下面的問題:
這個新介面可以獨立於輸入設備麼?如果不能,那麼可以依賴哪些東西?
我創建的這個新介面是否遵循了瀏覽器或者其它富界面的準則(你可以透過滑鼠在多層選單中直接切換嗎?還是需要使用tab鍵?)
我需要提供什麼功能但是這個功能是依賴JavaScript的?
最後一個問題其實不是問題,因為如果需要你就可以使用DOM來憑空建立HTML。關於這一點的一個例子就是「列印」鏈接,由於瀏覽器沒有提供一個非JavaScript的列印文件功能,所以你需要使用DOM來建立這類連結。同樣地,一個實現了展開和收縮內容模組的、可以點擊的標題列也屬於這種情況。標題列不能被鍵盤激活,但是連結可以。所以為了創建一個可以點擊的標題列你需要使用JavaScript將連結加入進去,然後所有使用鍵盤的使用者就可以收縮和展開內容模組了。
解決這類問題的極佳的資源就是設計模式庫。至於要知道瀏覽器中的哪些東西是獨立於輸入裝置的,那就要靠經驗的累積了。首先你要理解的就是事件處理機制。
5.理解事件(事件處理會造成改變)
事件處理是走向不唐突的JavaScript的第二步。重點不是讓所有的東西都變得可以拖曳、可以點擊或為它們添加內聯處理,而是理解事件處理是一個可以完全分離出來的東西。我們已經將HTML,CSS和JavaScript分開來,但是在事件處理的分離方面卻沒有走得很遠。
事件處理器會監聽發生在文件中元素上的變化,如果有事件發生,處理器就會找到一個很奇妙的物件(一般會是一個名為e的參數),這個物件會告訴元素發生了什麼以及可以用它做什麼。
對於大多數事件處理來說,真正有趣的是它不止發生在你想要訪問的元素上,還會在DOM中較高層級的所有元素上發生(但是並不是所有的事件都是這樣,focus和blur事件是例外)。舉例來說,利用這個特性你可以為一個導航清單只增加一個事件處理器,並且使用事件處理器的方法來取得真正觸發事件的元素。這種技術叫做事件委託,它有幾點好處:
你只需要檢查一個元素是否存在,而不需要檢查每個元素你可以動態地添加或刪除子節點而並不需要刪除相應的事件處理器你可以在不同的元素上對相同的事件做出回應需要記住的另一件事是,在事件傳播到父元素的時候你可以停止它而且你可以覆寫掉HTML元素(例如連結)的缺省行為。不過,有時候這並不是個好主意,因為瀏覽器賦予HTML元素那些行為是有原因的。舉個例子,連結可能會指向頁面內的某個目標,不去修改它們能確保使用者可以將頁面目前的腳本狀態也加入書籤。
6.為他人著想(命名空間,作用域和模式)
你的程式碼幾乎從來不會是文件中的唯一的腳本程式碼。所以保證你的程式碼裡沒有其它腳本可以覆蓋的全域函數或全域變數就顯得特別重要。有一些可用的模式可以來避免這個問題,最基礎的一點就是要使用var 關鍵字來初始化所有的變數。假設我們編寫了下面的腳本:
var nav = document.getElementById('nav');
function init(){
// do stuff
}
function show(){
// do stuff
}
function reset(){
// do stuff
}
上面的程式碼中包含了一個叫做nav的全域變數和名字分別為init,show 和reset 的三個函數。這些函數都可以存取nav這個變數並且可以透過函數名稱互相存取:
var nav = document.getElementById('nav');
function init(){
show();
if(nav.className === 'show'){
reset();
}
// do stuff
}
function show(){
var c = nav.className;
// do stuff
}
function reset(){
// do stuff
}
你可以將程式碼封裝到一個物件中來避免上面的那種全域式編碼,這樣就可以將函數變成物件中的方法,將全域變數變成物件中的屬性。 你需要使用「名字+冒號」的方式來定義方法和屬性,並且需要在每個屬性或方法後面加上逗號作為分割符。
var myScript = {
nav:document.getElementById('nav'),
init:function(){
// do stuff
},
show:function(){
// do stuff
},
reset:function(){
// do stuff
}
}
所有的方法和屬性都可以透過使用「類別名稱+點操作符」的方式從外部和內部存取。
var myScript = {
nav:document.getElementById('nav'),
init:function(){
myScript.show();
if(myScript.nav.className === 'show'){
myScript.reset();
}
// do stuff
},
show:function(){
var c = myScript.nav.className;
// do stuff
},
reset:function(){
// do stuff
}
}
這種模式的缺點就是,你每次從一個方法中存取其它方法或屬性都必須在前面加上物件的名字,而且物件中的所有東西都是可以從外部存取的。如果你只是想要部分程式碼可以被文件中的其他腳本訪問,可以考慮下面的模組(module)模式:
var myScript = function(){
//這些都是私有方法和屬性
var nav = document.getElementById('nav');
function init(){
// do stuff
}
function show(){
// do stuff
}
function reset(){
// do stuff
}
//公有的方法和屬性被使用物件語法包裝在return 語句裡面
return {
public:function(){
},
foo:'bar'
}
}();
你可以使用和前面的程式碼同樣的方式存取傳回的公有的屬性和方法,在本範例中可以這麼存取:myScript.public() 和myScript.foo 。但這裡還有一點讓人覺得不舒服:當你想要從外部或從內部的一個私有方法存取公有方法的時候,還是要寫一個冗長的名字(物件的名字可以非常長)。為了避免這一點,你需要將它們定義為私有的並且在return語句中只回傳一個別名:
var myScript = function(){
// 這些都是私有方法和屬性
var nav = document.getElementById('nav');
function init(){
// do stuff
}
function show(){
// do stuff
// do stuff
}
function reset(){
// do stuff
}
var foo = 'bar';
function public(){
}
//只回傳指向那些你想要存取的私有方法和屬性的指標
return {
public:public,
foo:foo
}
}();
這就保證了程式碼風格一致性,並且你可以使用短一點的別名來存取其中的方法或屬性。
如果你不想對外部暴露任何的方法或屬性,你可以將所有的程式碼封裝到一個匿名方法中,並在它的定義結束後立刻執行它:
(function(){
// these are all private methods and properties
var nav = document.getElementById('nav');
function init(){
// do stuff
show(); // 這裡不需要類別名稱前綴
}
function show(){
// do stuff
}
function reset(){
// do stuff
}
})();
對於那些只執行一次並且對其它函數沒有依賴的程式碼模組來說,這種模式非常好。
透過遵循上面的那些規則,你的程式碼更好地為使用者工作,也可以讓你的程式碼在機器上更好地運作並與其他開發者的程式碼和睦相處。不過,還有一個群體要考慮。
7.為接手的開發者考慮(使維護更加容易)
使你的腳本真正地unobtrusive的最後一步是在編寫完代碼之後仔細檢查一遍,並且要照顧到一旦腳本上線之後要接手你的代碼的開發者。考慮下面的問題:
所有的變數和函數名字是否合理且易於理解?
代碼是否經過了合理的組織?從頭到尾都很流暢嗎?
所有的依賴都顯而易見嗎?
在那些可能引起混淆的地方都添加了註釋嗎?
最重要的一點是:要認識到文件中的HTML和CSS程式碼相對於JavaScript來說更有可能被改變(因為它們負責視覺效果)。所以不要在腳本程式碼中包含任何可以讓終端使用者看到的class和ID,而是要將它們分離出來放到一個儲存設定資訊的物件中。
myscript = function(){
var config = {
navigationID:'nav',
visibleClass:'show'
};
var nav = document.getElementById(config.navigationID);
function init(){
show();
if(nav.className === config.visibleClass){
reset();
};
// do stuff
};
function show(){
var c = nav.className;
// do stuff
};
function reset(){
// do stuff
};
}();
這樣維護者就知道要去哪裡修改這些屬性,而不需要改變其他程式碼。
更多資訊
以上就是我發現的七條準則。如果你想了解更多與上面所探討的主題相關的東西,可以看看下面的連結: