萬字長文總結DOM操做

0、前言

DOM操做是JS中的一大重點,本次回顧DOM操做,總結如文。javascript

儘管是一篇總結,但我儘可能把內容說清,邏輯理順,我的認爲讀起來仍是容易接受。對許多細節和注意點進行標註,須要展開的內容,也附上拓展連接。css

但願能合各位看官的口味,廢了好幾天,但願求各位一讚,你看收藏夾那麼多灰,不介意一塊兒分點吧?剛開始寫點博文,但願給點鼓勵。html

配套腦圖: html5

一、DOM基本概念

1.一、DOM、DOM節點與DOM樹

  • DOM:(Document Object Model, 文檔對象模型)是文檔內容(HTML或XML)在編程語言上的抽象模型,它建模了文檔的內容和結構,並提供給編程語言一套完整的操縱文檔的API。

本文面向HTML,不討論XML等文檔,下文中「文檔===HTML」。java

  • DOM節點:簡稱節點(Node),是DOM模型的組成單元。HTML的基本單元是標籤,節點經常與標籤對應,但連續的文本內容也是一個文本標籤
  • DOM樹:DOM樹是DOM結構的表示形式,DOM把文檔的每一個節點根據父子關係鏈接,造成DOM樹。

面試官:如何理解DOM?爲何須要DOM?node

1.二、節點、節點類型和節點類

  • 節點:前面提過,節點是DOM樹的組成單元。在JS看來,一個節點就是JS對象。下面用node表示任意的節點。git

  • 節點類型:並不是全部的節點都是同樣的,DOM規定文檔中有12種節點類型,分別用常量1 ~ 12(有與之對應的常量名稱Node.XXX_NODE)表示,能夠經過node.nodeType屬性獲取節點的類型常量。web

    如今,有些類型的節點已經棄用了,常見到的只有幾種類型的節點,包括:面試

    • 元素節點: 類型常量爲Node.ELEMENT_NODE1。最多見的一類節點,對應文檔中的元素。大部分DOM操做都是在元素節點層次的。編程

    • 文本節點:類型常量爲Node.TEXT_NODE2。對應文檔中的文本,任何文檔內容都有對應的文本節點,即便空格和換行符

      空格和換行不會對頁面內容產生影響,但它們確實以文本節點的形式存在於DOM樹中。

    • Document節點:類型常量爲Node.DOCUMENT_NODE9。它不對應文檔的內容,而是做爲文檔的入口節點,每一個文檔都有且僅有一個入口,由於這種獨特性,賦予一個特殊的變量名稱document

    • 註釋節點:類型常量爲Node.COMMENT_NODE8。它對應文檔中的註釋標籤,文檔的註釋內容也是可讀取和修改的。

  • 節點類:DOM內置許多節點類,類之間存在繼承關係,造成一套節點類框架。每一個節點對象都屬於節點類,擁有該類和其父類的方法與屬性,這使得操做節點十分簡單。節點類框架的一部分大概如圖:

    這些節點上有豐富的屬性和方法,是繼承的結果,能夠看到,一個HTML標籤元素至少有四層的繼承關係。

    <a>標籤爲例,它屬於HTMLAnchorElement類,得到了a.targeta.download等屬性,接着繼承了HTMLElement類上的title, hidden等屬性和click()等方法,又從Element類繼承了tagName, className等屬性和getAttribute(), setAttribute()等方法,再從Node類繼承了nodeType(前面說過的節點類型), appenChild(), removeChild()等方法,最後從EventTarget類中繼承了事件相關的屬性和方法。

不要混淆節點類型和節點類這兩個概念。前者是一個生活中的類別,後者是編程意義上的。節點對象的nodeType屬性表示了它的類型,而節點類是該節點的從屬的類。由於Dode是一個抽象類,因此,若是知道了某個節點從屬的類,咱們就知道它的節點類型。

區分節點與元素節點。咱們常常關心元素節點(簡稱元素),由於這是一類最常使用的節點,可是並不是全部節點都是元素。

1.三、探索DOM結構

前面的說法太抽象了,讓咱們用實際的例子看看文檔、DOM樹與節點的關係。

Live DOM Viewer是一個能夠根據HTML文檔實時查看DOM樹的網站。你把下面的例子複製過去,或者本身去探索。

一個簡單的HTML文檔:

<!DOCTYPE HTML>
<html>
<body>
  A simple text.
  <ol title="this is a title">
    <li>czpcalm</li>
    <!-- comment -->
  </ol>
</body>
</html>
複製代碼

它對應的DOM圖(顏色區分了節點類型):

