JavaScript與DOM(下)

2018-07-28 17:31 更新

介紹

上一章我們介紹了JavaScript的基本內(nèi)容和DOM對(duì)象的各個(gè)方面,包括如何訪問(wèn)node節(jié)點(diǎn)。本章我們將講解如何通過(guò)DOM操作元素并且討論瀏覽器事件模型。

本文參考:http://net.tutsplus.com/tutorials/javascript-ajax/javascript-and-the-dom-lesson-2/

操作元素

上一章節(jié)我們提到了DOM節(jié)點(diǎn)集合或單個(gè)節(jié)點(diǎn)的訪問(wèn)步驟,每個(gè)DOM節(jié)點(diǎn)都包括一個(gè)屬性集合,大多數(shù)的屬性都提供為相應(yīng)的功能提供了抽象。例如,如果有一個(gè)帶有ID屬性intro的文本元素,你可以很容易地通過(guò)DOM API來(lái)改變?cè)撛氐念伾?/p>

document.getElementById('intro').style.color = '#FF0000';

為了理解這個(gè)API的功能,我們一步一步分開(kāi)來(lái)看就非常容易理解了:

var myDocument = document;
var myIntro = myDocument.getElementById('intro');
var myIntroStyles = myIntro.style;

// 現(xiàn)在,我們可以設(shè)置顏色了:  
myIntroStyles.color = '#FF0000';

現(xiàn)在,我們有了該文本的style對(duì)象的引用了,所以我們可以添加其它的CSS樣式:

myIntroStyles.padding = '2px 3px 0 3px';
myIntroStyles.backgroundColor = '#FFF';
myIntroStyles.marginTop = '20px'; 

這里我們只是要了基本的CSS屬性名稱(chēng),唯一區(qū)別是CSS屬性的名稱(chēng)如果帶有-的話(huà),就需要去除,比如用marginTop代替margin-top。例如,下面的代碼是不工作的,并且會(huì)拋出語(yǔ)法錯(cuò)誤:

myIntroStyles.padding-top = '10em';

// 產(chǎn)生語(yǔ)法錯(cuò)誤:
// 在JavaScript里橫線(xiàn)-是減法操作符
// 而且也沒(méi)有這樣的屬性名稱(chēng)

屬性可以像數(shù)組一樣訪問(wèn),所以利用這個(gè)知識(shí)我們可以創(chuàng)建一個(gè)函數(shù)來(lái)改變?nèi)魏谓o定元素的樣式:

function changeStyle(elem, property, val) {
    elem.style[property] = val; // 使用[]來(lái)訪問(wèn)屬性
}

// 使用上述的函數(shù):  
var myIntro = document.getElementById('intro'); // 獲取intro文本對(duì)象
changeStyle(myIntro, 'color', 'red');  

這僅僅是個(gè)例子,所以該函數(shù)也許沒(méi)什么用,語(yǔ)法上來(lái)說(shuō),直接用還是會(huì)快點(diǎn),例如(elem.style.color = ‘red’)。除了style屬性以外,一個(gè)節(jié)點(diǎn)(或元素)也還有其他很多屬性可以操作,如果你使用Firebug,點(diǎn)擊DOM選項(xiàng)卡可以看到所有該節(jié)點(diǎn)(或元素)的所有屬性:

所有的屬性都可以通過(guò)點(diǎn)標(biāo)示符來(lái)訪問(wèn)(例如:Element.tabIndex)。不是所有的屬性都是原始數(shù)據(jù)類(lèi)型(strings, numbers, Booleans等等),sytle屬性也是一個(gè)包含自己屬性的對(duì)象,很多元素的屬性都是只讀的,也就是說(shuō)不能修改他們的值。例如,你不能直接修改一個(gè)節(jié)點(diǎn)的parentNode屬性,如果你修改只讀屬性的時(shí)候?yàn)g覽器會(huì)拋出錯(cuò)誤:例如,拋出錯(cuò)誤“setting a property that has only a getter”,只是我們需要注意的。

通常DOM操作都是改變?cè)嫉膬?nèi)容,這里有幾種方式來(lái)實(shí)現(xiàn)這個(gè),最簡(jiǎn)單的是使用innerHTML屬性,例如:

var myIntro = document.getElementById('intro');

// 替換當(dāng)前的內(nèi)容
myIntro.innerHTML = 'New content for the <strong>amazing</strong> paragraph!';

// 添加內(nèi)容到當(dāng)前的內(nèi)容里 
myIntro.innerHTML += '... some more content...';

