走訪 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,以獲得更高的效率。