留意這個圖,你要注意幾點:

  • 總共有4種類型的節點,分別是標籤節點(紅色),文本節點(灰色),註釋節點(黃色)和DOCTYPE節點(紫色)。

  • 文檔沒有<head>標籤卻出現了HEAD節點。這是由於HTML必然存在<html><head><body>標籤,不存在時會自動補上。順便一提,當出現<table>標籤時,也必定會有<tbody>標籤。

  • 文檔中的文本都會造成文本節點的內容,包括空格␣換行↵。第一,單獨的空格和換行都會造成對應的文本節點;第二,有內容的文本節點的值包含前導和後繼的空白。

    不是說HTML中的空白字符都被忽略嗎?怎麼這裏又說全都是有效的字符?

    在從文檔解析生成DOM樹的過程當中,HTML中的任何字符都是有效的;不過,在接下去的頁面渲染的過程當中,空白內容被忽略。因此從文檔到頁面的整過過程當中,空白確實被忽略了。

  • 理解DOM樹中的父子關係對應文檔中的包含(嵌套)關係。一個極佳的類比是文件樹,把元素看作文件夾,文本看作文件,文件夾中能夠存放文件和新的文件夾,而後一層層深刻下去,DOM樹也是如此。

幾個原則能夠幫咱們快速理解這個DOM樹的構建:

  • 自動補全, 上面提到的自動添加必要元素,另外,還會自動補齊缺乏的關閉標籤。
  • 文檔有的DOM樹都有。這個原則要求空白也會有。不過,做爲補充的,<head>前面的空白會被忽略(歷史緣由)。
  • <body>後若是有內容,會被移到<body>裏面。

二、導航與搜索

操做節點前,先要找到節點。導航是從一個節點到另外一個節點;搜索是從一個範圍中選出知足條件的節點。

2.一、節點導航

Node的類規定了節點具備的許多屬性,方便咱們從某個節點中找到跟它相關的另外節點。

頂級節點通常直接獲取:

  • document -- 入口節點。
  • document.documentElement -- HTML節點。
  • document.head -- head節點。
  • document.body -- body節點。

node節點,有如下屬性:

  • node.parentNode -- 獲取節點的父節點。

  • node.previousSibling -- 獲取節點的上一個兄弟節點。

  • node.nextSibling -- 獲取節點的下一個兄弟節點。

  • node.childNodes -- 獲取節點的孩子節點列表。沒有子節點返回空列表。

  • node.firstChildnode.lastChild -- 獲取第一個和最後一個孩子節點,同node.childNodes[0]node.childNodes[node.childNodes.length-1]

助記:都是兩個單詞拼寫的。

這些是屬性,不是方法。不要錯誤使用bode.childNodes()之類的。

以上導航是基於節點的,包括元素節點、文本節點、註釋等。如1.3中的例子,document.body.firstChild獲得的是A simple text所在的文本節點。

由於咱們常常只關心元素節點,DOM也提供了一組純元素的導航屬性, 對元素elem或者節點node, 有:

  • node.parentElement -- 父元素節點,該屬性來自Node類。
  • elem.previousElementSibling -- 上一個兄弟元素
  • elem.nextElementSibling -- 下一個兄弟元素
  • elem.children -- 孩子元素列表
  • elem.firstElementChildelem.lastElementChild,第一個和最後一個孩子元素節點

助記:children特殊,其它都有Element,沒有Node。

解惑:node.parentNodenode.parentElement有區別嗎?父節點不該該都是元素節點嗎?通常狀況下,兩者等效,但html.parentNode === document, document不是元素節點。

提醒:上面的導航屬性都是隻讀屬性。elem.parentElement = anotherElem; 是錯誤的

2.二、搜索節點(重點)

常常地,咱們老是直接從文檔中找出知足某些條件的元素,從而獲取到目標元素。

2.2.一、document/element.getElementBy*()系列

getElementBy*系列方法大家確定不陌生,但我想要提醒的是,注意個人寫法document/element.getElementBy*(),表示這是兩個不一樣的類上的方法(參看上面節點類)。

首先,是兩個來自Document類的方法:

  • document.getElementById(id) : 根據id獲取文檔中的元素。
  • document.getElementsByName(name): 根據name獲取文檔中的元素。不多使用。

其次,是Document類和Element類都具備的方法:

  • document/element.getElementsByTagName(tagName): 根據標籤名稱獲取文檔或某個元素內的元素。
  • document/element.getElementByClassName(className):根據類名獲取文檔或某個元素內的元素。

注意:沒有element.getElementById(id)

注意:不要忘記或多加了s。除了document.getElementById(),其它方法的返回結果都是一個集合,沒有知足條件的元素則是空集合。

2.2.二、document/element.querySelectorAll/querySelector()

如今,推薦使用一組更強大的搜索新方法, 它們支持CSS選擇器:

  • document/element.querySelectorAll(CSSSelector):返回知足選擇器的一組節點列表。
  • document/element.querySelector(CSSSelector): 返回第一個知足選擇器的元素。

補充:elem.matches(selector)能夠檢查某個元素是否與選擇器匹配。

2.2.三、兩組搜索方法的對比

querySelector*源自Selectors API 規範,與CSS選擇器結合,更加靈活,強大,已被全部現代瀏覽器支持。是如今比較推薦的作法。

getElementBy*()源自DOM2標準,被認爲是傳統接口,老項目中普遍使用。或者須要兼容IE8以前使用。可是有較好的性能,如今仍然有人在用。

補充:做爲結果的集合。在以集合返回結果的時候,或者返回一個NodeList對象,或者返回一個HTMLCollection對象,通常來講,兩者都是可迭代的類數組對象,能夠經過下標範圍或for...of遍歷。

