《JavaScript 闖關記》之事件

JavaScript 程序採用了異步事件驅動編程模型。在這種程序設計風格下,當文檔、瀏覽器、元素或與之相關的對象發生某些有趣的事情時,Web 瀏覽器就會產生事件(event)。例如,當 Web 瀏覽器加載完文檔、用戶把鼠標指針移到超連接上或敲擊鍵盤時,Web 瀏覽器都會產生事件。若是 JavaScript 應用程序關注特定類型的事件,那麼它能夠註冊當這類事件發生時要調用的一個或多個函數。請注意,這種風格並不僅應用於 Web 編程,全部使用圖形用戶界面的應用程序都採用了它,它們靜待某些事情發生(即,它們等待事件發生),而後它們響應。javascript

請注意,事件自己並非一個須要定義的技術名詞。簡而言之,事件就是 Web 瀏覽器通知應用程序發生了什麼事情,這種在傳統軟件工程中被稱爲觀察員模式。html

事件流

當瀏覽器發展到第四代時(IE4 及 Netscape Communicator 4),瀏覽器開發團隊遇到了一個頗有意思的問題:頁面的哪一部分會擁有某個特定的事件?要明白這個問題問的是什麼,能夠想象畫在一張紙上的一組同心圓。若是你把手指放在圓心上,那麼你的手指指向的不是一個圓,而是紙上的全部圓。兩家公司的瀏覽器開發團隊在看待瀏覽器事件方面仍是一致的。若是你單擊了某個按鈕,他們都認爲單擊事件不只僅發生在按鈕上。換句話說,在單擊按鈕的同時,你也單擊了按鈕的容器元素,甚至也單擊了整個頁面。java

事件流描述的是從頁面中接收事件的順序。但有意思的是,IE 和 Netscape 開發團隊竟然提出了差很少是徹底相反的事件流的概念。IE 的事件流是事件冒泡流,而 Netscape Communicator 的事件流是事件捕獲流。git

事件冒泡

IE 的事件流叫作事件冒泡(event bubbling),即事件開始時由最具體的元素(文檔中嵌套層次最深的那個節點)接收,而後逐級向上傳播到較爲不具體的節點(文檔)。如下面的HTML頁面爲例:github

<!DOCTYPE html>
<html>
<head>
    <title>Event Bubbling Example</title>
</head>
<body>
    <div id="myDiv">Click Me</div>
</body>
</html>

若是你單擊了頁面中的 <div> 元素,那麼這個 click 事件會按照以下順序傳播:編程

  1. <div>瀏覽器

  2. <body>微信

  3. <html>less

  4. document異步

也就是說,click 事件首先在 <div> 元素上發生,而這個元素就是咱們單擊的元素。而後,click 事件沿 DOM 樹向上傳播,在每一級節點上都會發生,直至傳播到 document 對象。下圖展現了事件冒泡的過程。

事件捕獲

Netscape Communicator 團隊提出的另外一種事件流叫作事件捕獲(event capturing)。事件捕獲的思想是不太具體的節點應該更早接收到事件,而最具體的節點應該最後接收到事件。事件捕獲的用意在於在事件到達預約目標以前捕獲它。若是仍之前面的 HTML 頁面做爲演示事件捕獲的例子,那麼單擊 <div> 元素就會如下列順序觸發 click 事件。

  1. document

  2. <html>

  3. <body>

  4. <div>

在事件捕獲過程當中,document 對象首先接收到 click 事件,而後事件沿 DOM 樹依次向下,一直傳播到事件的實際目標,即 <div> 元素。下圖展現了事件捕獲的過程。

因爲老版本的瀏覽器不支持,所以不多有人使用事件捕獲。咱們也建議你們放心地使用事件冒泡,在有特殊須要時再使用事件捕獲。

事件處理程序

事件就是用戶或瀏覽器自身執行的某種動做。諸如 clickloadmouseover,都是事件的名字。而響應某個事件的函數就叫作事件處理程序(或事件偵聽器)。事件處理程序的名字以 "on" 開頭,所以 click 事件的事件處理程序就是 onclickload 事件的事件處理程序就是 onload。爲事件指定處理程序的方式有好幾種。

HTML 事件處理程序

某個元素支持的每種事件,均可以使用一個與相應事件處理程序同名的 HTML 特性來指定。這個特性的值應該是可以執行的 JavaScript 代碼。例如,要在按鈕被單擊時執行一些 JavaScript,能夠像下面這樣編寫代碼:

<input type="button" value="Click Me" onclick="console.log('Clicked')" />

