JavaScript DOM概念
當一個網頁被載入到瀏覽器時,瀏覽器會先分析這個 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"