注意:大部分結果集,包括getElementBy*node.childNode等,都是動態的,即便查詢並保存,以後若是修改文檔,該結果集也會隨之變化。但querySelectorAll()返回的結果是靜態的。

2.2.四、特殊的搜索方法

有一些特殊的元素搜索方法,就我所知有:

  • elem.closest(selector) -- 在elemd的父元素上查找最近一個知足選擇器的元素。
  • document.elementFromPoint(clientX,clientY) -- 返回相對當前視口座標嵌套最深的(最上方)的元素。

三、節點操做

這一節涉及對某個節點的操做,通常不會引發文檔內容(DOM樹)的變化。

3.一、通用節點操做

這些操做基於**Node**接口,對全部節點都是通用的。

  • 判斷節點類型node.nodeTypenode instanceof <NodeClass>。二者均可以用於判斷節點類型,當須要明確的節點類的時候,只能經過後者。

    node.nodeType === Node.ELEMENT_NODE; //或node.nodeType === 1;
    node instanceof Element;       //與上面等效
    node instanceof HTMLInputElement;   //判斷是不是輸入元素
    複製代碼
  • 獲取節點名稱:node.nodeName,對於元素節點,返回對應的標籤名稱,如audio。對其它類型節點,返回#與節點類型字符串,如#text, #comment,#document。也能經過節點名稱判斷節點類型,但基本不用。

  • 獲取或設置節點值node.nodeValue文本節點或註釋節點返回文本內容,元素節點與document節點返回null。讀寫屬性,支持node.nodeValue = "A simple text"。一樣的,空白文本也被包含在內容裏。

    提醒:文本節點和註釋節點有一個data屬性,使用與nodeValue相同,但它不是在Node接口上的。

  • 判斷節點是否擁有子節點:node.hasChildNodes(),返回true當節點有子節點時。

  • 判斷節點是否擁有特定子節點:node.contains(childNode),返回true當childNode是node的子節點。

3.二、元素節點操做(重點)

大部分狀況下咱們都是在元素節點上操做它的文本子節點,因此元素是咱們最關心的節點,**Element**接口提供了更多的屬性和方法。這裏只考慮HTML元素。

解惑:那何時須要操做文本節點?會看1.3節的例子,A simple text的父節點是body,但body有其它元素節點。假如沒有文本這種類型的節點,很難在body節點上只修改A simple text的內容。

  • 判斷元素類型:elem.tagNameelem.nodeName,效果同樣,返回標籤的字符串,如audio。另外,使用instanceof能夠實現不一樣級別的類型判斷。

  • 元素內容:有幾個屬性和元素內容相關:

    • elem.innerHTML -- 獲取或設置元素內的HTML片斷。設置的內容會被當成HTML片斷解析,可能會引發文檔結構的變化。

      注意:HTML片斷內的腳本不會執行。

    • elem.textContent -- 獲取或設置元素的文本內容(標籤被忽略)。設置的文本以安全模式(不會被解析)寫入。

    使用區別請參看實驗探究innerHTML,innerText,textContent的使用區別

3.2.一、元素的特性和屬性

特性(attribute)是指html中寫在標籤內的特性,而屬性(property)是隻元素節點做爲編程對象具備的屬性。

  • 特性 -- 屬性同步機制:對標準規定的特性,元素對象具備響應的讀寫屬性, 如a.href。這種機制極大的方便了在JS中獲取或修改元素的特性。

    提醒:對不一樣HTML元素,規定的特性不一樣,屬性也就不一樣,如: 存在a.href但不存在div.href

  • 通用的特性操做接口:

    • elem.hasAttribute(name) -- 檢查是否存在某個特性。
    • elem.getAttribute(name) -- 獲取某一特性的值。
    • elem.setAttribute(name, value) -- 設置某一特性。
    • elem.removeAttribute(name) -- 刪除某一特性。
    • elem.attributes() -- 獲取全部的特性對,每一個特性對具備name,value屬性。
  • 特殊的data-*data-*特性是一種合法且安全的傳遞自定義數據的方式。可經過elem.dataset.name讀取或修改data特性的值。屬性名稱採用駝峯寫法,如elem上的data-apple-price對應elem.dataset.applePrice

3.2.二、元素的類和樣式

修改樣式有兩種方式,一是把樣式寫到某個類裏,而後在代碼中修改元素的類,一是直接修改elem.style.*。前者適用於隨狀態改變樣式的狀況,在代碼可維護性上更加,用得較多。後者適用於頻繁計算或切換的樣式。

  • elem.classList: 一個包含elem全部類的可迭代的類數組對象。這個對象有幾個方法,方便咱們改變元素的類。
    • elem.classList.contains(class) -- 檢查是否有某個類。
    • elem.classList.add(class) -- 添加某個類。
    • elem.classList.remove(class) -- 移除某個類。
    • elem.classList.toggle(class) -- 切換某個類,若是有就刪除,沒有就添加。
  • elem.className: 一個讀寫屬性,把元素的class特性當成一個總體看待。如HTML中某個元素elem存在特性class="first red",那麼elem.className的值爲first red

區分:經過elem.classListelem.className均可以對元素的類進行改動,前者更加靈活,且具備相應的方法,後者是一個總體的字符串屬性,適合刪除全部的類從新設置。