當單擊這個按鈕時,就會在控制檯打印 "Clicked"。這個操做是經過指定 onclick 特性並將一些 JavaScript 代碼做爲它的值來定義的。因爲這個值是 JavaScript,所以不能在其中使用未經轉義的 HTML 語法字符,例如和號(&)、雙引號("")、小於號(<)或大於號(>)。爲了不使用 HTML 實體,這裏使用了單引號。若是想要使用雙引號,那麼就要將代碼改寫成以下所示:

<input type="button" value="Click Me" onclick="console.log(&quot;Clicked&quot;)" />

在 HTML 中定義的事件處理程序能夠包含要執行的具體動做,也能夠調用在頁面其餘地方定義的腳本,以下面的例子所示:

<script type="text/javascript">
    function showMessage(){
        console.log("Hello world!");
    }
</script>
<input type="button" value="Click Me" onclick="showMessage()" />

在這個例子中,單擊按鈕就會調用 showMessage() 函數。這個函數是在一個獨立的 <script> 元素中定義的,固然也能夠被包含在一個外部文件中。事件處理程序中的代碼在執行時,有權訪問全局做用域中的任何代碼。

這樣指定事件處理程序具備一些獨到之處。首先,這樣會建立一個封裝着元素屬性值的函數。這個函數中有一個局部變量 event,也就是事件對象:

<!-- 輸出 "click" -->
<input type="button" value="Click Me" onclick="console.log(event.type)">

經過 event 變量,能夠直接訪問事件對象,你不用本身定義它,也不用從函數的參數列表中讀取。

在這個函數內部,this 值等於事件的目標元素,例如:

<!-- 輸出 "Click Me" -->
<input type="button" value="Click Me" onclick="console.log(this.value)">

如此一來,事件處理程序要訪問本身的屬性就簡單多了。下面這行代碼與前面的例子效果相同:

<!-- 輸出 "Click Me" -->
<input type="button" value="Click Me" onclick="console.log(value)">

不過,在 HTML 中指定事件處理程序有三個缺點。首先,存在一個時差問題。由於用戶可能會在 HTML 元素一出如今頁面上就觸發相應的事件,但當時的事件處理程序有可能尚不具有執行條件。之前面的例子來講明,假設 showMessage() 函數是在按鈕下方、頁面的最底部定義的。若是用戶在頁面解析 showMessage() 函數以前就單擊了按鈕,就會引起錯誤。爲此,不少HTML事件處理程序都會被封裝在一個 try-catch 塊中,以便錯誤不會浮出水面,以下面的例子所示:

<input type="button" value="Click Me" onclick="try{showMessage();}catch(ex){}">

這樣,若是在 showMessage() 函數有定義以前單擊了按鈕,用戶將不會看到 JavaScript 錯誤,由於在瀏覽器有機會處理錯誤以前,錯誤就被捕獲了。

第二個缺點是,這樣擴展事件處理程序的做用域鏈在不一樣瀏覽器中會致使不一樣結果。不一樣 JavaScript 引擎遵循的標識符解析規則略有差別,極可能會在訪問非限定對象成員時出錯。

第三個缺點是,HTML 與 JavaScript 代碼緊密耦合。若是要更換事件處理程序,就要改動兩個地方:HTML 代碼和 JavaScript 代碼。而這正是許多開發人員摒棄 HTML 事件處理程序,轉而使用 JavaScript 指定事件處理程序的緣由所在。

DOM1 級事件處理程序

經過 JavaScript 指定事件處理程序的傳統方式,就是將一個函數賦值給一個事件處理程序屬性。這種爲事件處理程序賦值的方法是在第四代Web瀏覽器中出現的,並且至今仍然爲全部現代瀏覽器所支持。緣由一是簡單,二是具備跨瀏覽器的優點。要使用 JavaScript 指定事件處理程序,首先必須取得一個要操做的對象的引用。

每一個元素(包括 windowdocument)都有本身的事件處理程序屬性,這些屬性一般所有小寫,例如 onclick。將這種屬性的值設置爲一個函數,就能夠指定事件處理程序,以下所示:

var btn = document.getElementById("myBtn");
btn.onclick = function(){
    console.log("Clicked");
};

在此,咱們經過文檔對象取得了一個按鈕的引用,而後爲它指定了 onclick 事件處理程序。但要注意,在這些代碼運行之前不會指定事件處理程序,所以若是這些代碼在頁面中位於按鈕後面,就有可能在一段時間內怎麼單擊都沒有反應。

