簡(jiǎn)介
What
雖然網(wǎng)頁內(nèi)容使用各種標(biāo)簽平鋪而成的,但是在瀏覽器中它們卻不是同樣的平鋪結(jié)構(gòu),而是被解析為樹形結(jié)構(gòu),所有的標(biāo)簽按照層級(jí)關(guān)系建模構(gòu)建成一棵樹,這棵樹就被稱為“文檔對(duì)象模型”樹,英文為Document Of Object,簡(jiǎn)稱DOM樹。
Why
為什么要這么做?一方面是因?yàn)椤懊嫦驅(qū)ο蟆本幊痰膹?qiáng)大,幾乎所有在操作系統(tǒng)之上構(gòu)建的大型軟件都離不開這一思想和工具,另一方面是因?yàn)闃湫谓Y(jié)構(gòu)是對(duì)一切復(fù)雜層級(jí)系統(tǒng)的抽象,一本書的結(jié)構(gòu)、一個(gè)家族的人員關(guān)系、一個(gè)公司的上下級(jí)關(guān)系、甚至一個(gè)國家的治理模式,實(shí)際上都是一種樹形結(jié)構(gòu)。
所以,基于這些思想和工具來構(gòu)建復(fù)雜文檔的結(jié)構(gòu)也就可以理解了。
DOM API分類
DOM API就是DOM樹開放給開發(fā)者的接口,是對(duì)系統(tǒng)能力的延伸,大致會(huì)包含4個(gè)部分。
- 節(jié)點(diǎn):DOM樹形結(jié)構(gòu)中的節(jié)點(diǎn)相關(guān)API;
- 事件:觸發(fā)和監(jiān)聽事件相關(guān)API;
- Range:操作文字范圍相關(guān)API;
- 遍歷:遍歷DOM需要的API。
節(jié)點(diǎn)
主要的DOM樹節(jié)點(diǎn)的繼承關(guān)系如下:

使用代碼驗(yàn)證:
alert(document.body.constructor.name) // "HTMLBodyElement"
alert(document.body) // [object HTMLBodyElement]
document.body instanceof HTMLBodyElement // true
document.body instanceof HTMLElement // true
document.body instanceof Element // true
document.body instanceof Node // true
document.body instanceof EventTarget //true
HTMLInputElement.prototype.__proto__ === HTMLElement.prototype //true
HTMLAnchorElement.prototype.__proto__ === HTMLElement.prototype // true
完整的繼承關(guān)系如下所示:

