JavaScript DOM概念

Steven Wang
3 min readJan 23, 2019

--

當一個網頁被載入到瀏覽器時,瀏覽器會先分析這個 HTML 檔案,然後會依照這份 HTML 的內容解析成「DOM」 (Document Object Model,文件物件模型)。

DOM 是 W3C 制定的一個規範,它是獨立於平台與語言的標準。 換言之,只要遵守這樣的規範實作,不管是什麼平台或者是什麼語言開發,都可以透過 DOM 提供的 API 來操作 DOM 的內容、結構與樣式。

所以說,DOM 是網頁的根本,控制 DOM 就可以控制整個網頁,做出良好的互動體驗

當一個網頁被載入到瀏覽器時,瀏覽器會先分析這個 HTML 檔案,「由上而下」依序來讀取解析

當瀏覽器在 <head> ... </head> 之間遇到 <script> 標籤時,就會暫停解析網頁,並且「立即」執行 <script> 裡的內容,直到 script 執行完畢後再繼續解析網頁。

DOM 節點的選取

document 物件是 「DOM tree」 的根節點,所以當我們要存取 HTML 時,都從 document 物件開始。 而 DOM 的節點類型除了 「HTML 元素節點」 (element nodes) 外,還有「文字節點」 (text nodes)、「註解節點」 (comment nodes) 等。

DOM 選取方法有下列這些

// 根據傳入的值,找到 DOM 中 id 為 ‘xxx’ 的元素。 document.getElementById(‘xxx’);

// 針對給定的 tag 名稱,回傳所有符合條件的集合 document.getElementsByTagName(‘xxx’);

// 針對給定的 class 名稱,回傳所有符合條件的集合
// IE9 後開始支援 document.getElementsByClassName(‘xxx’);

// 針對給定的 Selector 條件,回傳第一個 或 所有符合條件的 NodeList。
// IE8 後開始支援
document.querySelector(‘xxx’);
document.querySelectorAll(‘xxx’);

document.getElementsBy**document.querySelector / document.querySelectorAll 的差異DOM 節點的類型常見的有下面幾種:

document.getElementById 以及 document.querySelector 因爲取得的一定只會有一個元素/節點,所以不會有 index 與 length 屬性。

document.getElementsBy** (注意,有個 s) 以及 document.querySelectorAll則分別回傳 「HTMLCollection」 與 「NodeList」。

這兩者其實是類似的規格實作,「HTMLCollection」只收集 HTML element 節點,而「NodeList」除了 HTML element 節點,也包含文字節點、屬性節點等。 雖然不能使用陣列型別的 method,但這兩種都可以用「陣列索引」的方式來存取內容。

另一個需要注意的地方是,HTMLCollection / NodeList 在大部分情況下是即時更新的,但透過 document.querySelector / document.querySelectorAll 取得的 NodeList 是靜態的。

Node.ELEMENT_NODE
對應數值1
說明HTML 元素的 Element 節點

Node.TEXT_NODE
對應數值3
說明實際文字節點,包括了換行與空格Node.COMMENT_NODE對應數值8說明註解節點

Node.DOCUMENT_NODE
對應數值9
說明根節點 (Document)

Node.DOCUMENT_TYPE_NODE
對應數值10
說明文件類型的 DocumentType 節點,例如 HTML5 的 <!DOCTYPE html>

Node.DOCUMENT_FRAGMENT_NODE
對應數值11
說明DocumentFragment 節點

可以透過節點類型「常數」或是「對應數值」來判斷:
document.nodeType === Node.DOCUMENT_NODE; // true document.nodeType === 9; // true

DOM 節點間的查找遍歷 (Traversing)

由於 DOM 節點有分層的概念,於是節點與節點之間的關係,我們大致上可以分成兩種:

  • 父子關係
    除了 document 之外,每一個節點都會有個上層的節點,我們通常稱之為「父節點」 (Parent node),而相對地,從屬於自己下層的節點,就會稱為「子節點」(Child node)。
  • 兄弟關係:有同一個「父節點」的節點,那麼他們彼此之間就是「兄弟節點」(Siblings node)。