使用 DOM1 級方法指定的事件處理程序被認爲是元素的方法。所以,這時候的事件處理程序是在元素的做用域中運行;換句話說,程序中的 this 引用當前元素。來看一個例子。

var btn = document.getElementById("myBtn");
btn.onclick = function(){
    console.log(this.id);    // "myBtn"
};

單擊按鈕顯示的是元素的 ID,這個 ID 是經過 this.id 取得的。不只僅是 ID,實際上能夠在事件處理程序中經過 this 訪問元素的任何屬性和方法。以這種方式添加的事件處理程序會在事件流的冒泡階段被處理。

也能夠刪除經過 DOM1 級方法指定的事件處理程序,只要像下面這樣將事件處理程序屬性的值設置爲 null 便可:

btn.onclick = null;     // 刪除事件處理程序

將事件處理程序設置爲 null 以後,再單擊按鈕將不會有任何動做發生。

DOM2 級事件處理程序

DOM2 級事件定義了兩個方法,用於處理指定和刪除事件處理程序的操做:addEventListener()removeEventListener()。全部 DOM 節點中都包含這兩個方法,而且它們都接受3個參數:要處理的事件名、做爲事件處理程序的函數和一個布爾值。最後這個布爾值參數若是是 true,表示在捕獲階段調用事件處理程序;若是是 false,表示在冒泡階段調用事件處理程序。

要在按鈕上爲 click 事件添加事件處理程序,可使用下列代碼:

var btn = document.getElementById("myBtn");
btn.addEventListener("click", function(){
    console.log(this.id);
}, false);

上面的代碼爲一個按鈕添加了 onclick 事件處理程序,並且該事件會在冒泡階段被觸發(由於最後一個參數是 false)。與 DOM1 級方法同樣,這裏添加的事件處理程序也是在其依附的元素的做用域中運行。使用 DOM2 級方法添加事件處理程序的主要好處是能夠添加多個事件處理程序。來看下面的例子。

var btn = document.getElementById("myBtn");
btn.addEventListener("click", function(){
    console.log(this.id);
}, false);
btn.addEventListener("click", function(){
    console.log("Hello world!");
}, false);

這裏爲按鈕添加了兩個事件處理程序。這兩個事件處理程序會按照添加它們的順序觸發,所以首先會顯示元素的 ID,其次會顯示 "Hello world!" 消息。

經過 addEventListener() 添加的事件處理程序只能使用 removeEventListener() 來移除;移除時傳入的參數與添加處理程序時使用的參數相同。這也意味着經過 addEventListener() 添加的匿名函數將沒法移除,以下面的例子所示。

var btn = document.getElementById("myBtn");
btn.addEventListener("click", function(){
    console.log(this.id);
}, false);
btn.removeEventListener("click", function(){ // 沒有用!
    console.log(this.id);
}, false);

在這個例子中,咱們使用 addEventListener() 添加了一個事件處理程序。雖然調用 removeEventListener() 時看似使用了相同的參數,但實際上,第二個參數與傳入 addEventListener() 中的那一個是徹底不一樣的函數。而傳入 removeEventListener() 中的事件處理程序函數必須與傳 入addEventListener() 中的相同,以下面的例子所示。

var btn = document.getElementById("myBtn");
var handler = function(){
    console.log(this.id);
};
btn.addEventListener("click", handler, false);
btn.removeEventListener("click", handler, false); // 有效!

重寫後的這個例子沒有問題,是由於在 addEventListener()removeEventListener() 中使用了相同的函數。

大多數狀況下,都是將事件處理程序添加到事件流的冒泡階段,這樣能夠最大限度地兼容各類瀏覽器。最好只在須要在事件到達目標以前截獲它的時候將事件處理程序添加到捕獲階段。若是不是特別須要,咱們不建議在事件捕獲階段註冊事件處理程序。

IE九、Firefox、Safari、Chrome 和 Opera 支持 DOM2 級事件處理程序。

IE 事件處理程序

IE 實現了與 DOM 中相似的兩個方法:attachEvent()detachEvent()。這兩個方法接受相同的兩個參數:事件處理程序名稱與事件處理程序函數。因爲 IE8 及更早版本只支持事件冒泡,因此經過 attachEvent() 添加的事件處理程序都會被添加到冒泡階段。

要使用 attachEvent() 爲按鈕添加一個事件處理程序,可使用如下代碼。

var btn = document.getElementById("myBtn");
btn.attachEvent("onclick", function(){
    console.log("Clicked");
});

注意,attachEvent() 的第一個參數是 "onclick",而非 DOM 的 addEventListener() 方法中的 "click"