唯一的問(wèn)題是該方法沒(méi)在規(guī)范里定義,而且在DOM規(guī)范里也沒(méi)有定義,如果你不反感的話(huà)請(qǐng)繼續(xù)使用,因?yàn)樗任覀兿旅嬉懻撈渌姆椒於嗔恕?/p>

Node節(jié)點(diǎn)

通過(guò)DOM API創(chuàng)建內(nèi)容的時(shí)候需要注意node節(jié)點(diǎn)的2種類(lèi)型,一種是元素節(jié)點(diǎn),一種是text節(jié)點(diǎn),上一章節(jié)已經(jīng)列出了所有的節(jié)點(diǎn)類(lèi)型,這兩種需要我們現(xiàn)在特別注意。創(chuàng)建元素可以通過(guò)createElement方法,而創(chuàng)建text節(jié)點(diǎn)可以使用createTextNode,相應(yīng)代碼如下:

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é)點(diǎn)附件到文本字段,這樣做比非標(biāo)準(zhǔn)的innerHTML方法顯得有點(diǎn)長(zhǎng),但了解這些原理依然很重要,這里有一個(gè)使用DOM方法的更詳細(xì)例子:

var myIntro = document.getElementById('intro');

// 添加新連接到文本節(jié)點(diǎn)
// 首先,創(chuàng)建新連接元素
var myNewLink = document.createElement('a'); // <a/>  
myNewLink.href = 'http://google.com'; // <a href="http://google.com"/&gt;  
myNewLink.appendChild(document.createTextNode('Visit Google'));
// <a href="http://google.com">Visit Google</a>  

// 將內(nèi)容附件到文本節(jié)點(diǎn)
myIntro.appendChild(myNewLink);

另外DOM里還有一個(gè)insertBefore方法用于再節(jié)點(diǎn)前面附件內(nèi)容,通過(guò)insertBefore和appendChild我們可以實(shí)現(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目表達(dá)式:  
// 格式:條件?條件為true時(shí)的表達(dá)式:條件為false時(shí)的表達(dá)式

上面的函數(shù)首先檢查target元素的同級(jí)下一個(gè)節(jié)點(diǎn)是否存在,如果存在就在該節(jié)點(diǎn)前面添加bullet節(jié)點(diǎn),如果不存在,就說(shuō)明target是最后一個(gè)節(jié)點(diǎn)了,直接在后面append新節(jié)點(diǎn)就可以了。DOM API沒(méi)有給提供insertAfter是因?yàn)檎娴臎](méi)必要了——我們可以自己創(chuàng)建。

DOM操作有很多內(nèi)容,上面你看到的只是其中一部分。

Event事件

瀏覽器事件是所有web程序的核心,通過(guò)這些事件我們定義將要發(fā)生的行為,如果在頁(yè)面里有個(gè)按鈕,那點(diǎn)擊此按鈕之前你需要驗(yàn)證表單是否合法,這時(shí)候就可以使用click事件,下面列出的最標(biāo)準(zhǔn)的事件列表:

注:正如我們上章所說(shuō)的,DOM和JavaScript語(yǔ)言是2個(gè)單獨(dú)的東西,瀏覽器事件是DOM API的一部分,而不是JavaScript的一部分。

鼠標(biāo)事件

  1.  ‘mousedown’ – 鼠標(biāo)設(shè)備按下一個(gè)元素的時(shí)候觸發(fā)mousedown事件。
  2.  ‘mouseup’ – 鼠標(biāo)設(shè)備從按下的元素上彈起的時(shí)候觸發(fā)mouseup事件。
  3.  ‘click’ – 鼠標(biāo)點(diǎn)擊元素的時(shí)候觸發(fā)click事件。
  4.  ‘dblclick’ – 鼠標(biāo)雙擊元素的時(shí)候觸發(fā)dblclick事件。
  5. mouseover’ – 鼠標(biāo)移動(dòng)到某元素上的時(shí)候觸發(fā)mouseover事件。
  6.  ‘mouseout’ – 鼠標(biāo)從某元素離開(kāi)的時(shí)候觸發(fā)mouseout事件。
  7.  ‘mousemove’ – 鼠標(biāo)在某元素上移動(dòng)但未離開(kāi)的時(shí)候觸發(fā)mousemove事件。