Node
Node是DOM樹繼承關(guān)系的根節(jié)點(diǎn),相當(dāng)于一個(gè)“抽象類”,提供了操作DOM的基礎(chǔ)能力,主要有下面這些API:
- 關(guān)系節(jié)點(diǎn)
-
parentNode,父節(jié)點(diǎn); -
childNodes,孩子節(jié)點(diǎn); -
firstChild,第一個(gè)孩子節(jié)點(diǎn); -
lastChild,最后一個(gè)孩子節(jié)點(diǎn); -
nextSibling,下一個(gè)兄弟節(jié)點(diǎn); -
previousSibling,上一個(gè)兄弟節(jié)點(diǎn)。
-
- 操作節(jié)點(diǎn)
-
appendChild,在后面添加孩子節(jié)點(diǎn); -
insertBefore,在某個(gè)節(jié)點(diǎn)之前添加; -
removeChild,刪除某個(gè)節(jié)點(diǎn); -
replaceChild,替換某個(gè)節(jié)點(diǎn) -
cloneNode,復(fù)制一個(gè)節(jié)點(diǎn),如果傳入?yún)?shù)true,則會(huì)連同子元素做深拷貝。
-
- 比較關(guān)系
-
compareDocumentPosition,是一個(gè)用于比較兩個(gè)節(jié)點(diǎn)中關(guān)系; -
contains,檢查一個(gè)節(jié)點(diǎn)是否包含另一個(gè)節(jié)點(diǎn); -
isEqualNode,檢查兩個(gè)節(jié)點(diǎn)是否完全相同; -
isSameNode,檢查兩個(gè)節(jié)點(diǎn)是否是同一個(gè)節(jié)點(diǎn),實(shí)際上在JavaScript中可以用“===”;
-
- 新建節(jié)點(diǎn)(DOM標(biāo)準(zhǔn)規(guī)定文檔節(jié)點(diǎn)必須由
create方法創(chuàng)建,而不能用JavaScript的new創(chuàng)建)createElementcreateTextNodecreateCDATASectioncreateCommentcreateProcessingInstructioncreateDocumentFragmentcreateDocumentType
Element
Node提供了在樹形結(jié)構(gòu)中操作節(jié)點(diǎn)的能力,但是很多時(shí)候我們需要的Element元素,是Node的子類,對(duì)應(yīng)了HTML中的標(biāo)簽,即有子節(jié)點(diǎn),又有屬性。
屬性
innerHTML
表示元素后代的HTML序列,設(shè)置新元素將會(huì)先刪除老元素,再創(chuàng)建新元素。
如果設(shè)置一個(gè)非法HTML值,瀏覽器會(huì)進(jìn)行修正。
document.body.innerHTML = '<b>test';
alert( document.body.innerHTML );
// <b>test</b>
outerHTML
該節(jié)點(diǎn)的所有HTML內(nèi)容,是它自己和innerHTML的合集。
注意,修改outerHTML并不會(huì)改變?cè)撛氐闹?,因?yàn)檫@樣操作不符合“面向?qū)ο蟆钡奶卣鳎焊冈負(fù)碛凶釉?,只有父元素才能“操作”子元素。顯然,要修改outerHTML的值,只有outerHTML的父元素通過innerHTML來修改。
data
innerHTML適用于所有Element節(jié)點(diǎn)。但是有些節(jié)點(diǎn)不是Element,比如Text,Comment,這時(shí)候可以使用data屬性獲取,或者用nodeValue獲取。
<body>
Hello
<!-- Comment -->
<script>
let text = document.body.firstChild;
alert(text.data); // Hello
let comment = text.nextSibling;
alert(comment.data); // Comment
</script>
</body>
關(guān)系屬性
類似Node中的屬性,只是變成了Element子類。
-
parentElement,父Element節(jié)點(diǎn); -
children,所有Element子節(jié)點(diǎn); -
firstElementChild,第一個(gè)Element子節(jié)點(diǎn); -
lastElementChild,最后一個(gè)Element子節(jié)點(diǎn); -
nextElementSibling,下一個(gè)兄弟Element節(jié)點(diǎn); -
previousElementSibling,上一個(gè)兄弟Element節(jié)點(diǎn)。
Others
-
nodeName: 對(duì)應(yīng)Node,所有繼承自Node的子類都有; -
tagName: 對(duì)應(yīng)Element,只有繼承自Element的才有; -
textContent: 獲取所有后代元素的Text值; -
hidden: 是否顯示,和style="display:none"左右相同; -
value: 對(duì)應(yīng)<input>,<select>and<textarea>的值; -
href: 對(duì)應(yīng)鏈接的href值; - ……
方法
操作屬性
getAttributesetAttributeremoveAttributehasAttributegetAttributeNodesetAttributeNode
此外,還可以像訪問property一樣,訪問attribute,比如document.body.attributes.class = “a” 等效于 document.body.setAttribute(“class”, “a”)。
查找元素
querySelectorquerySelectorAllgetElementByIdgetElementsByNamegetElementsByTagNamegetElementsByClassName
需要注意,getElementById、getElementsByName、getElementsByTagName、getElementsByClassName,這幾個(gè)API的性能高于querySelector。
另外,由getElementsByName、getElementsByTagName、getElementsByClassName獲取的并非一個(gè)靜態(tài)數(shù)組,而是一個(gè)可動(dòng)態(tài)更新的集合。
遍歷
DOM API中還提供了NodeIterator 和 TreeWalker 來遍歷樹。
NodeIterator
document.createNodeIterator(root, whatToShow); 實(shí)際上,由于性能等問題,工作中很少用到。
參數(shù)
- root: 文檔根節(jié)點(diǎn)
- whatToShow: 可被訪問節(jié)點(diǎn)
- NodeFilter.SHOW_ALL:所有節(jié)點(diǎn)
- NodeFilter.SHOW_ELEMENT:元素節(jié)點(diǎn)
- NodeFilter.SHOW_TEXT:文本節(jié)點(diǎn)
- NodeFilter.SHOW_COMMENT:注釋節(jié)點(diǎn)
- NodeFilter.SHOW_DOCUMENT:文檔節(jié)點(diǎn)
- filter: 過濾器,根據(jù)需求返回下面這些:
- NodeFilter.FILTER_ACCEPT
- NodeFilter.FILTER_REJECT: 跳過當(dāng)前節(jié)點(diǎn)和所有后代節(jié)點(diǎn)
- NodeFilter.FILTER_SKIP: 僅僅跳過當(dāng)前節(jié)點(diǎn)
比如,如果要訪問所有鏈接,可以用下面的filter
var filter = function(node) {
return node.tagName === 'ARTICLE' ? NodeFilter.FILTER_REJECT :
node.tagName === 'A' ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP
}
基本用法:
var iterator = document.createNodeIterator(document.body, NodeFilter.SHOW_ALL, filter, false);
var node;
while(node = iterator.nextNode())
{
console.log(node);
}
TreeWalker
document.createTreeWalker(root, whatToShow);
參數(shù)、用法與document.createNodeIterator類似。但是,TreeWalker多了在DOM樹上自由移動(dòng)當(dāng)前節(jié)點(diǎn)的能力,一般來說,這種API用于“跳過”某些節(jié)點(diǎn),或者重復(fù)遍歷某些節(jié)點(diǎn)。
var walker = document.createTreeWalker(document.body, NodeFilter.SHOW_ELEMENT, null, false)
var node;
while(node = walker.nextNode()) {
if(node.tagName === "p")
node.nextSibling();
console.log(node);
}
遍歷DOM樹
深度優(yōu)先遍歷
function deepWalk(el, action) {
if (el) {
action(el.nodeName)
deepWalk(el.firstElementChild, action)
deepWalk(el.nextElementSibling, action)
}
}
deepWalk(document.documentElement, console.log)
廣度優(yōu)先遍歷
var nodes = [document.documentElement]
function breadthWalk(action) {
if (nodes.length > 0) {
let el = nodes.shift()
action(el.nodeName)
nodes = [...nodes, ...el.children]
breadthWalk(action)
}
}
breadthWalk(console.log)
Range
Range API主要用在富文本編輯領(lǐng)域,平時(shí)使用較少。它表示一個(gè)范圍,以文字為最小單位,不過也可以通過offset獲取部分文字。Range
API的性能更好,但是用起來比較麻煩。
比如,對(duì)于這段HTML:
<p id="p">Example: <i>italic</i> and <b>bold</b></p>
它的DOM結(jié)構(gòu)如下:
-
P- text: Example
-
I- text: italic
- text: and
-
B- text: bold
如果要選擇而前兩個(gè)字,也就是 Example: <i>italic</i> , 用如下代碼就可以選取。
let p = document.getElementById("p")
let range = new Range()
range.setStart(p, 0)
range.setEnd(p, 2)
// 顯示選取,和按鼠標(biāo)右鍵選擇效果一樣
document.getSelection().addRange(range);
如果要選擇 ample: <i>italic</i> and <b>bol , 就需要offset。
let p = document.getElementById("p")
let range = new Range()
range.setStart(p.firstChild, 2);
range.setEnd(p.querySelector('b').firstChild, 3);
document.getSelection().addRange(range)
更多詳細(xì)指南請(qǐng)看javascript.info。
事件
概述
一般來說,事件來自輸入設(shè)備,我們平時(shí)能夠接觸到的輸入設(shè)備有三類:
- 鍵盤
- 鼠標(biāo)
- 觸摸屏
觸摸屏和鼠標(biāo)又有一定的共性,它們被稱作pointer設(shè)備,所謂pointer設(shè)備,是指它的輸入最終會(huì)被抽象成屏幕上面的一個(gè)點(diǎn)。但是觸摸屏和鼠標(biāo)又有一定區(qū)別,它們的精度、反應(yīng)時(shí)間和支持的點(diǎn)的數(shù)量都不一樣。
所有現(xiàn)代UI系統(tǒng),都來自WIMP(Window Icon Menu Pointer)系統(tǒng),以Window、Icon、Menu、Pointer這四種基本互動(dòng)元素為基礎(chǔ),構(gòu)建出了一個(gè)具有完備性的人機(jī)交互系統(tǒng)。WIMP最初由施樂公司發(fā)明,后來被蘋果和微軟用在了自家產(chǎn)品上,最終演變?yōu)楫?dāng)今UI系統(tǒng)的樣子。
關(guān)于WIMP,喬布斯和蓋茨之間還有一段小故事:
WIMP是由Alan Kay主導(dǎo)設(shè)計(jì)的,這位巨匠,同時(shí)也是面向?qū)ο笾负蚐malltalk語言之父。
喬布斯曾經(jīng)受邀參觀施樂,他見到當(dāng)時(shí)的WIMP界面,認(rèn)為非常驚艷,不久后就領(lǐng)導(dǎo)蘋果研究了新一代麥金塔系統(tǒng)。
后來,在某次當(dāng)面對(duì)話中,喬布斯指責(zé)比爾蓋茨抄襲了WIMP的設(shè)計(jì),蓋茨淡定地回答:“史蒂夫,我覺得應(yīng)該用另一種方式看待這個(gè)問題。這就像我們有個(gè)叫施樂的有錢鄰居,當(dāng)我闖進(jìn)去想偷走電視時(shí),卻發(fā)現(xiàn)你已經(jīng)這么干了。”
捕獲和冒泡
在HTML DOM API中,有兩種事件傳播機(jī)制:捕獲Capture和冒泡Bubble。很多人都知道捕獲是從外向內(nèi)傳播,而冒泡是從內(nèi)向外傳播。
但是,為什么是這樣的?
實(shí)際上點(diǎn)擊事件來自觸摸屏或者鼠標(biāo),鼠標(biāo)點(diǎn)擊并沒有位置信息,但是一般操作系統(tǒng)會(huì)根據(jù)位移的累積計(jì)算出來,跟觸摸屏一樣,提供一個(gè)坐標(biāo)給瀏覽器。
那么,把這個(gè)坐標(biāo)轉(zhuǎn)換為具體的元素上事件的過程,就是捕獲過程了。而冒泡過程,則是符合人類理解邏輯的:當(dāng)你按電視機(jī)開關(guān)時(shí),你也按到了電視機(jī)。
所以,計(jì)算機(jī)為了響應(yīng)事件,必須能夠獲取到精確的位置信息,這個(gè)過程必然是從外向內(nèi)、從大到小去做命中測(cè)試hitTest,這就是捕獲。而冒泡更加符合我們?nèi)祟惖男袨槟J?,所有事件或行為一定是先在小范圍?nèi)被感知,然后才在更大范圍內(nèi)被感知。
捕獲Capture
當(dāng)多個(gè)元素形成嵌套關(guān)系,其中的元素接收到輸入事件時(shí),此事件首先會(huì)被最外層的元素獲取,然后不斷向內(nèi)傳播。如圖所示(來自quirksmode.org):
| |
---------------| |-----------------
| element1 | | |
| -----------| |----------- |
| |element2 \ / | |
| ------------------------- |
| Event CAPTURING |
-----------------------------------
根據(jù)DOM Event標(biāo)準(zhǔn),整個(gè)傳播過程有三個(gè)階段:
- Capturing phase: the event goes down to the element.
- Target phase: the event reached the target element.
- Bubbling phase: the event bubbles up from the element.