在 IE 中使用 attachEvent() 與使用 DOM1 級方法的主要區別在於事件處理程序的做用域。在使用 DOM1 級方法的狀況下,事件處理程序會在其所屬元素的做用域內運行;在使用 attachEvent() 方法的狀況下,事件處理程序會在全局做用域中運行,所以 this 等於 window。來看下面的例子。

var btn = document.getElementById("myBtn");
btn.attachEvent("onclick", function(){
    console.log(this === window);    // true
});

在編寫跨瀏覽器的代碼時,牢記這一區別很是重要。

addEventListener() 相似,attachEvent() 方法也能夠用來爲一個元素添加多個事件處理程序。不過,與 DOM 方法不一樣的是,這些事件處理程序不是以添加它們的順序執行,而是以相反的順序被觸發。

使用 attachEvent() 添加的事件能夠經過 detachEvent() 來移除,條件是必須提供相同的參數。與 DOM 方法同樣,這也意味着添加的匿名函數將不能被移除。不過,只要可以將對相同函數的引用傳給 detachEvent(),就能夠移除相應的事件處理程序。

支持 IE 事件處理程序的瀏覽器有 IE 和 Opera。

跨瀏覽器的事件處理程序

爲了以跨瀏覽器的方式處理事件,很多開發人員會使用可以隔離瀏覽器差別的 JavaScript 庫,還有一些開發人員會本身開發最合適的事件處理的方法。本身編寫代碼其實也不難,只要恰當地使用能力檢測便可。要保證處理事件的代碼能在大多數瀏覽器下一致地運行,只需關注冒泡階段。

第一個要建立的方法是 addHandler(),它的職責是視狀況分別使用 DOM1 級方法、DOM2 級方法或 IE 方法來添加事件。這個方法屬於一個名叫 EventUtil 的對象,本書將使用這個對象來處理瀏覽器間的差別。addHandler() 方法接受3個參數:要操做的元素、事件名稱和事件處理程序函數。

addHandler() 對應的方法是 removeHandler(),它也接受相同的參數。這個方法的職責是移除以前添加的事件處理程序——不管該事件處理程序是採起什麼方式添加到元素中的,若是其餘方法無效,默認採用 DOM1 級方法。

EventUtil 的用法以下所示。

var EventUtil = {
    addHandler: function(element, type, handler){
        if (element.addEventListener){
            element.addEventListener(type, handler, false);
        } else if (element.attachEvent){
            element.attachEvent("on" + type, handler);
        } else {
            element["on" + type] = handler;
        }
    },
    removeHandler: function(element, type, handler){
        if (element.removeEventListener){
            element.removeEventListener(type, handler, false);
        } else if (element.detachEvent){
            element.detachEvent("on" + type, handler);
        } else {
            element["on" + type] = null;
        }
    }
};

這兩個方法首先都會檢測傳入的元素中是否存在 DOM2 級方法。若是存在 DOM2 級方法,則使用該方法:傳入事件類型、事件處理程序函數和第三個參數 false(表示冒泡階段)。若是存在的是 IE 的方法,則採起第二種方案。注意,爲了在 IE8 及更早版本中運行,此時的事件類型必須加上 "on" 前綴。最後一種可能就是使用 DOM1 級方法(在現代瀏覽器中,應該不會執行這裏的代碼)。此時,咱們使用的是方括號語法來將屬性名指定爲事件處理程序,或者將屬性設置爲 null

能夠像下面這樣使用 EventUtil 對象:

var btn = document.getElementById("myBtn");
var handler = function(){
    console.log("Clicked");
};
EventUtil.addHandler(btn, "click", handler);
EventUtil.removeHandler(btn, "click", handler);

addHandler()removeHandler() 沒有考慮到全部的瀏覽器問題,例如在 IE 中的做用域問題。不過,使用它們添加和移除事件處理程序仍是足夠了。

事件對象

在觸發 DOM 上的某個事件時,會產生一個事件對象 event,這個對象中包含着全部與事件有關的信息。包括致使事件的元素、事件的類型以及其餘與特定事件相關的信息。例如,鼠標操做致使的事件對象中,會包含鼠標位置的信息,而鍵盤操做致使的事件對象中,會包含與按下的鍵有關的信息。全部瀏覽器都支持 event 對象,但支持方式不一樣。

DOM 中的事件對象

兼容 DOM 的瀏覽器會將一個 event 對象傳入到事件處理程序中。不管指定事件處理程序時使用什麼方法(DOM1 級或 DOM2 級),都會傳入 event 對象。來看下面的例子。

