《JavaScript 闖關記》之事件

JavaScript 程序采用了異步事件驅動編程模型。在這種程序設計風格下,當文檔、瀏覽器、元素或與之相關的對象發生某些有趣的事情時,Web 瀏覽器就會產生事件(event)。例如,當 Web 瀏覽器加載完文檔、用戶把鼠標指針移到超鏈接上或敲擊鍵盤時,Web 瀏覽器都會產生事件。如果 JavaScript 應用程序關注特定類型的事件,那么它可以注冊當這類事件發生時要調用的一個或多個函數。請注意,這種風格并不只應用于 Web 編程,所有使用圖形用戶界面的應用程序都采用了它,它們靜待某些事情發生(即,它們等待事件發生),然后它們響應。

請注意,事件本身并不是一個需要定義的技術名詞。簡而言之,事件就是 Web 瀏覽器通知應用程序發生了什么事情,這種在傳統軟件工程中被稱為觀察員模式。

事件流

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

事件流描述的是從頁面中接收事件的順序。但有意思的是,IE 和 Netscape 開發團隊居然提出了差不多是完全相反的事件流的概念。IE 的事件流是事件冒泡流,而 Netscape Communicator 的事件流是事件捕獲流。

事件冒泡

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

<!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>
  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() 中使用了相同的函數。

大多數情況下,都是將事件處理程序添加到事件流的冒泡階段,這樣可以最大限度地兼容各種瀏覽器。最好只在需要在事件到達目標之前截獲它的時候將事件處理程序添加到捕獲階段。如果不是特別需要,我們不建議在事件捕獲階段注冊事件處理程序。

IE9、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,獲取最新動態。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,461評論 6 532
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,538評論 3 417
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,423評論 0 375
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,991評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,761評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,207評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,268評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,419評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,959評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,782評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,983評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,528評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,222評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,653評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,901評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,678評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,978評論 2 374

推薦閱讀更多精彩內容