鍵盤(pán)事件

  1. keypress’ – 按鍵按下的時(shí)候觸發(fā)該事件。
  2. keydown’ – 按鍵按下的時(shí)候觸發(fā)該事件,并且在keypress事件之前。
  3. keyup’ – 按鍵松開(kāi)的時(shí)候觸發(fā)該事件,在keydown和keypress事件之后。

表單事件

  1. select’ – 文本字段(input, textarea等)的文本被選擇的時(shí)候觸發(fā)該事件。
  2. change’ – 控件失去input焦點(diǎn)的時(shí)候觸發(fā)該事件(或者值被改變的時(shí)候)。
  3. submit’ – 表單提交的時(shí)候觸發(fā)該事件。
  4. reset’ – 表單重置的時(shí)候觸發(fā)該事件。
  5. focus’ – 元素獲得焦點(diǎn)的時(shí)候觸發(fā)該事件,通常來(lái)自鼠標(biāo)設(shè)備或Tab導(dǎo)航。
  6. blur’ – 元素失去焦點(diǎn)的時(shí)候觸發(fā)該事件,通常來(lái)自鼠標(biāo)設(shè)備或Tab導(dǎo)航。

其它事件

  1. load’ – 頁(yè)面加載完畢(包括內(nèi)容、圖片、frame、object)的時(shí)候觸發(fā)該事件。
  2. resize’ – 頁(yè)面大小改變的時(shí)候觸發(fā)該事件(例如瀏覽器縮放)。
  3. scroll’ – 頁(yè)面滾動(dòng)的時(shí)候觸發(fā)該事件。
  4. unload’ – 從頁(yè)面或frame刪除所有內(nèi)容的時(shí)候觸發(fā)該事件(例如離開(kāi)一個(gè)頁(yè)面)。

還有很多各種各樣的事件,上面展示的事件是我們?cè)贘avaScript里最常用的事件,有些事件在跨瀏覽器方面可能有所不同。還有其它瀏覽器實(shí)現(xiàn)的一些屬性事件,例如Gecko實(shí)現(xiàn)的DOMContentLoaded或DOMMouseScroll等,Gecko的詳細(xì)事件列表請(qǐng)查看這里。

事件處理

我們將了事件,但是還沒(méi)有將到如何將處理函數(shù)和事件管理起來(lái),使用這些事件之前,你首先要注冊(cè)這些事件句柄,然后描述該事件發(fā)生的時(shí)候該如何處理,下面的例子展示了一個(gè)基本的事件注冊(cè)模型:

基本事件注冊(cè):

<!-- HTML -->
<button id="my-button">Click me!</button>  
// JavaScript:  
var myElement = document.getElementById('my-button');

// 事件處理句柄:  
function buttonClick() {
    alert('You just clicked the button!');
}

// 注冊(cè)事件
myElement.onclick = buttonClick; 

使用document.getElementById命令,通過(guò)ID=my-button獲取該button對(duì)象,然后創(chuàng)建一個(gè)處理函數(shù),隨后將該函數(shù)賦值給該DOM的onclick屬性。就這么簡(jiǎn)單!

基本事件注冊(cè)是非常簡(jiǎn)單的,在事件名稱(chēng)前面添加前綴on作為DOM的屬性就可以使用了,這是事件處理的基本核心,但下面的代碼我不推薦使用:

<button onclick="return buttonClick()">Click me!</button>

上述Inline的事件處理方式不利用頁(yè)面維護(hù),建議將這些處理函數(shù)都封裝在單獨(dú)的js文件,原因和CSS樣式的一樣的。

高級(jí)事件注冊(cè):

別被標(biāo)題迷惑了,“高級(jí)”不意味著好用,實(shí)際上上面討論的基本事件注冊(cè)是我們大部分時(shí)候用的方式,但有一個(gè)限制:不能綁定多個(gè)處理函數(shù)到一個(gè)事件上。這也是我們要講解該小節(jié)原因:

該模型運(yùn)行你綁定多個(gè)處理句柄到一個(gè)事件上,也就是說(shuō)一個(gè)事件觸發(fā)的時(shí)候多個(gè)函數(shù)都可以執(zhí)行,另外,該模型也可以讓你很容易里刪除某個(gè)已經(jīng)綁定的句柄。

嚴(yán)格來(lái)說(shuō),有2中不同的模型:W3C模型和微軟模型,除IE之外W3C模型支持所有的現(xiàn)代瀏覽器,而微軟模型只支持IE,使用W3C模型的代碼如下:

// 格式:target.addEventListener( type, function, useCapture );  
// 例子:  
var myIntro = document.getElementById('intro');
myIntro.addEventListener('click', introClick, false);