var btn = document.getElementById("myBtn");
btn.onclick = function(event){
    console.log(event.type);     // "click"
};
btn.addEventListener("click", function(event){
    console.log(event.type);     // "click"
}, false);

這個例子中的兩個事件處理程序都會彈出一個警告框,顯示由 event.type 屬性表示的事件類型。這個屬性始終都會包含被觸發的事件類型,例如 "click"(與傳入 addEventListener()removeEventListener() 中的事件類型一致)。

在經過 HTML 特性指定事件處理程序時,變量 event 中保存着 event 對象。請看下面的例子。

<input type="button" value="Click Me" onclick="console.log(event.type)"/>

以這種方式提供 event 對象,可讓 HTML 特性事件處理程序與 JavaScript 函數執行相同的操做。

event 對象包含與建立它的特定事件有關的屬性和方法。觸發的事件類型不同,可用的屬性和方法也不同。不過,全部事件都會有下表列出的成員。

  • bubbles,代表事件是否冒泡。

  • cancelable,代表是否能夠取消事件的默認行爲。

  • currentTarget,其事件處理程序當前正在處理事件的那個元素。

  • defaultPrevented,爲 true 表示已經調用了 preventDefault()(DOM3 級事件中新增)。

  • detail,與事件相關的細節信息。

  • eventPhase,調用事件處理程序的階段:1表示捕獲階段,2表示「處於目標」,3表示冒泡階段。

  • preventDefault(),取消事件的默認行爲。若是 cancelabletrue,則可使用這個方法。

  • stopImmediatePropagation(),取消事件的進一步捕獲或冒泡,同時阻止任何事件處理程序被調用(DOM3 級事件中新增)。

  • stopPropagation(),取消事件的進一步捕獲或冒泡。若是 bubblestrue,則可使用這個方法。

  • target,事件的目標。

  • trusted,爲 true 表示事件是瀏覽器生成的。爲 false 表示事件是由開發人員經過 JavaScript 建立的(DOM3 級事件中新增)。

  • type,被觸發的事件的類型。

  • view,與事件關聯的抽象視圖,等同於發生事件的 window 對象。

在事件處理程序內部,對象 this 始終等於 currentTarget 的值,而 target 則只包含事件的實際目標。若是直接將事件處理程序指定給了目標元素,則 thiscurrentTargettarget 包含相同的值。來看下面的例子。

var btn = document.getElementById("myBtn");
btn.onclick = function(event){
    console.log(event.currentTarget === this);    // true
    console.log(event.target === this);           // true
};

這個例子檢測了 currentTargettargetthis 的值。因爲 click 事件的目標是按鈕,所以這三個值是相等的。若是事件處理程序存在於按鈕的父節點中(例如 document.body),那麼這些值是不相同的。再看下面的例子。

document.body.onclick = function(event){
    console.log(event.currentTarget === document.body);  // true
    console.log(this === document.body);                 // true
    console.log(event.target === document.getElementById("myBtn"));  // true
};

當單擊這個例子中的按鈕時,thiscurrentTarget 都等於document.body,由於事件處理程序是註冊到這個元素上的。然而,target 元素卻等於按鈕元素,由於它是 click 事件真正的目標。因爲按鈕上並無註冊事件處理程序,結果 click 事件就冒泡到了 document.body,在那裏事件才獲得了處理。

在須要經過一個函數處理多個事件時,可使用 type 屬性。例如:

var btn = document.getElementById("myBtn");
var handler = function(event){
    switch(event.type){
        case "click":
            console.log("Clicked");
            break;
        case "mouseover":
            event.target.style.backgroundColor = "red";
            break;
        case "mouseout":
            event.target.style.backgroundColor = "";
            break;
    }
};
btn.onclick = handler;
btn.onmouseover = handler;
btn.onmouseout = handler;

這個例子定義了一個名爲 handler 的函數,用於處理3種事件:clickmouseovermouseout。當單擊按鈕時,會出現一個與前面例子中同樣的警告框。當按鈕移動到按鈕上面時,背景顏色應該會變成紅色,而當鼠標移動出按鈕的範圍時,背景顏色應該會恢復爲默認值。這裏經過檢測 event.type 屬性,讓函數可以肯定發生了什麼事件,並執行相應的操做。

要阻止特定事件的默認行爲,可使用 preventDefault() 方法。例如,連接的默認行爲就是在被單擊時會導航到其 href 特性指定的 URL。若是你想阻止連接導航這一默認行爲,那麼經過連接的 onclick 事件處理程序能夠取消它,以下面的例子所示。

var link = document.getElementById("myLink");
link.onclick = function(event){
    event.preventDefault();
};