實(shí)際上,捕獲模式用的比較少,除過一些特殊場(chǎng)景,一般都不會(huì)用到。在事件注冊(cè)APIaddEventListener(type, listener, useCapture)中,第三個(gè)參數(shù)useCapture默認(rèn)為false。
冒泡Bubble
與捕獲相反,當(dāng)接收到輸入事件時(shí),首先會(huì)被最里面的元素獲取,然后不斷向外傳播。如下圖所示:
/ \
---------------| |-----------------
| element1 | | |
| -----------| |----------- |
| |element2 | | | |
| ------------------------- |
| Event BUBBLING |
-----------------------------------
比如這段代碼:
<form onclick="alert('form')">FORM
<div onclick="alert('div')">DIV
<p onclick="alert('p')">P</p>
</div>
</form>
執(zhí)行結(jié)果
- 點(diǎn)擊最里層的
p,那么會(huì)連續(xù)顯示三個(gè)彈窗,分別為p、div和form; - 點(diǎn)擊中間的
div,那么會(huì)連續(xù)顯示兩個(gè)彈窗,分別為div和form; - 點(diǎn)擊最外層的
form,只出現(xiàn)一個(gè)彈窗,顯示form。
如果要阻止事件向上傳播,可用event.stopPropagation()或者event.stopImmediatePropagation().
<body onclick="alert(`the bubbling doesn't reach here`)">
<button onclick="event.stopPropagation()">Click me</button>
</body>
在IE9之前,只支持冒泡,但是IE9之后和其他瀏覽器對(duì)兩種模式都支持。
event.target
event.target表示觸發(fā)事件的所有元素中,嵌套層次最深的那一個(gè)元素。event.currentTarget則是運(yùn)行事件回調(diào)函數(shù)的元素, event.currentTarget = this 。
焦點(diǎn)
焦點(diǎn)系統(tǒng)控制鍵盤事件。當(dāng)不論操作系統(tǒng)還是瀏覽器引入多窗口UI系統(tǒng)時(shí),必須有一種機(jī)制用來區(qū)分到底是哪個(gè)窗口正在接收輸入設(shè)備的輸入input事件,這一機(jī)制就是焦點(diǎn)系統(tǒng)的核心職責(zé)。當(dāng)一個(gè)窗口獲取到焦點(diǎn)時(shí),表示它正在接收輸入設(shè)備(比如鍵盤、鼠標(biāo))的輸入事件,這時(shí)其他窗口會(huì)自動(dòng)釋放焦點(diǎn)。否則,所有的窗口就會(huì)失控。
在桌面瀏覽器中,可以用Tab鍵切換到下一個(gè)可聚焦的元素,用shift + Tab可以切到上一個(gè)。瀏覽器也提供了API來操作焦點(diǎn):
// 獲取
document.body.focus();
// 釋放
document.body.blur();
總結(jié)
瀏覽器DOM API為我們提供了操作DOM的能力,這大大提高了瀏覽器的功能性,為開發(fā)人員編寫可交互應(yīng)用提供了更多可能性。具體而言,可分為4類:
- 節(jié)點(diǎn)。節(jié)點(diǎn)API為操作DOM樹提供了基本的支持,比如增刪改查節(jié)點(diǎn),獲取各種關(guān)系節(jié)點(diǎn),以及節(jié)點(diǎn)屬性等等;
- 遍歷。作為樹形結(jié)構(gòu)系統(tǒng),遍歷操作是不可避免的,不過這些遍歷API用的并不多,主要是性能和易用性欠佳;
- Range。Range為富文本領(lǐng)域的應(yīng)用提供了很多便利,不斷功能強(qiáng)大,而且性能出眾;
- 事件。輸入輸出設(shè)備的引入,擴(kuò)展了計(jì)算機(jī)系統(tǒng)的能力,而事件就是對(duì)這些能力的抽象。