走訪 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),透過元素的 getElementsByTagNamegetElementByIdgetElementsByClassName 方法來取得節點。例如若想取得文件中所有的 <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');

透過 getElementsByTagNamegetElementByIdgetElementsByClassName 方法取得的元素,是以該元素為根的 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 中設置的屬性如 cellspacingcolspanframebordermaxlengthreadonlyrowspantabindexusemap 等,要透過 DOM 的特性取得則必須是 cellSpacingcolSpanframeBordermaxLengthreadOnlyrowSpantabIndexuseMap 等。

若使用 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 選擇器

可以透過 querySelectorquerySelectorAll 方法,搭配〈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');

基本上,getElementsByTagNamegetElementByIdgetElementsByClassName 方法能夠取得的元素,就使用相對應的方法,因為效率會比 querySelectorquerySelectorAll 高一些,由於 querySelectorquerySelectorAll 是原生 API,如果過去你使用其他程式庫以程式流程實現的選擇器,可以改成 querySelectorquerySelectorAll,以獲得更高的效率。

分享到 LinkedIn 分享到 Facebook 分享到 Twitter