# BOM 和 DOM

廣泛來說,在瀏覽器上的 JavaScript 實際包含了:

  • JavaScript 核心 (以 ECMAScript 標準為基礎)
  • BOM (Browser Object Model) - 瀏覽器物件模型
    • JavaScript 與「瀏覽器」溝通的窗口,不涉及網頁內容
    • 完全依賴於瀏覽器廠商實作本身無標準規範
  • DOM (Document Object Model) - 文件物件模型
    • JavaScript 用來控制「網頁」的節點與內容的標準
    • 有 W3C 所制定的標準來規範

# BOM

BOM 是瀏覽器所有功能的核心,與網頁的內容無關。

# BOM 的核心是 window 物件

window 物件提供的屬性主要為 documentlocationnavigatorscreenhistoryframes

在瀏覽器中的 window 物件扮演著兩種角色;

  • ECMAScript 標準裡的「全域物件」
  • JavaScript 用來與瀏覽器溝通的窗口

提醒

只要在「全域作用範圍」內宣告的變數、物件、函式等,都會自動變成「全域物件」的屬性

通常這樣的變數,我們會稱它們叫做「全域變數」,可以透過 window.xxx 的方式取得它們。

在「全域作用範圍」宣告的全域變數還有一個特性,就是無法使用 delete 關鍵字來移除:

var a = 10;
console.log(window.a); // 10

delete window.a; // false
console.log(window.a); // 10
1
2
3
4
5

但若是直接透過指定 window 物件的屬性,則可以刪除:

window.a = 10; // 或直接使用 a = 10;
console.log(window.a); // 10

delete window.a; // true
console.log(window.a); // undefined
1
2
3
4
5

# 瀏覽器內建的對話框

常見的對話框 API 有 alertconfirmprompt 等,因為這些 API 屬於 window 物件下的成員,window 關鍵字是可以省略不用打的。

# DOM

DOM 是一個將 HTML 文件以樹狀的結構來表示的模型,而組合起來的樹狀圖,我們稱之為「DOM Tree」。

在最根部的地方就是 window 物件下的 document。往下可以延伸出一個個的 HTML 標籤,一個節點就是一個標籤,往下又可以再延伸出「文本節點」(text nodes)、「屬性的節點」、「註解節點」 (comment nodes) 等。

而 DOM API 就是定義了讓 JavaScript 可以存取、改變 HTML 架構、樣式和內容的方法,甚至是對節點綁定的事件。JavaScript 就是透過 DOM 提供的 API 來對 HTML 做存取與操作。

# DOM 節點的類型

DOM 節點的類型常見的有下列幾種:

節點類型常數 對應數值 說明
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
1
2

# querySelectorgetElementBy** 的差異

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 是 靜態 的,可以參考下方範例。

<p id="outer">
  <span>span</span>
  <span>span</span>
</p>
1
2
3
4

使用 getElementBy** 實做:

const outer = document.getElementById('outer');
const allSpans = document.getElementsByTagName('span');

console.log(allSpans.length); // 2

outer.innerHTML = '';

console.log(allSpans.length); // 0
1
2
3
4
5
6
7
8

改成 document.querySelector 的寫法:

const outer = document.querySelector('outer');
const allSpans = document.querySelectorAll('span');

console.log(allSpans.length); // 2

outer.innerHTML = '';

console.log(allSpans.length); // 2
1
2
3
4
5
6
7
8

# document.createDocumentFragment()

在 DOM 規範的所有節點之中,DocumentFragment 算是最特殊的一種,它是一種沒有父層節點的「最小化文件物件」。 可以把它看作是一個輕量化的 Document,用如同標準文件一般的方式來保存「片段的文件結構」。

const ul = document.getElementById('myList');

// 建立一個 DocumentFragment,可以把它看作一個「虛擬的容器」
const fragment = document.createDocumentFragment();

for (let i = 0; i < 3; i++) {
  // 生成新的 li,加入文字後置入 fragment 中。
  const li = document.createElement('li');
  li.appendChild(document.createTextNode('Item ' + (i + 1)));
  fragment.appendChild(li);
}

// 最後將組合完成的 fragment 放進 ul 容器
ul.appendChild(fragment);
1
2
3
4
5
6
7
8
9
10
11
12
13
14

透過操作 DocumentFragment 與直接操作 DOM 最關鍵的區別在於 DocumentFragment 不是真實的 DOM 結構,所以說 DocumentFragment 的變動並不會影響目前的網頁文件,也不會導致回流(reflow)或引起任何影響效能的情況發生。

也就是說,當需要進行大量的 DOM 操作時,用 DocumentFragment 的效能會比直接操作 DOM 好很多。

# NODE.insertBefore(newNode, refNode)

insertBefore() 方法,則是將新節點 (newNode) 插入至指定的 (refNode) 節點的 前面

const myList = document.getElementById('myList');

const refNode = document.querySelectorAll('li')[1];

// 建立 li 元素節點
const newNode = document.createElement('li');

// 建立 textNode 文字節點
const textNode = document.createTextNode('Hello world!');
newNode.appendChild(textNode);

// 將新節點 newNode 插入 refNode 的前方
myList.insertBefore(newNode, refNode);
1
2
3
4
5
6
7
8
9
10
11
12
13

# NODE.replaceChild(newChildNode, oldChildNode)

replaceChild() 方法,則是將原本的 (oldChildNode) 替換成指定的 (newChildNode)。

const myList = document.getElementById('myList');

const oldNode = document.querySelectorAll('li')[1];

// 建立 li 元素節點
const newNode = document.createElement('li');

// 建立 textNode 文字節點
const textNode = document.createTextNode('Hello world!');
newNode.appendChild(textNode);

// 將原有的 oldNode 替換成新節點 newNode
myList.replaceChild(newNode, oldNode);
1
2
3
4
5
6
7
8
9
10
11
12
13

# NODE.removeChild(childNode)

removeChild() 方法,則是將指定的 (childNode) 子節點移除。

const myList = document.getElementById('myList');

// 取得 "<li>Item 02</li>" 的元素
const removeNode = document.querySelectorAll('li')[1];

// 將 myList 下的 removeNode 節點移除
myList.removeChild(removeNode);
1
2
3
4
5
6
7

# 參考

瀏覽器裡的 JavaScript (opens new window)

DOM API 查找節點 (opens new window)

Node 的建立,刪除與修改 (opens new window)

Last Updated: 2020/11/6 上午2:16:51