只有 cancelable 屬性設置爲 true 的事件,纔可使用 preventDefault() 來取消其默認行爲。

另外,stopPropagation() 方法用於當即中止事件在 DOM 層次中的傳播,即取消進一步的事件捕獲或冒泡。例如,直接添加到一個按鈕的事件處理程序能夠調用 stopPropagation(),從而避免觸發註冊在 document.body 上面的事件處理程序,以下面的例子所示。

var btn = document.getElementById("myBtn");
btn.onclick = function(event){
    console.log("Clicked");
    event.stopPropagation();
};
document.body.onclick = function(event){
    console.log("Body clicked");
};

對於這個例子而言,若是不調用 stopPropagation(),就會在單擊按鈕時出現兩個警告框。但是,因爲 click 事件根本不會傳播到 document.body,所以就不會觸發註冊在這個元素上的 onclick 事件處理程序。

事件對象的 eventPhase 屬性,能夠用來肯定事件當前正位於事件流的哪一個階段。若是是在捕獲階段調用的事件處理程序,那麼 eventPhase 等於 1;若是事件處理程序處於目標對象上,則 eventPhase 等於 2;若是是在冒泡階段調用的事件處理程序,eventPhase 等於 3。這裏要注意的是,儘管「處於目標」發生在冒泡階段,但 eventPhase 仍然一直等於 2。來看下面的例子。

var btn = document.getElementById("myBtn");
btn.onclick = function(event){
    console.log(event.eventPhase); // 2
};
document.body.addEventListener("click", function(event){
    console.log(event.eventPhase); // 1
}, true);
document.body.onclick = function(event){
    console.log(event.eventPhase); // 3
};

當單擊這個例子中的按鈕時,首先執行的事件處理程序是在捕獲階段觸發的添加到 document.body 中的那一個,結果會彈出一個警告框顯示錶示 eventPhase1。接着,會觸發在按鈕上註冊的事件處理程序,此時的 eventPhase 值爲 2。最後一個被觸發的事件處理程序,是在冒泡階段執行的添加到 document.body 上的那一個,顯示 eventPhase 的值爲 3。而當 eventPhase 等於 2 時,thistargetcurrentTarget 始終都是相等的。

只有在事件處理程序執行期間,event對象纔會存在;一旦事件處理程序執行完成,event對象就會被銷燬。

IE 中的事件對象

與訪問 DOM 中的 event 對象不一樣,要訪問IE中的 event 對象有幾種不一樣的方式,取決於指定事件處理程序的方法。在使用 DOM1 級方法添加事件處理程序時,event 對象做爲 window 對象的一個屬性存在。來看下面的例子。

var btn = document.getElementById("myBtn");
btn.onclick = function(){
    var event = window.event;
    console.log(event.type);     // "click"
};

在此,咱們經過 window.event 取得了 event 對象,並檢測了被觸發事件的類型(IE 中的 type 屬性與 DOM 中的 type 屬性是相同的)。但是,若是事件處理程序是使用 attachEvent() 添加的,那麼就會有一個 event 對象做爲參數被傳入事件處理程序函數中,以下所示。

var btn = document.getElementById("myBtn");
btn.attachEvent("onclick", function(event){
    console.log(event.type);     // "click"
});

在像這樣使用 attachEvent() 的狀況下,也能夠經過 window 對象來訪問 event 對象,就像使用 DOM1 級方法時同樣。不過爲方便起見,同一個對象也會做爲參數傳遞。

若是是經過 HTML 特性指定的事件處理程序,那麼還能夠經過一個名叫 event 的變量來訪問 event 對象(與 DOM 中的事件模型相同)。再看一個例子。

<input type="button" value="Click Me" onclick="console.log(event.type)">

IE 的 event 對象一樣也包含與建立它的事件相關的屬性和方法。其中不少屬性和方法都有對應的或者相關的 DOM 屬性和方法。與 DOM 的 event 對象同樣,這些屬性和方法也會由於事件類型的不一樣而不一樣,但全部事件對象都會包含下表所列的屬性和方法。

  • cancelBubble,默認值爲 false,但將其設置爲 true 就能夠取消事件冒泡(與 DOM 中的 stopPropagation() 方法的做用相同)。

  • returnValue,默認值爲 true,但將其設置爲 false 就能夠取消事件的默認行爲(與 DOM 中的 preventDefault() 方法的做用相同) 。

  • srcElement,事件的目標(與 DOM 中的 target 屬性相同) 。

  • type,被觸發的事件的類型 。