若是須要直接設定元素的樣式,能夠設置elem.style.*

elem.style.width = "20px";     //設置元素寬度
elem.style.backgroundColor = "red";     //設置顏色
複製代碼

提醒:多詞CSS屬性的轉化方式也是駝峯寫法,如padding-left對應elem.style.paddingLeft-webkit-border-radius對應elem.style.WebkitBorderRadius-意味着大寫。

補充:常用node.hidden = true實現節點隱藏,也能用於隱藏文本,在元素上,它的效果與elem.style.display:none一致。

這種方式其實是經過元素上的style特性實現的,它的優先級高於通常的CSS,因此都是有效的,除非你在其它地方用了!importantelem.style.*也是可讀取的,**可是它們只會讀取元素中style特性存在的屬性,**對其它CSS是無效的。但咱們每每須要的是最終應用在元素的樣式數值。這種時候,須要使用

  • getComputedStyle(elem) :獲取元素最終應用的樣式,它返回一個樣式對象,好比,可經過getComputedStyle(elem).backgroundColor獲取顏色值。

注意:getComputedStyle不支持簡寫屬性,如getComputedStyle(elem).padding是無效的。

<style> div { font-size: 20px; } </style>
<div id="div" style="padding:20px;">
    czpcalm
</div>
<script> console.log(div.style.fontSize); //"" style特性上沒有該CSS屬性,因此爲空 console.log(div.style.padding); //20px console.log(getComputedStyle(div).fontSize); //20px </script>
複製代碼

若是須要清除代碼設定的樣式, 能夠把elem.style.cssAttr設爲""(空字符串),反作用是原來html中的特性樣式也會被清除。

還有一種基本不會使用的重寫整個元素style特性的方式elem.cssText = csstext,csstext不包含選擇器和花括號。

3.2.三、元素的位置和尺寸

當設計元素的大小變化或位置移動時,咱們須要獲取元素的位置或尺寸。設置則用CSS方式設置。

位置是相對於參照物的,一個元素,有相對於定位父元素相對於視口相對於文檔三種關係位置。

提醒:定位父元素是指CSS定位元素(position爲relative,absolute,fixed)或td,th,table元素,或者是body元素。

  • 相對於定位父元素:elem.offsetLeft/offsetTop ,相對於參照父節點的左/上邊距。elem.offsetParent獲取元素的定位父元素。

  • 相對於視口:**elem.getBoundingClientRect()**獲取元素的定位矩形elemRect

    • elem.getBoundingClientRect().left/top/right/bottom分別表示元素盒子(含邊框)四角到視口左或上邊的距離。
    • elem.getBoundingClientRect().width/height, 與elem.offsetWidth/offsetHeight等效,盒子的寬高。
    • elem.getBoundingClientRect().x/y ,通常狀況下,與elem.getBoundingClientRect().left/top等效,且不兼容IE,不推薦使用。
  • 相對於文檔:沒有直接獲取的方式,但能夠經過相對於視口+滾動距離簡單計算。

    • 盒子上方相對於到文檔的距離:elem.scrollTop + elem.getBoundingClientRect().top
    • 盒子左邊到文檔的距離:elem.scrollLeft + elem.getBoundingClientRect().left

元素盒子的尺寸也有多種狀況,須要考慮邊框、內邊距、是否爲標準盒子模型、甚至是否有滾動條。

  • 含邊框的盒子尺寸:elem.offsetWidth/offsetHeight或者elem.getBoundingClientRect().width/height均可以獲取含邊框的寬高。

  • 邊框寬度:elem.clientLeft/clientTop -- 元素左上角邊框的寬度。能夠理解爲左邊框寬度和上邊框的寬度,若是存在滾動條,也包含滾動條的寬度。

    補充:通常來講,上邊框和左邊框是經常使用的。若是四條邊框寬度不一,能夠經過getComputedStyle(elem).borderRight獲取,注意這是含單位的字符串。

  • elem.clientWidth/clientHeight -- 內容寬度高度,包含padding,不包含滾動條。

  • 不含邊框:elem.clientWidth/clientHeight,獲取元素盒子內容寬高,不含邊框和滾動條,含padding。

  • 內邊距問題與盒子類型:在涉及內邊距的時候,須要getComputedStyle(elem)方法獲取,而且須要考慮是否爲標準盒子。

若是元素的內容存在滾動時,咱們可能須要知道與滾動相關的尺寸:

  • elem.scrollLeft/scrollTop -- 水平和垂直方向上滾動的長度。可寫屬性,經過設置該值改變控制內容滾動。

  • elem.scrollWidth/scrollHeight -- 元素內容的長度和寬度,包括滾動的內容。

    區分:在沒有內容溢出發生滾動時,clientWidth/HeightscrollWidth/Height等效;存在滾動時,前者是盒子的可視內容大小,後者是內容的大小,包括須要滾動查看的部分。

關於位置和尺寸,沒有圖片難以說清,不理解請參看:

四、修改文檔(重點)

DOM操做中,常常須要修改文檔結果或內容。這類操做涉及節點的插入、移除、替換等。