使用IE模型.aspx)的代碼如下:

// 格式: target.attachEvent ( 'on' + type, function );  
// 例子:  
var myIntro = document.getElementById('intro');
myIntro.attachEvent('onclick', introClick);

introClick的代碼如下:

function introClick() {
    alert('You clicked the paragraph!');
}

事實(shí)上,要做出通用的話(huà),我們可以自定義一個(gè)函數(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屬性,誰(shuí)可以就用誰(shuí),這兩種類(lèi)型的模型都支持刪除句柄功能,參考下面的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!!!');
});

注意到我們傳入了一個(gè)匿名函數(shù)作為第三個(gè)參數(shù),JavaScript運(yùn)行我們定義和執(zhí)行匿名函數(shù),這種匿名函數(shù)特別適合作為參數(shù)傳遞,實(shí)際上我們也可以傳遞有名的函數(shù)(代碼如下),但是你們函數(shù)更容易做。

如果你只想在第一次click的時(shí)候觸發(fā)一個(gè)函數(shù),你可以這么做:

// 注意:前提是我們已經(jīng)定于好了addEvent/removeEvent函數(shù)
// (定義好了才能使用哦)  

var myIntro = document.getElementById('intro');
addEvent(myIntro, 'click', oneClickOnly);

function oneClickOnly() {
    alert('WOW!');
    removeEvent(myIntro, 'click', oneClickOnly);
} 

當(dāng)?shù)谝淮斡|發(fā)以后,我們就立即刪除該句柄,但是有匿名函數(shù)的話(huà)卻很難將自身的引用刪除,不過(guò)實(shí)際上可以通過(guò)如下的形式來(lái)做(只不過(guò)有點(diǎn)麻煩):

addEvent(myIntro, 'click', function () {
    alert('WOW!');
    removeEvent(myIntro, 'click', arguments.callee);
}); 

這里我們是有了arguments對(duì)象的callee屬性,arguments對(duì)象包含了所有傳遞進(jìn)來(lái)的參數(shù)以及該函數(shù)自身(callee),這樣我們就可以放心地刪除自身的引用了。 關(guān)于W3C和微軟模型還有其他的少許差異,比如this,在觸發(fā)事件的時(shí)候函數(shù)中的this一般都是該元素上下文,,也就說(shuō)this引用該元素自身,在基本事件注冊(cè)和W3C模型中都沒(méi)有問(wèn)題,但在微軟模型的實(shí)現(xiàn)里卻可能出錯(cuò),請(qǐng)參考如下代碼:

function myEventHandler() {
    this.style.display = 'none';
}

// 正常工作,this是代表該元素
myIntro.onclick = myEventHandler;

// 正常工作,this是代表該元素
myIntro.addEventListener('click', myEventHandler, false);

// 不正常,這時(shí)候的this是代表Window對(duì)象
myIntro.attachEvent('onclick', myEventHandler);

這里有一些方式可以避免這個(gè)問(wèn)題,最簡(jiǎn)單的方式是使用前面的基本事件注冊(cè)方式,或者是再做一個(gè)通用的addEvent,通用代碼請(qǐng)參考John ResigDean Edward的文章。

Event對(duì)象

另外一個(gè)非常重要的內(nèi)容是Event對(duì)象,當(dāng)事件發(fā)生的時(shí)候出發(fā)某個(gè)函數(shù),該Event對(duì)象將自動(dòng)在函數(shù)內(nèi)可用,該對(duì)象包含了很多事件觸發(fā)時(shí)候的信息,但I(xiàn)E卻沒(méi)有這么實(shí)現(xiàn),而是自己實(shí)現(xiàn)的,IE瀏覽器是通過(guò)全局對(duì)象window下的event屬性來(lái)包含這些信息,雖然不是大問(wèn)題,但我們也需要注意一下,下面的代碼是兼容性的:

function myEventHandler(e) {

    // 注意參數(shù)e
    // 該函數(shù)調(diào)用的時(shí)候e是event對(duì)象(W3C實(shí)現(xiàn))

    // 兼容IE的代碼
    e = e || window.event;

    // 現(xiàn)在e就可以兼容各種瀏覽器了

}

// 這里可以自由地綁定事件了