隔層的節點基本上沒有直接關係。

Node.childNodes

所有的 DOM 節點物件都有 childNodes 屬性,且此種屬性無法修改。
我們可以透過 Node.hasChildNodes() 來檢查某個 DOM 節點是否有子節點。

var node = document.querySelector('#hello');// 如果 node 內有子元素
if( node.hasChildNodes() ) {
// 可以透過 node.childNodes[n] (n 為數字索引) 取得對應的節點
// 注意,NodeList 物件內容為即時更新的集合
for (var i = 0; i < node.childNodes[i].length; i++) {
// ...
};
}

Node.childNodes 回傳的可能會有這幾種:

  • HTML 元素節點 (element nodes)
  • 文字節點 (text nodes),包含空白
  • 註解節點 (comment nodes)

Node.firstChild

Node.firstChild 可以取得 Node 節點的第一個子節點,如果沒有子節點則回傳 null

要注意的是,子節點包括「空白」節點,所以像下面範例:

<p>
<span>span 1</span>
<span>span 2</span>
<span>span 3</span>
</p>
<script>
var p = document.querySelector('p');
// tagName 屬性可以取得 node 的標籤名稱
console.log(p.firstChild.tagName); // undefined
</script>

因為拿到的是 <p> 與第一個 <span> 中間的「換行字元」,所以 p.firstChild.tagName 會得到 undefined

改成這樣:

<p><span>span 1</span><span>span 2</span><span>span 3</span></p><script>
var p = document.querySelector('p');
// tagName 屬性可以取得 node 的標籤名稱
console.log(p.firstChild.tagName); // "SPAN"
</script>

把中間的換行與空白移除,就會得到預期中的 "SPAN" 了。

Node.lastChild

Node.lastChild 可以取得 Node 節點的最後一個子節點,如果沒有子節點則回傳 null

Node.firstChild 一樣的是,子節點包括「空白」節點,所以像這樣:

<p>
<span>span 1</span>
<span>span 2</span>
<span>span 3</span>
</p>
<script>
var p = document.querySelector('p');
// textContent 屬性可以取得節點內的文字內容
console.log(p.lastChild.textContent); // "" (換行字元)
</script>

得到的會是一個換行字元的空字串。

移除節點之間多餘的空白後:

<p><span>span 1</span><span>span 2</span><span>span 3</span></p><script>
var p = document.querySelector('p');
// textContent 屬性可以取得節點內的文字內容
console.log(p.lastChild.textContent); // "span 3"
</script>

就會是正確的 “span 3” 了。

Node.parentNode

那麼相較於「Child 系列」,parentNode 就單純一些。
透過 Node.parentNode 可以用來取得父元素,回傳值可能會是一個元素節點 (Element node)、根節點 (Document node) 或 DocumentFragment 節點。

<p><span>span 1</span><span>span 2</span><span>span 3</span></p><script>
var el = document.querySelector('span');
console.log( el.parentNode.nodeName ); // "P"
</script>

Node.previousSibling

看完了 DOM「父與子」之後,接著來看看兄弟節點。
透過 Node.previousSibling 可以取得同層之間的「前一個」節點,如果 node 已經是第一個節點,則回傳 null

<p><span>span 1</span><span>span 2</span><span>span 3</span></p><script>
var el = document.querySelector('span');
console.log( el.previousSibling ); // null
// document.querySelectorAll 會取得所有符合條件的集合,
// 而 document.querySelectorAll('span')[2] 指的是「第三個」符合條件的元素。
var el2 = document.querySelectorAll('span')[2];
console.log( el2.previousSibling.textContent ); // "span 2"
</script>

Node.nextSibling

Node.previousSibling 類似,透過 Node.previousSibling 可以取得同層之間的「下一個」節點,如果 node 已經是最後一個節點,則回傳 null

// document.querySelector 會取得第一個符合條件的元素
var el = document.querySelector('span');
console.log( el.nextSibling.textContent ); // "span 2"

--

--

Steven Wang
Steven Wang

No responses yet