文檔的操做有一套普遍使用的傳統方法。也有一套新推出的API方法,它們更加靈活易用,但IE不兼容。

4.一、插入節點

插入節點能夠分三步走:

  1. 建立一個節點:

    • 建立一個元素節點: let elem = document.createElement(tagName)
    • 建立一個文本節點:·let text = document.createTextNode(data)
    • 從已有節點克隆:let dupNode = node.cloneNode(deep), deep爲true表示深拷貝,經常使用。默認爲false。
  2. 編輯節點的屬性和內容

  3. 把節點插入文檔樹中

    • 傳統方式:傳統方式須要在父節點上執行對節點的插入

      • parentNode.appendChild(node) -- 把node做爲最後一個子節點插入。
      • parentNode.insertBefore(node, nextSibling) -- 在nextSibling以前插入node。
    • 現代方式:能夠實現多位置插入,你能夠在父節點上執行插入或在兄弟節點上執行插入。

      • parentNode.prepend(...nodes or strings)。在第一個子節點以前插入。
      • parentNode.append(...nodes or strings)。在最後一個子節點以後插入。
      • nextSibling.before(...nodes or strings)。在本節點以前同級插入。
      • previousSibling.after(...nodes or strings)。在本節點以後同級插入。

      提醒:參數的形式說明它們支持一次插入多個,而且字符串會做爲文本節點插入。

有一種狀況,咱們但願直接描述節點的插入HTML代碼段,這種時候,可使用以前的elem.innerHTML屬性, 或者使用**elem.insertAdjacentHTML(position, html)**進行插入。其中,position的可選值有:"beforebegin", "afterbegin", "beforeend", afterend.

區分:節點對象存在不一樣於節點在文檔樹中。前者只是在代碼中能夠經過node等變量名稱引用節點,但建立一個節點,並不會對文檔樹結構有影響,只有插入文檔樹後,節點才成爲文檔的一部分。反之,從文檔中移除節點,並不會致使節點變量實效,它仍是存在,能夠繼續被修改,並在某個時間點從新插入文檔中。

4.二、移除節點

從文檔樹中移除更加簡單。

  1. 找到須要移除的節點。參考節點導航與搜索小節。
  2. 移除節點:node.remove(),移除節點。但IE不兼容,須要使用傳統方式,獲取其父節點,在父節點上移除子節點:node.parentNode.removeChild(node)

4.三、替換節點

與移除相似,使用node.repalceWith(...nodes or strings),一樣IE使用parentNode.replaceChild(newNode, node)

五、事件處理

5.一、基本概念

  • 事件是某事發生的信號, 全部的DOM對象都具備這些信號。
  • 事件處理程序是當一個事件信號發生時運行的函數,用於對事件做出響應。
  • 事件處理就是爲事件分配正確的處理函數,在事件發生時做出正確處理。
  • 事件類型 是事件的分類,常見的事件類型有
    • 鼠標事件:click, contextmenu, dbclick, mousedown, mouseup,mouseover, mouseout等。
    • 鍵盤類型:keydown, keyup
    • 焦點事件:focus, blur, focusin, focusout,
    • 表單事件: submit, reset
    • 剪切板事件: cut, copy, paste
    • 資源事件:load, unload, error, abort
  • 事件對象是事件在編程上的對象,具備不少和事件相關的屬性。經常在處理函數調用時自動做爲參數傳入。

5.二、事件流

**假設一個div元素中放了一個button元素,若是點擊了button,算不算點擊了div?若是算,是先點擊了button仍是先點擊了div?**由於元素的嵌套關係,使得事件的產生對象每每是嵌套的,而不是獨立的,在上面的中,還有body元素,html元素,甚至整個文檔document。

第一個問題,對大多數的事件來講,應該是確定的,一個在子元素上發生的事件,也看作在該元素上發生了。

第二個問題,也被當作事件流問題,即如何肯定事件在節點中的傳播順序。巧的是,當年IE和Netscape分別提出了事件冒泡事件捕獲這兩個幾乎徹底相反的概念。

  • 事件冒泡:事件從最小的發生對象開始依次往外圍對象冒泡。即button->div->body->html->document
  • 事件捕獲:事件從整個文檔開始依次向最小目標捕獲。即document->html->body->div->button

爲了統一這兩種觀點,DOM事件規範提出了3階段的事件流:事件捕獲階段->目標階段->事件冒泡階段

然而,事件捕獲階段不多被使用,下面的討論會忽略事件捕獲,默認事件從目標上開始冒泡。

注意:並不是全部事件都會冒泡。

5.三、事件處理函數

5.3.一、3種事件處理方式

有3種爲設置事件處理函數的方式:

  1. HTML特性處理:在HTML元素標籤中使用on<event>特性。
  2. DOM0級事件處理:把處理函數賦給節點的對象的on<event>屬性。
  3. DOM2級事件處理:使用node.addEventListener(event, handler, capture)removeEventListener(event, handler)capture是一個布爾值,表示是否在捕獲階段響應。

對比:HTML特性的方式具備很大的侷限性,代碼量限制,維護性差,某些事件不支持等。DOM0級事件也對某些事件不支持,關鍵在於沒法爲一個事件類型分配多個處理函數。DOM2解決了這些問題,是最通用的方式,推薦使用。