由於事件處理程序的做用域是根據指定它的方式來肯定的,因此不能認爲 this 會始終等於事件目標。故而,最好仍是使用 event.srcElement 比較保險。例如:

var btn = document.getElementById("myBtn");
btn.onclick = function(){
    console.log(window.event.srcElement === this);  // true
};
btn.attachEvent("onclick", function(event){
    console.log(event.srcElement === this);         // false
});

在第一個事件處理程序中(使用 DOM1 級方法指定的),srcElement 屬性等於 this,但在第二個事件處理程序中,這二者的值不相同。

如前所述,returnValue 屬性至關於 DOM 中的 preventDefault() 方法,它們的做用都是取消給定事件的默認行爲。只要將 returnValue 設置爲 false,就能夠阻止默認行爲。來看下面的例子。

var link = document.getElementById("myLink");
link.onclick = function(){
    window.event.returnValue = false;
};

這個例子在 onclick 事件處理程序中使用 returnValue 達到了阻止連接默認行爲的目的。與 DOM 不一樣的是,在此沒有辦法肯定事件是否能被取消。

相應地,cancelBubble 屬性與 DOM 中的 stopPropagation() 方法做用相同,都是用來中止事件冒泡的。因爲IE不支持事件捕獲,於是只能取消事件冒泡;但 stopPropagatioin() 能夠同時取消事件捕獲和冒泡。例如:

var btn = document.getElementById("myBtn");
btn.onclick = function(){
    console.log("Clicked");
    window.event.cancelBubble = true;
};
document.body.onclick = function(){
    console.log("Body clicked");
};

經過在 onclick 事件處理程序中將 cancelBubble 設置爲 true,就可阻止事件經過冒泡而觸發 document.body 中註冊的事件處理程序。結果,在單擊按鈕以後,只會顯示一個警告框。

跨瀏覽器的事件對象

雖然 DOM 和 IE 中的 event 對象不一樣,但基於它們之間的類似性依舊能夠拿出跨瀏覽器的方案來。IE中 event 對象的所有信息和方法 DOM 對象中都有,只不過實現方式不同。不過,這種對應關係讓實現兩種事件模型之間的映射很是容易。能夠對前面介紹的 EventUtil 對象加以加強,添加以下方法以求同存異。

var EventUtil = {
    addHandler: function(element, type, handler){
        // 省略的代碼
    },
    getEvent: function(event){
        return event ? event : window.event;
    },
    getTarget: function(event){
        return event.target || event.srcElement;
    },
    preventDefault: function(event){
        if (event.preventDefault){
            event.preventDefault();
        } else {
            event.returnValue = false;
        }
    },
    removeHandler: function(element, type, handler){
        // 省略的代碼
    },
    stopPropagation: function(event){
        if (event.stopPropagation){
            event.stopPropagation();
        } else {
            event.cancelBubble = true;
        }
    }
};

以上代碼顯示,咱們爲 EventUtil 添加了4個新方法。第一個是 getEvent(),它返回對 event 對象的引用。考慮到 IE 中事件對象的位置不一樣,可使用這個方法來取得 event 對象,而沒必要擔憂指定事件處理程序的方式。在使用這個方法時,必須假設有一個事件對象傳入到事件處理程序中,並且要把該變量傳給這個方法,以下所示。

btn.onclick = function(event){
    event = EventUtil.getEvent(event);
};

在兼容 DOM 的瀏覽器中,event 變量只是簡單地傳入和返回。而在 IE 中,event 參數是未定義的 undefined,所以就會返回 window.event。將這一行代碼添加到事件處理程序的開頭,就能夠確保隨時都能使用 event 對象,而沒必要擔憂用戶使用的是什麼瀏覽器。

第二個方法是 getTarget(),它返回事件的目標。在這個方法內部,會檢測 event 對象的 target 屬性,若是存在則返回該屬性的值;不然,返回 srcElement 屬性的值。能夠像下面這樣使用這個方法。

btn.onclick = function(event){
    event = EventUtil.getEvent(event);
    var target = EventUtil.getTarget(event);
};

第三個方法是 preventDefault(),用於取消事件的默認行爲。在傳入 event 對象後,這個方法會檢查是否存在 preventDefault() 方法,若是存在則調用該方法。若是 preventDefault() 方法不存在,則將 returnValue 設置爲 false。下面是使用這個方法的例子。

var link = document.getElementById("myLink");
link.onclick = function(event){
    event = EventUtil.getEvent(event);
    EventUtil.preventDefault(event);
};