這里判斷e對(duì)象(Event對(duì)象)是否存在我們使用了OR操作符:如果e不存在(為null, undefined,0等)的時(shí)候,將window.event賦值給e,否則的話(huà)繼續(xù)使用e。通過(guò)這方式很快就能在多瀏覽器里得到真正的Event對(duì)象,如果你不喜歡這種方式的話(huà),你可以使用if語(yǔ)句來(lái)處理:

if (!e) {
    e = window.event;
} // 沒(méi)有else語(yǔ)句,因?yàn)閑在其它瀏覽器已經(jīng)定義了

另外Event對(duì)象下的命令和屬性都很有用,遺憾的是不不能全兼容瀏覽器,例如當(dāng)你想取消默認(rèn)的行為的時(shí)候你可以使用Event對(duì)象里的preventDefault()方法,但I(xiàn)E里不得不使用對(duì)象的returnValue屬性值來(lái)控制,兼容代碼如下:

function myEventHandler(e) {
    e = e || window.event;
    // 防止默認(rèn)行為
    if (e.preventDefault) {
        e.preventDefault();
    } else {
        e.returnValue = false;
    }
}

例如,當(dāng)你點(diǎn)擊一個(gè)連接的時(shí)候,默認(rèn)行為是導(dǎo)航到href里定義的地址,但有時(shí)候你想禁用這個(gè)默認(rèn)行為,通過(guò)returnValue和preventDefault就可以實(shí)現(xiàn),Event對(duì)象里的很多屬性在瀏覽器里都不兼容,所以很多時(shí)候需要處理這些兼容性代碼。

注意:現(xiàn)在很多JS類(lèi)庫(kù)都已經(jīng)封裝好了e.preventDefault代碼,也就是說(shuō)在IE里可用了,但是原理上依然是使用returnValue來(lái)實(shí)現(xiàn)的。

事件冒泡

事件冒泡,就是事件觸發(fā)的時(shí)候通過(guò)DOM向上冒泡,首先要知道不是所有的事件都有冒泡。事件在一個(gè)目標(biāo)元素上觸發(fā)的時(shí)候,該事件將觸發(fā)一一觸發(fā)祖先節(jié)點(diǎn)元素,直到最頂層的元素:

如圖所示,如果a連接被點(diǎn)擊,觸發(fā)觸發(fā)連接的click事件,然后觸發(fā)p的click事件,以此再觸發(fā)div和body的click事件。順序不變,而且不一定是在同時(shí)觸發(fā)的。 這樣你就可以利用該特性去處理自己的邏輯了,并且再任何時(shí)候都可以停止冒泡,比如,如果你只想冒泡到文本節(jié)點(diǎn)上,而不再進(jìn)一步冒泡,你可以在p的click事件處理函數(shù)里丁停止冒泡:

function myParagraphEventHandler(e) {

    e = e || window.event;

    // 停止向上冒泡
    if (e.stopPropagation) {
        // W3C實(shí)現(xiàn)  
        e.stopPropagation();
    } else {
        // IE實(shí)現(xiàn)  
        e.cancelBubble = true;
    }

}

// 使用我們自定義的addEvent函數(shù)將myParagraphEventHandler綁定到click事件上:  
addEvent(document.getElementsByTagName('p')[0], 'click', myParagraphEventHandler);

事件委托

舉例來(lái)說(shuō),如果你有一個(gè)很多行的大表格,在每個(gè)<tr>上綁定點(diǎn)擊事件是個(gè)非常危險(xiǎn)的想法,因?yàn)樾阅苁莻€(gè)大問(wèn)題。流行的做法是使用事件委托。事件委托描述的是將事件綁定在容器元素上,然后通過(guò)判斷點(diǎn)擊的target子元素的類(lèi)型來(lái)觸發(fā)相應(yīng)的事件。

var myTable = document.getElementById('my-table');

myTable.onclick = function () {

    // 處理瀏覽器兼容
    e = e || window.event;
    var targetNode = e.target || e.srcElement;

    // 測(cè)試如果點(diǎn)擊的是TR就觸發(fā)
    if (targetNode.nodeName.toLowerCase() === 'tr') {
        alert('You clicked a table row!');
    }

}

事件委托依賴(lài)于事件冒泡,如果事件冒泡到table之前被禁用的話(huà),那上面的代碼就無(wú)法工作了。

總結(jié)

本章我們覆蓋到了DOM元素的操作以及相關(guān)的瀏覽器事件模型,希望大家能對(duì)DOM有了進(jìn)一步的了解。有任何問(wèn)題,請(qǐng)留言討論。


以上內(nèi)容是否對(duì)您有幫助:
在線(xiàn)筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)