<div id="div1" onclick="alert(event.type+': div1')">div1</div>
<div id="div2" onclick="handler('div2',event)">div2</div>
<div id="div3">div3</div>
<div id="div4">div4</div>
<script> function handler(message, event) { alert(event.type + ": " + message); } div3.onclick = e => handler("div3", e); //處理函數具備多參數,須要嵌套調用。 div4.addEventListener("click", e => handler("div4", e), false); </script>
複製代碼

上面的例子中, div1和div2使用了HTML特性添加事件處理,div3使用DOM0級事件處理,div4使用DOM2級事件處理。

HTML特性的內容會成爲事件處理函數的內容,DOM事件處理賦值的是函數對象。好比對一個處理函數func,特性上的寫法是onclick="func()",而DOM0級事件處理的寫法是node.onclick=func。區分它們的主要點在於理解特性的內容會被套上一個函數外殼,而後成爲處理函數。onclick="func()"最後的效果是:

node.onclick = function(event){   //傳遞的事件參數名爲event
    func();         //這一行是來自特性
}
複製代碼

這樣,你應該能明白爲何特性上是函數調用,而DOM上是函數對象賦值了。

由於這種設定,還產生兩個要注意的點。

第一是事件對象參數event,若是你習慣了在DOM事件處理中使用變量名稱e而不是event,那麼在特性內容上可能寫出相似e.type的表達,這是錯誤的,由於特性傳入的事件變量名是event

第二,在試圖阻止事件的默認響應的時候,可能會寫出這樣的代碼:

