通過多年的開發、教學和編寫不唐突的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 }; }(); 這樣維護者就知道去哪裏修改這些屬性,而不須要改動其餘代碼。