上一章我們介紹了 JavaScript 的基本內(nèi)容和 DOM 對象的各個方面,包括如何訪問 node 節(jié)點。本章我們將講解如何通過 DOM 操作元素并且討論瀏覽器事件模型。
上一章節(jié)我們提到了 DOM 節(jié)點集合或單個節(jié)點的訪問步驟,每個 DOM 節(jié)點都包括一個屬性集合,大多數(shù)的屬性都提供為相應的功能提供了抽象。例如,如果有一個帶有 ID 屬性 intro 的文本元素,你可以很容易地通過 DOM API 來改變該元素的顏色:
document.getElementById('intro').style.color = '#FF0000';
為了理解這個 API 的功能,我們一步一步分開來看就非常容易理解了:
var myDocument = document;
var myIntro = myDocument.getElementById('intro');
var myIntroStyles = myIntro.style;
// 現(xiàn)在,我們可以設(shè)置顏色了:
myIntroStyles.color = '#FF0000';
現(xiàn)在,我們有了該文本的 style 對象的引用了,所以我們可以添加其它的 CSS 樣式:
myIntroStyles.padding = '2px 3px 0 3px';
myIntroStyles.backgroundColor = '#FFF';
myIntroStyles.marginTop = '20px';
這里我們只是要了基本的 CSS 屬性名稱,唯一區(qū)別是 CSS 屬性的名稱如果帶有-的話,就需要去除,比如用 marginTop 代替 margin-top。例如,下面的代碼是不工作的,并且會拋出語法錯誤:
myIntroStyles.padding-top = '10em';
// 產(chǎn)生語法錯誤:
// 在JavaScript里橫線-是減法操作符
// 而且也沒有這樣的屬性名稱
屬性可以像數(shù)組一樣訪問,所以利用這個知識我們可以創(chuàng)建一個函數(shù)來改變?nèi)魏谓o定元素的樣式:
function changeStyle(elem, property, val) {
elem.style[property] = val; // 使用[]來訪問屬性
}
// 使用上述的函數(shù):
var myIntro = document.getElementById('intro'); // 獲取intro文本對象
changeStyle(myIntro, 'color', 'red');
這僅僅是個例子,所以該函數(shù)也許沒什么用,語法上來說,直接用還是會快點,例如(elem.style.color = ‘red’)。除了 style 屬性以外,一個節(jié)點(或元素)也還有其他很多屬性可以操作,如果你使用 Firebug,點擊 DOM 選項卡可以看到所有該節(jié)點(或元素)的所有屬性:
http://wiki.jikexueyuan.com/project/javascript-depth-understanding/images/15.png" alt="" />
所有的屬性都可以通過點標示符來訪問(例如:Element.tabIndex)。不是所有的屬性都是原始數(shù)據(jù)類型(strings,numbers,Booleans 等等),sytle 屬性也是一個包含自己屬性的對象,很多元素的屬性都是只讀的,也就是說不能修改他們的值。例如,你不能直接修改一個節(jié)點的 parentNode 屬性,如果你修改只讀屬性的時候瀏覽器會拋出錯誤:例如,拋出錯誤“setting a property that has only a getter”,只是我們需要注意的。
通常 DOM 操作都是改變原始的內(nèi)容,這里有幾種方式來實現(xiàn)這個,最簡單的是使用 innerHTML 屬性,例如:
var myIntro = document.getElementById('intro');
// 替換當前的內(nèi)容
myIntro.innerHTML = 'New content for the <strong>amazing</strong> paragraph!';
// 添加內(nèi)容到當前的內(nèi)容里
myIntro.innerHTML += '... some more content...';
唯一的問題是該方法沒在規(guī)范里定義,而且在 DOM 規(guī)范里也沒有定義,如果你不反感的話請繼續(xù)使用,因為它比我們下面要討論其它的方法快多了。
通過 DOM API 創(chuàng)建內(nèi)容的時候需要注意 node 節(jié)點的 2 種類型,一種是元素節(jié)點,一種是 text 節(jié)點,上一章節(jié)已經(jīng)列出了所有的節(jié)點類型,這兩種需要我們現(xiàn)在特別注意。創(chuàng)建元素可以通過 createElement 方法,而創(chuàng)建 text 節(jié)點可以使用 createTextNode,相應代碼如下:
var myIntro = document.getElementById('intro');
// 添加內(nèi)容
var someText = 'This is the text I want to add';
var textNode = document.createTextNode(someText);
myIntro.appendChild(textNode);
這里我們使用了 appendChild 方法將新 text 節(jié)點附件到文本字段,這樣做比非標準的 innerHTML 方法顯得有點長,但了解這些原理依然很重要,這里有一個使用 DOM 方法的更詳細例子:
var myIntro = document.getElementById('intro');
// 添加新連接到文本節(jié)點
// 首先,創(chuàng)建新連接元素
var myNewLink = document.createElement('a'); // <a/>
myNewLink.; // <a />
myNewLink.appendChild(document.createTextNode('Visit Google'));
// <a >Visit Google</a>
// 將內(nèi)容附件到文本節(jié)點
myIntro.appendChild(myNewLink);
另外 DOM 里還有一個 insertBefore 方法用于再節(jié)點前面附件內(nèi)容,通過 insertBefore 和 appendChild 我們可以實現(xiàn)自己的 insertAfter 函數(shù):
// 'Target'是DOM里已經(jīng)存在的元素
// 'Bullet'是要插入的新元素
function insertAfter(target, bullet) {
target.nextSibling ?
target.parentNode.insertBefore(bullet, target.nextSibling)
: target.parentNode.appendChild(bullet);
}
// 使用了3目表達式:
// 格式:條件?條件為true時的表達式:條件為false時的表達式
上面的函數(shù)首先檢查 target 元素的同級下一個節(jié)點是否存在,如果存在就在該節(jié)點前面添加 bullet 節(jié)點,如果不存在,就說明 target 是最后一個節(jié)點了,直接在后面 append 新節(jié)點就可以了。DOM API 沒有給提供 insertAfter 是因為真的沒必要了——我們可以自己創(chuàng)建。
DOM 操作有很多內(nèi)容,上面你看到的只是其中一部分。
瀏覽器事件是所有 web 程序的核心,通過這些事件我們定義將要發(fā)生的行為,如果在頁面里有個按鈕,那點擊此按鈕之前你需要驗證表單是否合法,這時候就可以使用 click 事件,下面列出的最標準的事件列表:
注:正如我們上章所說的,DOM 和 JavaScript 語言是 2 個單獨的東西,瀏覽器事件是 DOM API 的一部分,而不是 JavaScript 的一部分。
還有很多各種各樣的事件,上面展示的事件是我們在 JavaScript 里最常用的事件,有些事件在跨瀏覽器方面可能有所不同。還有其它瀏覽器實現(xiàn)的一些屬性事件,例如 Gecko 實現(xiàn)的 DOMContentLoaded 或 DOMMouseScroll 等,Gecko 的詳細事件列表請查看這里。
我們將了事件,但是還沒有將到如何將處理函數(shù)和事件管理起來,使用這些事件之前,你首先要注冊這些事件句柄,然后描述該事件發(fā)生的時候該如何處理,下面的例子展示了一個基本的事件注冊模型:
基本事件注冊:
<!-- HTML -->
<button id="my-button">Click me!</button>
// JavaScript:
var myElement = document.getElementById('my-button');
// 事件處理句柄:
function buttonClick() {
alert('You just clicked the button!');
}
// 注冊事件
myElement.onclick = buttonClick;
使用 document.getElementById 命令,通過 ID=my-button 獲取該 button 對象,然后創(chuàng)建一個處理函數(shù),隨后將該函數(shù)賦值給該 DOM 的 onclick 屬性。就這么簡單!
基本事件注冊是非常簡單的,在事件名稱前面添加前綴 on 作為 DOM 的屬性就可以使用了,這是事件處理的基本核心,但下面的代碼我不推薦使用:
<button onclick="return buttonClick()">Click me!</button>
上述 Inline 的事件處理方式不利用頁面維護,建議將這些處理函數(shù)都封裝在單獨的 js 文件,原因和CSS樣式的一樣的。
高級事件注冊:
別被標題迷惑了,“高級”不意味著好用,實際上上面討論的基本事件注冊是我們大部分時候用的方式,但有一個限制:不能綁定多個處理函數(shù)到一個事件上。這也是我們要講解該小節(jié)原因:
該模型運行你綁定多個處理句柄到一個事件上,也就是說一個事件觸發(fā)的時候多個函數(shù)都可以執(zhí)行,另外,該模型也可以讓你很容易里刪除某個已經(jīng)綁定的句柄。
嚴格來說,有 2 種不同的模型:W3C 模型和微軟模型,除 IE 之外 W3C 模型支持所有的現(xiàn)代瀏覽器,而微軟模型只支持 IE,使用 W3C 模型的代碼如下:
// 格式:target.addEventListener( type, function, useCapture );
// 例子:
var myIntro = document.getElementById('intro');
myIntro.addEventListener('click', introClick, false);
使用 IE 模型的代碼如下:
// 格式: target.attachEvent ( 'on' + type, function );
// 例子:
var myIntro = document.getElementById('intro');
myIntro.attachEvent('onclick', introClick);
introClick 的代碼如下:
function introClick() {
alert('You clicked the paragraph!');
}
事實上,要做出通用的話,我們可以自定義一個函數(shù)以支持跨瀏覽器:
function addEvent(elem, type, fn) {
if (elem.attachEvent) {
elem.attachEvent('on' + type, fn);
return;
}
if (elem.addEventListener) {
elem.addEventListener(type, fn, false);
}
}
該函數(shù)首先檢查 attachEvent 和 addEventListener 屬性,誰可以就用誰,這兩種類型的模型都支持刪除句柄功能,參考下面的 removeEvent 函數(shù)。
function removeEvent(elem, type, fn) {
if (elem.detachEvent) {
elem.detachEvent('on' + type, fn);
return;
}
if (elem.removeEventListener) {
elem.removeEventListener(type, fn, false);
}
}
你可以這樣使用:
var myIntro = document.getElementById('intro');
addEvent(myIntro, 'click', function () {
alert('YOU CLICKED ME!!!');
});
注意到我們傳入了一個匿名函數(shù)作為第三個參數(shù),JavaScript 運行我們定義和執(zhí)行匿名函數(shù),這種匿名函數(shù)特別適合作為參數(shù)傳遞,實際上我們也可以傳遞有名的函數(shù)(代碼如下),但是你們函數(shù)更容易做。
如果你只想在第一次 click 的時候觸發(fā)一個函數(shù),你可以這么做:
// 注意:前提是我們已經(jīng)定于好了addEvent/removeEvent函數(shù)
// (定義好了才能使用哦)
var myIntro = document.getElementById('intro');
addEvent(myIntro, 'click', oneClickOnly);
function oneClickOnly() {
alert('WOW!');
removeEvent(myIntro, 'click', oneClickOnly);
}
當?shù)谝淮斡|發(fā)以后,我們就立即刪除該句柄,但是有匿名函數(shù)的話卻很難將自身的引用刪除,不過實際上可以通過如下的形式來做(只不過有點麻煩):
addEvent(myIntro, 'click', function () {
alert('WOW!');
removeEvent(myIntro, 'click', arguments.callee);
});
這里我們是有了 arguments 對象的 callee 屬性,arguments 對象包含了所有傳遞進來的參數(shù)以及該函數(shù)自身(callee),這樣我們就可以放心地刪除自身的引用了。
關(guān)于 W3C 和微軟模型還有其他的少許差異,比如 this,在觸發(fā)事件的時候函數(shù)中的 this 一般都是該元素上下文,,也就說 this 引用該元素自身,在基本事件注冊和 W3C 模型中都沒有問題,但在微軟模型的實現(xiàn)里卻可能出錯,請參考如下代碼:
function myEventHandler() {
this.style.display = 'none';
}
// 正常工作,this是代表該元素
myIntro.onclick = myEventHandler;
// 正常工作,this是代表該元素
myIntro.addEventListener('click', myEventHandler, false);
// 不正常,這時候的this是代表Window對象
myIntro.attachEvent('onclick', myEventHandler);
這里有一些方式可以避免這個問題,最簡單的方式是使用前面的基本事件注冊方式,或者是再做一個通用的 addEvent,通用代碼請參考 John Resig 或 Dean Edward 的文章。
另外一個非常重要的內(nèi)容是 Event 對象,當事件發(fā)生的時候出發(fā)某個函數(shù),該 Event 對象將自動在函數(shù)內(nèi)可用,該對象包含了很多事件觸發(fā)時候的信息,但IE卻沒有這么實現(xiàn),而是自己實現(xiàn)的,IE 瀏覽器是通過全局對象 window 下的 event 屬性來包含這些信息,雖然不是大問題,但我們也需要注意一下,下面的代碼是兼容性的:
function myEventHandler(e) {
// 注意參數(shù)e
// 該函數(shù)調(diào)用的時候e是event對象(W3C實現(xiàn))
// 兼容IE的代碼
e = e || window.event;
// 現(xiàn)在e就可以兼容各種瀏覽器了
}
// 這里可以自由地綁定事件了
這里判斷 e 對象(Event 對象)是否存在我們使用了 OR 操作符:如果 e 不存在(為 null,undefined,0 等)的時候,將 window.event 賦值給 e,否則的話繼續(xù)使用 e。通過這方式很快就能在多瀏覽器里得到真正的 Event 對象,如果你不喜歡這種方式的話,你可以使用 if 語句來處理:
if (!e) {
e = window.event;
} // 沒有else語句,因為e在其它瀏覽器已經(jīng)定義了
另外 Event 對象下的命令和屬性都很有用,遺憾的是不不能全兼容瀏覽器,例如當你想取消默認的行為的時候你可以使用 Event 對象里的 preventDefault()方法,但 IE 里不得不使用對象的 returnValue 屬性值來控制,兼容代碼如下:
function myEventHandler(e) {
e = e || window.event;
// 防止默認行為
if (e.preventDefault) {
e.preventDefault();
} else {
e.returnValue = false;
}
}
例如,當你點擊一個連接的時候,默認行為是導航到 href 里定義的地址,但有時候你想禁用這個默認行為,通過 returnValue 和 preventDefault 就可以實現(xiàn),Event 對象里的很多屬性在瀏覽器里都不兼容,所以很多時候需要處理這些兼容性代碼。
注意:現(xiàn)在很多 JS 類庫都已經(jīng)封裝好了 e.preventDefault 代碼,也就是說在 IE 里可用了,但是原理上依然是使用 returnValue 來實現(xiàn)的。
事件冒泡,就是事件觸發(fā)的時候通過 DOM 向上冒泡,首先要知道不是所有的事件都有冒泡。事件在一個目標元素上觸發(fā)的時候,該事件將觸發(fā)一一觸發(fā)祖先節(jié)點元素,直到最頂層的元素:
http://wiki.jikexueyuan.com/project/javascript-depth-understanding/images/16.png" alt="" />
如圖所示,如果 a 連接被點擊,觸發(fā)觸發(fā)連接的 click 事件,然后觸發(fā) p 的 click 事件,以此再觸發(fā) div 和 body 的 click 事件。順序不變,而且不一定是在同時觸發(fā)的。
這樣你就可以利用該特性去處理自己的邏輯了,并且再任何時候都可以停止冒泡,比如,如果你只想冒泡到文本節(jié)點上,而不再進一步冒泡,你可以在 p 的 click 事件處理函數(shù)里丁停止冒泡:
function myParagraphEventHandler(e) {
e = e || window.event;
// 停止向上冒泡
if (e.stopPropagation) {
// W3C實現(xiàn)
e.stopPropagation();
} else {
// IE實現(xiàn)
e.cancelBubble = true;
}
}
// 使用我們自定義的addEvent函數(shù)將myParagraphEventHandler綁定到click事件上:
addEvent(document.getElementsByTagName('p')[0], 'click', myParagraphEventHandler);
舉例來說,如果你有一個很多行的大表格,在每個 <tr> 上綁定點擊事件是個非常危險的想法,因為性能是個大問題。流行的做法是使用事件委托。事件委托描述的是將事件綁定在容器元素上,然后通過判斷點擊的 target 子元素的類型來觸發(fā)相應的事件。
var myTable = document.getElementById('my-table');
myTable.onclick = function () {
// 處理瀏覽器兼容
e = e || window.event;
var targetNode = e.target || e.srcElement;
// 測試如果點擊的是TR就觸發(fā)
if (targetNode.nodeName.toLowerCase() === 'tr') {
alert('You clicked a table row!');
}
}
事件委托依賴于事件冒泡,如果事件冒泡到 table 之前被禁用的話,那上面的代碼就無法工作了。
本章我們覆蓋到了 DOM 元素的操作以及相關(guān)的瀏覽器事件模型,希望大家能對 DOM 有了進一步的了解。