以上代碼能夠確保在全部瀏覽器中單擊該連接都不會打開另外一個頁面。首先,使用 EventUtil.getEvent() 取得 event 對象,而後將其傳入到 EventUtil.preventDefault() 以取消默認行爲。

第四個方法是 stopPropagation(),其實現方式相似。首先嚐試使用DOM方法阻止事件流,不然就使用 cancelBubble 屬性。下面看一個例子。

var btn = document.getElementById("myBtn");
btn.onclick = function(event){
    console.log("Clicked");
    event = EventUtil.getEvent(event);
    EventUtil.stopPropagation(event);
};
document.body.onclick = function(event){
    console.log("Body clicked");
};

在此,首先使用 EventUtil.getEvent() 取得了 event 對象,而後又將其傳入到 EventUtil.stopPropagation()。別忘了因爲 IE 不支持事件捕獲,所以這個方法在跨瀏覽器的狀況下,也只能用來阻止事件冒泡。

事件類型

Web 瀏覽器中可能發生的事件有不少類型。如前所述,不一樣的事件類型具備不一樣的信息,而 DOM3 級事件規定了如下幾類事件。

  • UI(User Interface,用戶界面)事件,當用戶與頁面上的元素交互時觸發;

  • 焦點事件,當元素得到或失去焦點時觸發;

  • 鼠標事件,當用戶經過鼠標在頁面上執行操做時觸發;

  • 滾輪事件,當使用鼠標滾輪(或相似設備)時觸發;

  • 文本事件,當在文檔中輸入文本時觸發;

  • 鍵盤事件,當用戶經過鍵盤在頁面上執行操做時觸發;

  • 合成事件,當爲IME(Input Method Editor,輸入法編輯器)輸入字符時觸發;

  • 變更(mutation)事件,當底層 DOM 結構發生變化時觸發。

  • 變更名稱事件,當元素或屬性名變更時觸發。此類事件已經被廢棄,沒有任何瀏覽器實現它們,所以本章不作介紹。

除了這幾類事件以外,HTML5 也定義了一組事件,而有些瀏覽器還會在 DOM 和 BOM 中實現其餘專有事件。這些專有的事件通常都是根據開發人員需求定製的,沒有什麼規範,所以不一樣瀏覽器的實現有可能不一致。

DOM3 級事件模塊在 DOM2 級事件模塊基礎上從新定義了這些事件,也添加了一些新事件。包括 IE9 在內的全部主流瀏覽器都支持 DOM2 級事件。 IE9 也支持 DOM3 級事件。

想要了解更多 DOM 和 HTML5 事件,請參見最新版的 W3C 規範:
https://www.w3.org/TR/uievents/

小結

事件是將 JavaScript 與網頁聯繫在一塊兒的主要方式。DOM3 級事件規範和 HTML5 定義了常見的大多數事件。即便有規範定義了基本事件,但不少瀏覽器仍然在規範以外實現了本身的專有事件,從而爲開發人員提供更多掌握用戶交互的手段。有些專有事件與特定設備關聯,例如移動 Safari 中的 orientationchange 事件就是特定關聯 iOS 設備的。

在使用事件時,須要考慮以下一些內存與性能方面的問題。

  • 有必要限制一個頁面中事件處理程序的數量,數量太多會致使佔用大量內存,並且也會讓用戶感受頁面反應不夠靈敏。

  • 創建在事件冒泡機制之上的事件委託技術,能夠有效地減小事件處理程序的數量。

  • 建議在瀏覽器卸載頁面以前移除頁面中的全部事件處理程序。

可使用 JavaScript 在瀏覽器中模擬事件。DOM2 級事件和 DOM3 級事件規範規定了模擬事件的方法,爲模擬各類有定義的事件提供了方便。此外,經過組合使用一些技術,還能夠在某種程度上模擬鍵盤事件。IE8 及以前版本一樣支持事件模擬,只不過模擬的過程有些差別。

關卡

憑理解和記憶手寫 EventUtil 通用類。

var EventUtil = {
    addHandler: function(element, type, handler){
        // 待補充的代碼
    },
    removeHandler: function(element, type, handler){
        // 待補充的代碼
    },
    getEvent: function(event){
        // 待補充的代碼
    },
    getTarget: function(event){
        // 待補充的代碼
    },
    preventDefault: function(event){
        // 待補充的代碼
    },
    stopPropagation: function(event){
        // 待補充的代碼
    }
};

更多

關注微信公衆號「劼哥舍」回覆「答案」,獲取關卡詳解。
關注 https://github.com/stone0090/javascript-lessons,獲取最新動態。

相關文章
相關標籤/搜索