<a href="www.baidu.com" onclick="handler()">百度</a>
<script> function handler(){ /*....處理點擊事件*/ return false; //返回false阻止跳轉 } </script>
複製代碼

然而點擊以後仍是發生跳轉了,問題出在哪裏?讓咱們看一下最後生成的處理函數:

a.onclick = function(event){
    handler();
}
複製代碼

事件處理函數只是調用了handlerhandler返回了false,而事件處理函數並無理會,因此,外部看來,事件處理函數沒有返回值。正確的寫法應該是onclick="return handler()"或者onclick="handler();return false;"

DOM2級事件處理以前,一個事件只能對應一個事件處理函數。若是你想解除事件處理函數,能夠簡單的採用node.on<event>=null的方式。

DOM2node.addEventListener()支持添加多個處理函數,這些處理函數的執行順序與添加順序一致,可使用node.removeEventListener(event, handler)解除處理函數。

注意:removeEventListener(handler)必須使用添加時的同一個函數,而不是具備相同執行體的函數。

node.addEventListener("click", ()=>alert("直接添加函數表達式將沒法別移除!"), false);
let handler = ()=>alert("something")
node.addEventListener("click", handler, false);
node.removeEventListerner("click", ()=>alert("somthing"));    //無效,傳入的是不一樣的函數對象
node.removeEventListener("click", handler);         //成功移除
複製代碼

5.3.二、事件處理函數的參數

讓咱們看另外一個問題,事件處理函數的參數問題。

咱們只編寫事件處理函數,函數在事件發生時自動被調用,它的調用語句相似這樣

node.handler(event);   //event在這裏是實參,這句語句是函數調用
複製代碼

咱們要根據調用規範本身的handler函數。

調用時傳入了一個參數,根據JS函數參數的特色,若是在事件處理函數中沒有使用到event,那處理函數能夠不聲明這個參數。其次,形參可使用任何合法變量名,咱們能夠不函數定義寫成handler(e),而後在函數內使用e而不是event

它的調用方式決定了handler最多隻能有一個參數。但在事件處理時確實有須要多個參數的時候,這種時候要使用嵌套的函數調用,如上面的例子。

最後,注意這種調用方式使得函數內部this === node。這頗有用,方便咱們在事件處理的時候獲取到事件發生對象的信息。

5.四、事件對象

事件對象event在調用事件處理函數時自動傳入,它具備不少屬性和方法,在事件處理的時候大有用處。

  • event.preventDefault() -- 取消事件的默認行爲,只有event.cancelable爲true時纔有效。

    補充:若是使用on<event>方式添加處理函數,在函數返回false也能夠取消默認事件。

  • event.stopPropagation() -- 中止事件的繼續冒泡,上層的事件響應不會再發生。

  • event.stopImmediatePropagation() -- 中止事件繼續響應。包括事件冒泡和當前目標的其它處理函數也不會發生。

  • event.type -- 事件類型字符串,在使用一個處理函數處理多類事件時,能夠判斷當前發生的事件類型。

  • **event.targetevent.currentTarget **-- 事件的最小目標和事件的當前目標。

    區分:事件在最小目標上發生,而後往上冒泡的過程當中,event.target始終不變,指向最小目標,但event.currentTarget === this,會隨着冒泡過程指向當前正在處理的節點。

  • event.phase -- 事件流階段,整數,1表明捕獲,2表明目標階段,3表明冒泡階段。

  • event.bubbles -- 布爾值,事件是否冒泡。

  • event.cancelable -- 布爾值,事件是否可取消默認行爲。

  • event.trusted -- 布爾值,若是事件是瀏覽器發生的,爲true,若是事件是js代碼發生的,爲false。

上面的屬性都是只讀的。

這些屬性和方法是通用的,對具體的事件類型,有更多的事件屬性和方法,將在每種事件中詳細說明。

5.五、鼠標事件

鼠標事件是最多見的一類事件,有:

  • click -- 鼠標左鍵點擊觸發,或觸摸屏的點擊。

    提醒:點擊一次的含義是鼠標在目標上按下並鬆起,若是鼠標按下後滑動到元素外部鬆起,或者元素位置變化致使鼠標鬆起時再也不元素上方,不能造成有效點擊。

  • contextmenu -- 鼠標右鍵點擊事件,該事件瀏覽器通常有默認的菜單,若是須要實現自定義菜單,須要阻止默認行爲。

  • dbclick -- 雙擊鼠標左鍵。雙擊具備選擇文本的默認行爲。

  • mousedown/mouseup -- 鼠標任意鍵按下/鬆起。

  • mouseenter/mouseleave -- 鼠標進入/離開元素,不會冒泡。

  • mouseover/mouseout -- 鼠標進入/離開元素,會冒泡,進入/離開子元素時也會觸發。

    區分:mouseenter/mouseleavemouseover/mouseout事件相似,可是後者會冒泡,且在進入子元素會觸發mouseout

  • mousemove -- 鼠標按下後鬆起前發送移動。

鼠標點擊時,會發生一系列事件,它們具備特定的順序,以某次雙擊爲例,依次觸發事件mousedown->mouseup->click->mousedown->mouseup->click->dbclick

鼠標事件有一些適合獲取事件相關信息的屬性:

  • 鼠標按鍵:按鍵屬性只對mousedown/mouseup有意義,event.button,數字1~5,表明鼠標上的按鍵,依次是:鼠標左鍵,中鍵,右鍵,前進,後退。

  • 座標:它們是事件發生時刻(定點類)的座標,或者實時的(mousemove)座標。

    • event.pageX/pageY -- 相對於文檔的座標。
    • event.clientX/clientY -- 相對於窗口的座標。
    • event.screenX/screenY -- 相對於屏幕的座標,較少使用。
  • 組合鍵:在鼠標事件發生時,若是下列按鍵被按下,對應的屬性爲true。用於在一個事件類型上綁定多種任務。

    • event.shiftKey -- shift鍵是否被按下。
    • event.ctrlKey -- ctrl鍵是否被按下。
    • event.altKey -- alt鍵是否被按下。
    • event.mateKey -- cmd鍵(Mac專用)是否被按下。

    提醒:若是想處理ctrl鍵,應該注意在Mac下使用cmd鍵,因此應該判斷if(event.ctrlKey||event.cmdKey)

  • 相關目標:event.relatedTargetmouseenter/mouseleavemouseover/mouseout事件的屬性。若是鼠標從divA->divB,在divA上發生mouseout(mouseleave),event.target === divA而且event.relatedTarget === divB。相反,在divB上發生mouseover(mouseenter),event.target ===divBevent.relatedTarget === divA

這是一篇鼠標拖放事件的文章,應該能對鼠標事件的使用有所幫助。

5.六、鍵盤事件

鍵盤事件經常被用於建立各類熱鍵。如今普遍使用的有兩類鍵盤事件:

  • keydown -- 按下任意鍵盤觸發。持續按住按鍵會持續觸發該事件。
  • keyup -- 鬆開任意按鍵。

補充:不要試圖監聽Fn鍵,它是在比OS更低的級別上實現的,沒有鍵盤事件。

補充:有一個被取消的keypress事件,這裏不討論。

鍵盤事件最重要的屬性是鍵碼,咱們經常須要獲取鍵盤的鍵碼,根據按下的鍵盤作出響應。

  • event.code -- 鍵碼。鍵碼是惟一的。在按鍵判斷時,常用的屬性。鍵碼是字符串,常見規則:

    • 數字鍵碼爲Digit<num>Numpad<num>(小鍵盤)。如Digit2, Numpad2
    • 字符按鍵Key<letter>, 如KeyZ, KeyA
    • 功能鍵通常爲按鍵名稱,如F4, Tab,Enter
    • 使用Left/Right區分左右,左shift的鍵碼爲ShiftLeft, 右shift的鍵碼爲ShiftRight

    補充:一個被廢棄的event.keyCodeevent.code有一樣的功能,但鍵碼是基於數字的。可能在兼容IE時須要用到。

  • event.key -- 鍵。表徵按鍵的含義而不是位置。跟是否按下shift、鍵盤語言有關。如按下z鍵時表示字符z,而shift+z表示字符Z。左右shift的key也同樣。只在使用基於意義的時候才使用event.key判斷。

鍵盤事件也一樣支持組合鍵,可使用event.ctrlKey, event.shiftKey, event.altKey, event.metaKey獲取其它按鍵是否被按下,而不用單獨監聽它們。

5.七、資源事件

有一類與資源相關的事件,認識它們的最好方式是經過頁面生命週期:

  • DOMContentLoaded -- 文檔被加載而且DOM樹構建完成後(圖片、樣式可能還未加載)。
  • load -- 頁面內容加載完畢,全部圖片、樣式資源都已應用。此時進行的操做都是安全的。
  • beforeunload -- 用戶正打算離開,能夠在此時訊問用戶是否離開,或者保存一些數據。
  • unload -- 用戶已經離開,但仍能夠進行少許操做,如釋放資源,發送數據。

load事件是這裏最重要的一個事件。它不只能夠用於確保頁面加載完成後執行任務,還能夠添加到img, script, style這些節點中,在它們完成資源加載時候進行事件處理。

六、番外篇:表單操做

6.一、表單導航

表單是一類特殊的元素,須要頻繁的獲取或修改,因此,通用的節點獲取方式上,增長了額外的屬性,方便表單操做。

  • document.forms -- 獲取文檔中的表單元素集合。這是一個命名集合。命名意味着能夠經過表單名稱方式document.forms.formName獲取表單,而集合自己又支持下標方式document.form[0]

  • form.elements -- 獲取表單form的輸入元素集合,一樣,這是一個動態的命名集合。能夠經過form.elements.inputName或者form.elements[index/inputName]獲取。

  • form.inputNameform[index/inputName] -- 獲取表單form的輸入元素。form.elements.inputName的簡寫。

    注意:

    (1)radio、checkbox等多個輸入共享一個name時,form.elements.inputNameform.inputName返回一個集合。

    (2)沒法獲取到type爲image的輸入組件。

    (3)若是表單中有<fieldset>(輸入組),它會成爲一個form.elements的一個元素,可進一步經過fieldset.elements獲取表單組中的輸入控件集合。

  • input.formName -- 獲取輸入元素所在的表單。form.inputName的反向引用。

<form name="myForm">
  	<input type="text" name="user"/>
      <input type="radio" name="sex" value="male"/>
      <input type="radio" name="sex" value="female"/>
  </form>
  <script> let form = document.forms.myForm; //或者document.forms[0] let userInput = form.user; //或者form.elements.user let sexInput = form.sex; sexInput[0].checked = true; //選擇第一個radio console.log(sexInput.value); //male </script>
複製代碼

6.二、表單基礎

根據屬性--特性同步,表單對象有如下屬性,它們與對應的特性意義相同。

  • form.action
  • form.method
  • form.name
  • form.target

另外,表單有兩個經常使用的方法:

  • form.submit() -- 手動提交表單
  • form.reset() -- 初始化表單內容

一個表單含有多個輸入字段,它們能夠是各種型的input標籤select標籤textarea標籤button標籤。每這些表單字段有一些通用的屬性(固然還有其它的同步屬性):

  • input.value -- 字段的值。對type="file"的input,它是隻讀的,表示計算機上的文件地址。
  • input.name -- 字段名稱。
  • input.type -- 字段類型,對textarea"textarea"。對select"select-one""select-multiple"
  • input.tabindex -- 字段的tab索引。
  • input.disabled -- 字段是否被禁用。
  • input.readOnly -- 字段是否只可讀。

這些屬性都是可寫的,大大便利了咱們對錶單元素的操做。

最關鍵,要數input.value,對不一樣字段,它有不一樣的用法:

  • button, reset, submit類型:表示用於顯示的字符串。
  • radio, select(單選)類型:一個與被選中字段value相同的值。
  • textboxselect(多選)類型:一個數組,對應被勾選的選線的值。
  • 其它類型:與填寫的值一致。

6.三、表單事件

有些事件是表單特有的:

  • ·submit -- 表單提交發生的事件。
  • reset -- 點擊reset按鈕時。
  • input -- 在字段輸入任意內容後。
  • change -- 字段失去焦點後,與以前輸入發生變化時。

此外,也有一些事件在表單上發揮了主要做用:

  • pressdown

  • focus/blur -- 得到或焦點時,不會冒泡。

  • focusin/focusout -- 得到或失去焦點時,與上相似,可是會冒泡。

  • cut/copy/paste -- 剪切板事件

    補充:能夠經過e.preventDefault()(或返回false)取消剪切板事件默認行爲,來禁用頁面/輸入的剪切板功能。event.clipboardData可用於讀寫剪切板內容。

    input.addEventListener("past", (event)=>{
        let data = e.clipboardData;
        alert("Data in clipboard: " + data+". But you can't paste!");
        return false;
    })
    複製代碼

6.四、表單驗證與表單提交

表單客戶端驗證幾乎成爲表單最重要的一個功能,這但是JS誕生的原因呀!

html5提供了一套表單的驗證API,但我的認爲難用且界面依賴瀏覽器,使用很少,更可能是自定義表單的驗證方式。

表單驗證主要有兩種思路,一是在客戶完成每一個字段輸入時,獨立驗證,每一個字段,全部字段都經過後才容許提交操做。這種方式主要利用change, input等事件。

一是在用戶發起提交操做時,對整個表單進行驗證,若是不經過則提醒用戶更正,阻止表單提交,直到全部字段正確。這種方式主要使用submit事件。

七、後記

累了,貼個圖總結吧。

花了我好幾天,一邊學習,一邊整理。感謝閱讀!

相關文章
相關標籤/搜索