上一節,咱們整理了DOM系列中的第一篇,主要介紹瀏覽器與DOM相關的知識。從標題中我們可以看出來,今天所要學的東西包含兩個部分,第一部分是DOM樹,第二部分是遍歷DOM。如果你和我一樣對於DOM樹和遍歷DOM是初次接觸,那個人建議您花點時間好好看看這兩部分的知識。
眾所周之,HTML文檔的主幹就是標記(也就是大家熟悉的HTML標籤元素)。
根據文檔對象模型(即:DOM),每個HTML標籤事實上都是一個對象。嵌套的標籤被稱為之元素(或子標籤)。除此之外,標籤內的文本也是一個對象。而這些對象都可以使用JavaScript訪問。
那麼啥是DOM樹呢?我們先來看看現實生活中的例子。想像一棵與所有世代有關係的家庭樹(大家熟知的族譜),其包括了:祖父母、父母、孩子、兄弟姐妹等等。我們通常以等級的方式組織豪庭樹(族譜)。
上圖是一個家族族譜的圖。其中Tossico
、Akikazu
、Hitomi
和Takemi
是祖父母。而Toshiaki
和juliana
是父母。另外TK
、Yuji
、Bruno
和Kaio
是父母的孩子(其實也是我的兄弟姐妹們)。
除了家族族譜之外,生活中還有另一個示例,那就是一個組織的結構層次,比如:
而在HTML中,DOM其實也類似一棵樹的,它和前面所舉例的家族族譜,組織機構圖是類似的,HTML中DOM就是一棵樹。
我們來看一個DOM的示例,比如下面這樣的一個HTML文檔:
<!DOCTYPE HTML><html> <head> <title>About elks</title> </head> <body> The truth about elks. </body></html>
DOM將HMTML表示為標記的樹結構(也就是大家所說的DOM樹),就如下面這樣的樣子:
在上面的圖中,你可以單擊元素的節點,它們的子節點可以展開或者收縮,如下圖所示:
HTML的標籤被稱為元素(element
)節點(或只是元素)。嵌套標籤成為一個子元素(也被稱為子)。因此,對於一個HTML文檔而言,<html>
是一個根節點(也被稱為根元素),然後<head>
和<body>
是<html>
的子元素。
元素內的文本被稱這文本節點,標記為#text
。文本節點僅包含一個字符串。它可能沒有子元素,也就是說它永遠只是樹的葉子(沒有成為樹枝的可能)。
除此之外,要注意文本節點中的兩個特殊字符:
↵
(對應JavaScript中的\n
)␣
空格和換行符是完全有效的字符,它們形成文本節點並成為DOM的一部分。因此,例如在<head>
標籤之上的示例中,在<title>
這前包含了一些空格,並且該文本成為一個#text
節點(它只包含一條換行符和一些空格)。
不過要注意的是,有兩個將會除外:
<head>
標籤之前的空格和換行符由於歷史原因將被忽略</body>
之後,那麼它就會自動地移到</body>
的前面,正如HTML規範要求的一樣,所有內容必須在</body>
中一樣。因此,在</body>
之後可能沒有空格在其他情況之下,一切都很簡單。如果文檔中有空格(就像任何字符一樣),那麼它們就會成為DOM中的文本節點,如果我們刪除它們,那麼就不會有任何東西,也不再會有空格符或換行符的節點。
比如下面這個示例:
<!DOCTYPE HTML><html><head><title>About elks</title></head><body>The truth about elks.</body></html>
上面的HTML結構對應的DOM樹如下圖所示:
相比上面的截圖可以看出來,沒有了空格符和換行符的文本節點。
通過上面的示例,可能你對DOM樹有一定的瞭解了。但對一些一技術的定義估計還不是非常的瞭解,接下來花點時間來說一下DOM中的一些技術定義。
DOM樹(tree
)是一個DOM節點(nodes
)的集合(拿到生活中來說,樹是稱為節眯的實體集合)。而其中節點由邊(edges
)連接。每個節點(node
)都包含一個值(value
)或數據(data
),它可能或有可能沒有子節點(child node
)。
tree
的first node
稱為root
節點。如果root
節點由另一個節點連接,則root
節點是父節點,連接的節點是子節點。
所有的樹節點(Tree nodes
)都被edges
連接在一起。它是樹(trees
)的重要組成部分,因為它管理節點(nodes
)之間的關係。
對於一棵樹而言,葉子(leaes
)是樹(tree
)上的最後一個節點(nodes
)。它們是沒有子節點。就像真正的樹一樣,DOM也是有根(root
)、枝(Element
)和葉子(文本節點)。
除此之外,其他還需要理解的重要概念是高度(height
)和深度(depth
)。樹的高度是葉子最長路徑的長度;節點的深度是路徑到其根的長度。用下圖來闡述會更形象一些:
簡單的總結一些術語:
root
(根節點)是樹(tree
)最頂端的節點edge
(邊緣)是兩個節點(node
)之間的連接child
(子節點)是具有父節點的節點parent
(父節點)是一個節點,它具有子節點的邊緣leaf
(樹葉)是樹中沒有子節點的節點height
(高度)是葉子最長路徑的長度depth
(深度)是路徑到其根的長度有關於這方面更深入的介紹可以閱讀@TK的《Everything you need to know about tree data structures》一文。
另外@TK的文章還涉及到了深度優先遍歷和廣度優秀遍歷,有關於這兩個概念的深入介紹,可以閱讀:
querySelectorAll
和 getElementsByTagName
其實有關於深度優先遍歷和廣度優秀遍歷在DOM樹中的作用並不明顯,對於後續的DOM遍歷還是有很大的影響。
經過的上面的學習,我們對於DOM樹有了一定的瞭解。除此之外,瀏覽器對於DOM還具有自動較正的特性。
如果瀏覽器遇到格式錯誤的HTML,它會自動更正它(校正)。
例如,HTML最頂端的標籤總是<html>
。即使它不存在文檔中 —— 它將存在DOM中,瀏覽器也會創建它。另外<body>
也是一樣。
例如,HTML文件只包含一個單詞Hello
,瀏覽器將它放置在成<html>
和<body>
中,並且也會添加所需的<head>
。其DOM將是:
另外,生成DOM時,瀏覽器會自動處理文檔中的錯誤,比如關閉標籤等等。比如下面這樣一個無效的文檔:
<p>Hello<li>Mom<li>and<li>Dad
事實上,瀏覽器渲染時,它照樣會成為一個正常的 DOM,那是因為瀏覽器讀取標籤並會自動修復丟失的部分(比如說關閉標籤):
除此之外,還有一個有趣的“特殊情況”,那就是table
(表格元素)。根據DOM規範,它必須有<tbody>
,但是如果你在HTML文檔中忘記寫該標籤元素時,瀏覽器會自動在DOM中添加<tbody>
標籤。比如:
<table id="table"><tr><td>1</td></tr></table>
此時瀏覽器渲染出來的DOM結構如下:
我們可以在一個HTML文檔中添加更多的標籤和在頁面中添加註釋,比如:
<!DOCTYPE HTML><html> <body> The truth about elks. <ol> <li>An elk is a smart</li> <!-- comment --> <li>...and cunning animal!</li> </ol> </body></html>
對於上面的HTML文檔,其對應的DOM樹如下圖所示:
上圖中,我們看到了一個新的節點類型 —— 註釋節點,標記為 #comment
。
你可能會想,為什麼要將註釋添加到DOM中呢?它不會以任何方式影響視覺上的效果,但是有一個規則,如果某個東西在HTML中,那麼它也必須在DOM樹中。
HTML中的一切,甚至是註釋,都將成為DOM的一部分。
即使是<!DOCTYPE ...>
指令也是一個DOM節點。它在DOM樹中,在<html>
之前。我們不會去觸摸那個節點,我們甚至不會在圖上畫它,但它卻實實大大的存在那裡。
document
對象也是一個DOM節點,表示整個文檔。在DOM中,其有12
種節點類型。在實際操作中,我們通常使用4種方法:
document
:進入DOM的入口點或許你和我一樣,希望能對每個HTML文檔對應的DOM結構能實時的查看,我們希望有對應的工具能幫助我們。事實上是有類似這樣的工具,比如 Live DOM Viewer。只要輸入文檔,它就會立即顯示DOM樹結構。
DOM 中的空白符會讓處理節點結構時增加不少麻煩。在Mozilla 的軟件中,原始文件裡所有空白符都會在 DOM 中出現(不包括標籤內含的空白符)。這樣的處理方式有其必要之處,一方面編輯器中可逕行排列文字、二方面 CSS 裡的 white-space: pre
也才能發揮作用。 如此一來就表示:
換句話說,下面這段 HTML 代碼對應的 DOM 節點結構會如附圖所示,其中\n
代表換行符:
<!-- My document --><html> <head> <title>My Document</title> </head> <body> <h1>Header</h1> <p> Paragraph </p> </body></html>
對應的DOM樹,如下圖所示:
這麼一來,要使用 DOM 遊走於節點結構間又不想要無用的空白符時,會有點困難。
以下的 JavaScript 代碼定義了許多函數,能夠讓你在處理 DOM 中的空白符時輕鬆點:
/*** 以下所謂的“空白符”代表:* "\t" TAB \u0009 (製表符)* "\n" LF \u000A (換行符)* "\r" CR \u000D (回車符)* " " SPC \u0020 (真正的空格符)** 不包括 JavaScript 的“\s”,因為那代表如不斷行字符等其他字符。*//*** 測知某節點的文字內容是否全為空白。** @參數 nod |CharacterData| 類的節點(如 |Text|、|Comment| 或 |CDATASection|)。* @傳回值 若 |nod| 的文字內容全為空白則傳回 true,否則傳回 false。*/function is_all_ws( nod ) { // Use ECMA-262 Edition 3 String and RegExp features return !(/[^\t\n\r ]/.test(nod.data));}/*** 測知是否該略過某節點。** @參數 nod DOM1 |Node| 對象* @傳回值 若 |Text| 節點內僅有空白符或為 |Comment| 節點時,傳回 true,* 否則傳回 false。*/function is_ignorable( nod ) { return ( nod.nodeType == 8) || // 註釋節點 ( (nod.nodeType == 3) && is_all_ws(nod) ); // 僅含空白符的文字節點}/*** 此為會跳過空白符節點及註釋節點的 |previousSibling| 函數* ( |previousSibling| 是 DOM 節點的特性值,為該節點的前一個節點。)** @參數 sib 節點。* @傳回值 有兩種可能:* 1) |sib| 的前一個“非空白、非註釋”節點(由 |is_ignorable| 測知。)* 2) 若該節點前無任何此類節點,則傳回 null。*/function node_before( sib ) { while ((sib = sib.previousSibling)) { if (!is_ignorable(sib)) return sib; } return null;}/*** 此為會跳過空白符節點及註釋節點的 |nextSibling| 函數** @參數 sib 節點。* @傳回值 有兩種可能:* 1) |sib| 的下一個“非空白、非註釋”節點。* 2) 若該節點後無任何此類節點,則傳回 null。*/function node_after( sib ) { while ((sib = sib.nextSibling)) { if (!is_ignorable(sib)) return sib; } return null;}/*** 此為會跳過空白符節點及註釋節點的 |lastChild| 函數* ( lastChild| 是 DOM 節點的特性值,為該節點之中最後一個子節點。)** @參數 par 節點。* @傳回值 有兩種可能:* 1) |par| 中最後一個“非空白、非註釋”節點。* 2) 若該節點中無任何此類子節點,則傳回 null。*/function last_child( par ){ var res=par.lastChild; while (res) { if (!is_ignorable(res)) return res; res = res.previousSibling; } return null;}/*** 此為會跳過空白符節點及註釋節點的 |firstChild| 函數** @參數 par 節點。* @傳回值 有兩種可能:* 1) |par| 中第一個“非空白、非註釋”節點。* 2) 若該節點中無任何此類子節點,則傳回 null。*/function first_child( par ){ var res=par.firstChild; while (res) { if (!is_ignorable(res)) return res; res = res.nextSibling; } return null;}/*** 此為傳回值不包含文字節點資料的首尾所有空白符、* 並將兩個以上的空白符縮減為一個的 |data| 函數。*( data 是 DOM 文字節點的特性值,為該文字節點中的資料。)** @參數 txt 欲傳回其中資料的文字節點* @傳回值 文字節點的內容,其中空白符已依前述方式處理。*/function data_of( txt ) { var data = txt.data; // Use ECMA-262 Edition 3 String and RegExp features data = data.replace(/[\t\n\r ]+/g, " "); if (data.charAt(0) == " ") data = data.substring(1, data.length); if (data.charAt(data.length - 1) == " ") data = data.substring(0, data.length - 1); return data;}
如果你閱讀了上面的的內容,或許你已經意識到,DOM看起來就像一個巨大的樹 —— 一棵巨大的樹,它的元素掛載在樹枝上。為了獲得更多的技術,DOM中的元素被安排在一個層次結構中,它定義了你最終在瀏覽器中看到的內容:
這個層次結構用於幫助我們組織HTML元素。它還用於幫助你的CSS樣式規則理解什麼樣式適用於哪些東西。從JavaScript角度來看,這個層次結構確實增加了一點複雜性。你會花相當多的時間去弄清楚你現在所有的DOM和你需要去的地方。當我們考慮創建新的元素或移動元素時,這將變得更加明顯。這種複雜性是你需要適應的。
在你找到元素並與它們做一些事情之前,你首先需要瞭解元素的位置。我解決這個問題,最簡單的方法就是從頭開始,然後一路向下。這就是我們要做的。
為了更易於幫助在大家理解,先回到上一節中的示例中:
<!DOCTYPE html> <html> <head> <meta content="DOM, JavaScript, W3cplus" name="keywords" /> <meta content="DOM系列,瀏覽器和DOM!" name="description" /> <title>LOL! Sea Otter! Little Kid!</title> <link href="style.css" rel="stylesheet"/> </head> <body> <div id="container"> <img src="w3cplus_logo.png"/> <h1>DOM系列學習!</h1> <p class="bodyText">開始學習DOM,這是一個有關於DOM學習的系列教程...<p> <div class="submitButton">next</div> </div> <script src="main.js"></script> </body> </html>
來自DOM頂部的視圖由window
、document
和html
元素組成:
由於這三樣東西的重要性,DOM為你提供了通過window
、document
和document.documentElement
訪問它們的方法。
var windowObject = window; var documentObject = document; var htmlElement = document.documentElement;
需要注意的一點是,window
和document
都是全局屬性。不必要明確的聲明它們,可以直接從容器裡拿出來用就行了。
往往,最頂層的樹節點可以直接作為document
屬性使用,比如:
<html> = document.documentElement
頂部文檔節點document.documentElement
,其對應的就是<html>
的DOM節點。另外一個廣泛使用的DOM節點是<body>
元素,其對應的是document.body
:
<body> = document.body
同樣的,<head>
標籤可以用document.head
。
不過有一點需要注意:
document.body
有可能為null
。當腳本在訪問不存在的元素時,返回的值將會為null
。
比如,當你的腳本在</head>
中運行,比如document.body
是將返回的值將是null
,因為瀏覽器還沒有讀取它。但在</body>
中的<script>
中返回的則是<body>
元素:
<html> <head> <script> console.log('From head:', document.body) // => null </script> </head> <body> <script> console.log('From body:', document.body) // => HTMLBodyElement </script> </body></html>
上面我們所看到的是html
、head
和body
元素的獲取。但事實上,一旦你進入HTML元素級別,你的DOM將開始分支並變得更有趣。在這一點上,你有幾種獲取DOM的方式。通過使用querySelector()
和querySelectorAll()
可以幫助你精確地獲取你想要獲取的DOM元素。或許你已經在項目中大量使用這兩種方法了。但事實上,對於許多實際案例來說,這兩種方法太過侷限。
有時候,你不知道你想去哪裡。querySelector()
和querySelectorAll()
主法在這裡無法幫助您。你只想上車然後開車,並想找到你想要去的地方。回到DOM的世界當中時,你會發現自己一直處理這個位置。這就是DOM提供的各種內置屬性,所有的Motorcycle Diaries將會幫助你,接下來我們將看看這些屬性。
能夠幫助你的是知道所有的DOM元素都至少有一個組合,包括父母(Parents)、兄弟姐妹(Siblings)和子元素(Children)。為了更直觀的幫助大家理解,來看下圖,下圖中包含div
的script
的一個樹形圖:
div
和script
是兄弟元素。他們是兄弟元素的原因是他們共有一個相同的父元素body
。script
元素沒有子元素,但是div
元素有四個子元素,img
、h1
、p
和div
。這四個元素也相互被稱為兄弟元素,同樣的是因為他們有相同的父元素。這其實很好理解,如果你閱讀了文章前面的DOM樹相關的內容,你會發現它們就像現實的生活中一樣,父母、孩子 和兄弟姐妹的關係基於你所關注的樹的位置(對應的就是家族族譜)。幾乎每個元素,取決於你看它們的角度,可以扮演多個家庭角色。
為了更好的理解,DOM中提供了一些對應的屬性(這些屬性具有一定的依賴關係)。包括:firstChild
、lastChild
、parentNode
、children
、previousSibling
和nextSibling
。從他們的名稱上來看,就可以推出這些屬性的作用。這幾個屬性結合在一起將構建一個DOM遍歷鏈接圖,允許在DOM節點間找到你想要找到的DOM:
為了更好的理解DOM遍歷相關的知識點,咱們接下來將圍繞這幾個屬性來展開。
在這些DOM屬性中,最容易處理的是父母和兄弟姐妹。對應的屬性有parentNode
、previousSibling
和nextSibling
。下面這張圖將幫助你瞭解這三個屬性是如何工作的:
這張圖雖然有點零亂,但是你仔細看的話,你可以理清楚它們之間的關係,以及他們之間發生了什麼。parentNode
屬性指向元素的父元素。previousSibling
和nextSibling
屬性允許元素元素它的前一個或下一個兄弟元素。你可以在圖中看到箭頭的方向指向。最後一行,img
的nextSibling
是div
,相應的div
的previousSibling
是img
元素。不管是通過img
或div
的parentNode
屬性都將把我們帶入到第二行中的div
(事實上就是img
和div
的元素)。通過上圖,大家理解起來是不是很簡單。
上面咱們看到的是如何通過DOM的屬性來訪問兄弟元素和父元素,事實上,除此之外,DOM還提供一些屬性可以訪問元素的子元素,比如firstChild
、lastChild
和children
。同樣用一圖來向大家展示:
firstChild
和lastChild
屬性是指父元素的第一個和最後一個子元素。如果父類只有一個子元素,就像例子中的body
元素一樣,firstChild
和lastChild
都指向相同的元素div
。如果一個元素沒有子元素,那麼firstChild
和lastChild
屬性將返回一個null
。
與其他屬性相比,其中children
屬性相對而方要更為複雜一些。當你在父類上訪問children
屬性時,基本上會得到父元素的子元素集合。這個集合並不是數組,但它確實有一些類似數組的能力,就是大家所說的類數組,其具有length
屬性,可以通過[]
或item()
來索引集合中具體的元素。比如上圖中的div.children[0]
訪問到的是第一個img
元素。
在DOM中獲取子節點,除了前面提到的三個屬性之外,還有一個childNodes
屬性,不過它和children
有一個很明顯的區別:
children
只獲取子節點(即子元素),而childNodes
除了獲取的子節點還包括文本節點。
除此之外,還有一個特殊的函數hasChildNodes()
可以用來判斷某個元素是否包含子節點。
這個時候,你把它們放在一起,你就可以對DOM進行遍歷。也可以做一些事情。比如,檢查某個元素是否有子元素存在,我們就可以這樣做:
let bodyElement = document.bodyif (bodyElement.firstChild) { // 這裡做你想做的事情...}
如果body
沒有子節點,那麼if
語獎將返回null
。當然,你也可以使用bodyElement.lastChild
或bodyElement.children
做為if
語句的條件。
再來看另一個簡單示例,前面提到過了,通過children
可以獲取某個元素的所有子節點(前提是這個元素有子元素存在)。這個時候得到的是一個類數組,如查你要獲取到該元素中的每個子節點,就需要使用for
循環來處理:
var bodyElement = document.body;for (var i = 0; i < bodyElement.children.length; i++) { var childElement = bodyElement.children[i]; document.writeln(childElement.tagName);}
上面咱們看到的是通過DOM節點來遍歷DOM。比如childNodes
屬性,除了可以獲取元素節點之外,還可以獲取文本節點,甚至是註釋節點。但很多時候,對於DOM的操作,咱們只需要獲取想要的DOM元素節點,而不需要考慮文本和註釋節點。這個時候咱們只需要操作元素節點即可,這對應DOM中操作元素節點的一些屬性。同樣的使用下圖來向大家闡述,易於理解:
和前面相比,這裡的屬性多了Element
這個詞,其對應的含義:
children
:元素節點的子元素firstElementChild
、lastElementChild
:元素的第一個或最後一個子元素previousElementSibling
和nextElementSibling
:元素的前一個或後一個相鄰元素parentElement
:元素的父元素這篇文章是DOM系列的第二篇文章,主要介紹了DOM樹和DOM的遍歷。前一部分只要介紹了DOM樹,簡單的理解,任何一個HTML文檔都可以類似於家族的族普來繪製對應的DOM樹。通過DOM樹可以理清楚每個DOM元素(或者說DOM節點)之間的關係。比如,父子關係、兄弟關係等。
另外,在DOM中找到對應的元素是每位JavaScript開發人員都應該需要掌握的技巧之一。這篇文章的後一部分主要向大家介紹了如何對DOM進行遍歷,其實就是通過DOM的屬性怎麼獲取DOM的元素或節點。簡單的歸納一下,分為:
parentNode
、parentElement
和closest
;querySelector()
、querySelectorAll()
、children
、firstChildren
、lastChildren
和childNodes
nextElementSibling
、previousElementSibling
、nextSibling
和previousSibling
如果文中有不對之處,或者你有更好的經驗,歡迎在下面的評論中與我們一起分享。最後要說明的是,文章中有些圖片來自互聯網,如涉及侵權,煩請告之。
联系客服