走訪 DOM 樹
September 17, 2022想要走訪文件有多種方式,基於 DOM 結構、標籤名稱/id/類別名稱或者 CSS 選擇器。
基於 DOM 結構
只要你取得了文件中某個節點(Node
),就可以取得它的父節點、子節點、鄰接節點等,相關的特性有:
- 取得父節點:
parentNode
- 前鄰接節點:
previousSibling
- 後鄰接節點:
nextSibling
- 首個子節點:
firstChild
- 最後一個子節點:
lastChild
- 所有直接子節點:
childNodes
以〈DOM 簡介〉中的文件為例:
<html>
<head>
<title>首頁</title>
</head>
<body>
<h1>Hello!World!</h1>
<a href="zh-tw/">學習筆記</a>
</body>
</html>
這份 HTML 文件,假設你使用 document.body
取得 body
節點,假設就是被 body
變數參考,從 body
開始,各特性可以取得的節點標示如下(為了簡化,不考慮換行與縮排的文字節點):
document
|-html(body.parentNode)
|-head(body.previousSibling)
| |-title
| |-首頁
|body
|-h1(body.firstNode)
| |-Hello!World!
|-a(body.lastNode)
|-學習筆記
如果是 body.childNodes
,則取得 NodeList
物件,這是一個類陣列具有索引存取的特性,索引從 0 開始就是第一個子節點,所以 body.childNodes[0]
就是 h1
節點,body.childNodes[1]
就是 a
節點。
(NodeList
規範的是使用 item()
搭配索引來取得元素,不過在 JavaScript 中,可以直接使用 []
搭配索引來取得元素。)
實際上不建議透過以上特性來存取元素,因為這會強烈依賴文件結構,一旦調整了文件內容,程式可能就無法運作了。
基於標籤名稱/id/類別名稱
常見的作法是,如果節點是元素(Element
),透過元素的 getElementsByTagName
、getElementById
、getElementsByClassName
方法來取得節點。例如若想取得文件中所有的 <p>
標籤代表的元素,則可以如下:
let ps = document.getElementsByTagName('p');
由於文件中可能不只有一個 <p>
標籤,取得的節點也可能不只一個,getElementsByTagName
取得的是 HTMLCollection
,可依標籤在文件中的順序使用索引取得對應節點。例如,document.getElementsByTagName('p')[0]
取得的就是文件中第一個出現的 <p>
標籤對應的節點。
如果標籤上定義有 id
屬性,則可以使用 getElementById
來取得元素。例如文件中有個 <div>
:
<div id="console">Console here</div>
可以透過以下取得:
let console = document.getElementById('console');
id
在文件中基本上應該是獨一無二的,如果文件中出現重複的 id
,那 getElementById
取得的會是文件中第一個符合的元素。
如果元素上定義有 class
,可以透過 getElementsByClassName
來查找,例如若有個 HTML 片段:
<div class="article newest">
<div class="sport">OOOOO</div>
</div>
<div class="article">
<div class="music">XXXXX</div>
</div>
那麼可以使用底下片段取得 HTMLCollection
,包含全部 class
屬性上設定有 article
的元素:
let articles = document.getElementsByClassName('article');
透過 getElementsByTagName
、getElementById
、getElementsByClassName
方法取得的元素,是以該元素為根的 DOM 樹,因而可以進一步取得子元素。例如文件有以下的內容:
<div id="test">
<div>Test 1 Here</div>
<div>Test 2 Here</div>
</div>
以下可以取得 Test 1 Here 的內容:
let testDiv = document.getElementById('test');
let test1DivHtml = testDiv.getElementsByTagName('div')[0].innerHTML;
innerHTML
可以取得標籤內含之 HTML,以字串形態傳回,過去 innerHTML
不是標準特性,不過幾乎所有瀏覽器都支援它,而 HTML 5 正式將 innerHTML
納入標準;如果你想取得純文字,可以使用 textContent
。
如果是 HTMLDocument
,會有個 getElementsByName
方法,只要標籤上有設定 name
屬性,就可以使用這個方法來取得對應元素。HTML 文件中 document
就是 HTMLDocument
,所以就可以使用這個方法。標籤的 name
屬性值可以重複,所以 getElementsByName
取得的不只一個元素,會以 HTMLCollection
收集符合的元素。
取得某個節點或元素,自然會想要知道有關這個節點或元素的一些資訊。例如方才使用 innerHTML
取得元素內含的 HTML 就是一個例子。另外,經常地,會想要得知元素的屬性為何,例如取得最上面列出的 HTML 文件中, <a>
的 href
屬性:
let href = document.getElementsByTagName('a')[0].href;
如果要以標準方式,可以透過 getAttribute
方法來取得標籤的屬性值。例如:
let href = document.getElementsByTagName('a')[0].getAttribute('href');
使用標準的作法,好處是處理像 class
屬性這樣的東西比較方便。例如有個標籤:
<div id="console" class="demo">DEMO</div>
如果要用特性的方式取得,由於 class
是保留字(在 ES6 以後被用來定義類別了),必須改用 className
:
let clzName = document.getElementById('console').className;
使用標準作法,可以這麼寫:
let clzName = document.getElementById('console').getAttribute('class');
<label>
的 for
屬性也是類似,由於 for
是 JavaScript 的關鍵字,使用特性取得時,必須改用 htmlFor
:
let htmlFor = document.getElementById('someLabel').htmlFor;
但使用標準方法的話,可以這麼寫:
let htmlFor = document.getElementById('someLabel').getAttribute('for');
要以特性方式要取得標籤設置的屬性,特性名稱要注意大小寫的問題,通常會是駝峰式命名。例如要取得 HTML 中設置的屬性如 cellspacing
、colspan
、frameborder
、maxlength
、readonly
、rowspan
、tabindex
、usemap
等,要透過 DOM 的特性取得則必須是 cellSpacing
、colSpan
、frameBorder
、maxLength
、readOnly
、rowSpan
、tabIndex
、useMap
等。
若使用 getAttribute
則通常不用注意大小寫。例如若要取得某個 <input>
元素的 readonly
屬性,則使用 getAttribute('readonly')
、getAttribute('readOnly')
、getAttribute('Readonly')
等任意大小寫組合都是可以的。
float
特性是 JavaScript 的保留字(雖然目前沒有使用),標準使用了 cssFloat
名稱。
或許存取屬性最常見的,就是取得表單中某個 <input>
標籤中的 value
屬性。例如:
<input id="username" name="user" value="caterpillar">
可以透過以下來取得欄位值:
let username = document.getElementById('username').value;
有個初學者常犯的錯誤,例如,想取得 HTML 文件中 <a>
標籤間的文字,卻撰寫如下:
let text = document.getElementsByTagName('a')[0].value;
這是錯的!<a>
上面並沒有 value
屬性,對應的物件上也沒有 value
特性,而且要記得,文字也是一個節點,所以要先取得文字節點,也就是 <a>
的子節點,再使用 data
(定義在 Text
)或 nodeValue
(定義在 Node
)取得文字本身:
let a = document.getElementsByTagName('a')[0];
let text = a.firstChild.data;
基於 CSS 選擇器
可以透過 querySelector
、querySelectorAll
方法,搭配〈CSS 選擇器語法〉來選取元素,底下舉幾個簡單的例子,像是…
let testDiv = document.getElementById('test');
可以使用底下程式達到相同效果:
let testDiv = document.querySelector('#test');
querySelector
始終傳回第一個匹配的元素,因此像是標籤選擇器:
let div = document.querySelector('div');
只會傳回第一個遇到的 <div>
標籤,如果想要取得全部的標籤,可以使用 querySelectorAll
,也就是底下這個片段:
let ps = document.getElementsByTagName('p');
可以改成:
let ps = document.querySelectorAll('p');
querySelectorAll
會傳回 NodeList
,可透過 []
來指定索引取得個別元素。
先前看過的:
let articles = document.getElementsByClassName('article');
改用 querySelectorAll
的話,可以寫成:
let articles = document.querySelectorAll('.article');
基本上,getElementsByTagName
、getElementById
、getElementsByClassName
方法能夠取得的元素,就使用相對應的方法,因為效率會比 querySelector
、querySelectorAll
高一些,由於 querySelector
、querySelectorAll
是原生 API,如果過去你使用其他程式庫以程式流程實現的選擇器,可以改成 querySelector
、querySelectorAll
,以獲得更高的效率。