文档对象模型 (DOM)

文档对象模型DOM)通过在内存中表示文档的结构(例如表示网页的 HTML),将网页连接到脚本或编程语言。通常它指 JavaScript,尽管将 HTML、SVG 或 XML 文档建模为对象并不是核心 JavaScript 语言的一部分。

DOM 以逻辑树的形式表示文档。树的每个分支都以节点结束,每个节点都包含对象。DOM 方法允许以编程方式访问树。通过它们,你可以更改文档的结构、样式或内容。

节点还可以附加事件处理程序。一旦事件触发,事件处理程序就会执行。

概念与用法

文档对象模型 (DOM) 是用于 Web 文档的编程接口。它表示页面,以便程序可以更改文档结构、样式和内容。DOM 将文档表示为节点和对象;这样,编程语言就可以与页面交互。

网页是一个文档,可以显示在浏览器窗口中,也可以作为 HTML 源代码。在这两种情况下,它都是相同的文档,但文档对象模型 (DOM) 表示允许对其进行操作。作为网页的面向对象表示,它可以用脚本语言(例如 JavaScript)进行修改。

例如,DOM 指定此代码片段中的 querySelectorAll 方法必须返回文档中所有 <p> 元素的列表

js
const paragraphs = document.querySelectorAll("p");
// paragraphs[0] is the first <p> element
// paragraphs[1] is the second <p> element, etc.
alert(paragraphs[0].nodeName);

所有可用于操作和创建网页的属性、方法和事件都组织成对象。例如,表示文档本身的 document 对象,实现用于访问 HTML 表格的 HTMLTableElement DOM 接口的任何 table 对象等等,都是对象。

DOM 是使用多个协同工作的 API 构建的。核心 DOM 定义了描述任何文档及其内部实体的实体。其他 API 会根据需要扩展它,为 DOM 添加新功能和能力。例如,HTML DOM API 为核心 DOM 添加了对表示 HTML 文档的支持,SVG API 添加了对表示 SVG 文档的支持。

什么是 DOM 树?

DOM 树是一种树形结构,其节点代表 HTML 或 XML 文档的内容。每个 HTML 或 XML 文档都有一个 DOM 树表示。例如,考虑以下文档

html
<html lang="en">
  <head>
    <title>My Document</title>
  </head>
  <body>
    <h1>Header</h1>
    <p>Paragraph</p>
  </body>
</html>

它有一个看起来像这样的 DOM 树

The DOM as a tree-like representation of a document that has a root and node elements containing content

尽管上面的树与上面文档的 DOM 树相似,但它们并不相同,因为实际的 DOM 树保留了空格

当网络浏览器解析 HTML 文档时,它会构建一个 DOM 树,然后使用它来显示文档。

DOM 和 JavaScript

前面的简短示例,和几乎所有示例一样,是 JavaScript。也就是说,它用 JavaScript 编写,但 使用 DOM 访问文档及其元素。DOM 不是一种编程语言,但没有它,JavaScript 语言就不会有任何关于网页、HTML 文档、SVG 文档及其组成部分的模型或概念。整个文档、头部、文档中的表格、表格标题、表格单元格中的文本以及文档中的所有其他元素都是该文档的文档对象模型的一部分。它们都可以使用 DOM 和 JavaScript 等脚本语言进行访问和操作。

DOM 不属于 JavaScript 语言,而是用于构建网站的 Web API。JavaScript 也可以在其他上下文中使用。例如,Node.js 在计算机上运行 JavaScript 程序,但提供了一组不同的 API,DOM API 不是 Node.js 运行时核心的一部分。

DOM 被设计成独立于任何特定编程语言,通过一个统一、一致的 API 提供文档的结构化表示。即使大多数 Web 开发人员只会通过 JavaScript 使用 DOM,但 DOM 的实现可以针对任何语言构建,如这个 Python 示例所示

python
# Python DOM example
import xml.dom.minidom as m
doc = m.parse(r"C:\Projects\Py\chap1.xml")
doc.nodeName # DOM property of document object
p_list = doc.getElementsByTagName("para")

有关 Web 上 JavaScript 编写所涉及的技术的更多信息,请参阅 JavaScript 技术概述

访问 DOM

您无需做任何特别的事情即可开始使用 DOM。您可以直接在 JavaScript 中,从一个被称为“脚本”的由浏览器运行的程序中,使用该 API。

当您创建脚本时,无论是在 <script> 元素中内联,还是包含在网页中,您都可以立即开始使用 documentwindow 对象的 API,以操作文档本身或网页中的各种元素(文档的后代元素)。您的 DOM 编程可能像以下示例一样简单,它使用 console.log() 函数在控制台上显示一条消息

html
<body onload="console.log('Welcome to my home page!');">
  …
</body>

由于通常不建议将页面结构(用 HTML 编写)和 DOM 操作(用 JavaScript 编写)混合在一起,因此 JavaScript 部分将在此处分组在一起,并与 HTML 分开。

例如,以下函数创建了一个新的 h1 元素,向该元素添加文本,然后将其添加到文档的树中

html
<html lang="en">
  <head> </head>
  <body>
    <script>
      // create a couple of elements in an otherwise empty HTML page
      const heading = document.createElement("h1");
      const headingText = document.createTextNode("Big Head!");
      heading.appendChild(headingText);
      document.body.appendChild(heading);
    </script>
  </body>
</html>

DOM 接口

以下是 DOM 规范定义的所有接口

本指南介绍了您可用于操作 DOM 层次结构的对象和实际“事物”。在很多方面,理解它们如何工作可能会令人困惑。例如,表示 HTML form 元素的对象从 HTMLFormElement 接口获取其 name 属性,但从 HTMLElement 接口获取其 className 属性。在这两种情况下,您想要的属性都在该表单对象中。

但对象及其在 DOM 中实现的接口之间的关系可能令人困惑,因此本节试图简单介绍 DOM 规范中的实际接口以及它们如何可用。

接口与对象

许多对象实现了多个不同的接口。例如,表格对象实现了一个专门的 HTMLTableElement 接口,其中包含 createCaptioninsertRow 等方法。但由于它也是一个 HTML 元素,table 实现了 DOM Element 参考章节中描述的 Element 接口。最后,由于就 DOM 而言,HTML 元素也是构成 HTML 或 XML 页面对象模型的节点树中的一个节点,因此表格对象还实现了更基本的 Node 接口,Element 就是从该接口派生出来的。

当您获得对 table 对象的引用时,如以下示例所示,您通常会在该对象上互换使用这三个接口,也许您并不知道。

js
const table = document.getElementById("table");
const tableAttrs = table.attributes; // Node/Element interface
for (const attr of tableAttrs) {
  // HTMLTableElement interface: border attribute
  if (attr.nodeName.toLowerCase() === "border") {
    table.border = "1";
  }
}
// HTMLTableElement interface: summary attribute
table.summary = "note: increased border";

基本数据类型

本页面尝试用简单的术语描述各种对象和类型。但您应该注意 API 中传递的许多不同数据类型。

注意:由于绝大多数使用 DOM 的代码都围绕着操作 HTML 文档,因此通常将 DOM 中的节点称为元素,尽管严格来说并非每个节点都是元素。

下表简要描述了这些数据类型。

数据类型(接口) 描述
Document 当成员返回 document 类型的对象(例如,元素的 ownerDocument 属性返回其所属的 document)时,此对象就是根 document 对象本身。DOM document 参考章节描述了 document 对象。
Node 文档中的每个对象都是某种类型的节点。在 HTML 文档中,一个对象可以是元素节点,也可以是文本节点或属性节点。
Element element 类型基于 node。它指的是由 DOM API 成员返回的 element 类型元素或节点。我们不说,例如,document.createElement() 方法返回对 node 的对象引用,我们只说此方法返回刚在 DOM 中创建的 elementelement 对象实现了 DOM Element 接口以及更基本的 Node 接口,这两个接口都包含在本参考资料中。在 HTML 文档中,元素通过 HTML DOM API 的 HTMLElement 接口以及描述特定类型元素功能的其他接口(例如,用于 <table> 元素的 HTMLTableElement)得到进一步增强。
Attr 当成员返回 attribute 时(例如,通过 createAttribute() 方法),它是一个对象引用,公开了一个特殊(尽管很小)的属性接口。属性是 DOM 中的节点,就像元素一样,尽管您可能很少将它们这样使用。

还有一些常见的术语需要记住。例如,通常将任何 Attr 节点称为 attribute,并将 DOM 节点数组称为 nodeList。您会发现这些术语和其他术语将在整个文档中引入和使用。

documentwindow 对象是您在 DOM 编程中通常最常使用的接口对象。简单来说,window 对象代表类似浏览器的事物,而 document 对象是文档本身的根。Element 继承自通用的 Node 接口,这两个接口共同提供了您在单个元素上使用的许多方法和属性。这些元素也可能有专门的接口来处理它们所持有的数据类型,如前一节中 table 对象的示例所示。

废弃的 DOM 接口

文档对象模型已经高度简化。为了实现这一点,DOM Level 3 或更早规范中的以下接口已被移除。它们不再可供 Web 开发人员使用。

  • DOMConfiguration
  • DOMErrorHandler
  • DOMImplementationList
  • DOMImplementationRegistry
  • DOMImplementationSource
  • DOMLocator
  • DOMObject
  • DOMSettableTokenList
  • DOMUserData
  • ElementTraversal
  • Entity
  • EntityReference
  • NameList
  • Notation
  • TypeInfo
  • UserDataHandler

HTML DOM

包含 HTML 的文档使用 Document 接口进行描述,该接口由 HTML 规范扩展,以包含各种 HTML 特定的功能。特别是,Element 接口被增强为 HTMLElement 和各种子类,每个子类代表一个(或一组密切相关)元素。

HTML DOM API 提供了对各种浏览器功能的访问,例如标签和窗口、CSS 样式和样式表、浏览器历史记录等。这些接口将在 HTML DOM API 文档中进一步讨论。

SVG DOM

同样,包含 SVG 的文档也使用 Document 接口进行描述,该接口由 SVG 规范扩展,以包含各种 SVG 特定的功能。特别是,Element 接口被增强为 SVGElement 和各种子类,每个子类代表一个元素或一组密切相关的元素。这些接口将在 SVG API 文档中进一步讨论。

示例

设置文本内容

此示例使用一个 <div> 元素,其中包含一个 <textarea> 和两个 <button> 元素。当用户单击第一个按钮时,我们在 <textarea> 中设置一些文本。当用户单击第二个按钮时,我们清除文本。我们使用

HTML

html
<div class="container">
  <textarea class="story"></textarea>
  <button id="set-text" type="button">Set text content</button>
  <button id="clear-text" type="button">Clear text content</button>
</div>

CSS

css
.container {
  display: flex;
  gap: 0.5rem;
  flex-direction: column;
}

button {
  width: 200px;
}

JavaScript

js
const story = document.body.querySelector(".story");

const setText = document.body.querySelector("#set-text");
setText.addEventListener("click", () => {
  story.textContent = "It was a dark and stormy night...";
});

const clearText = document.body.querySelector("#clear-text");
clearText.addEventListener("click", () => {
  story.textContent = "";
});

结果

添加子元素

此示例使用一个 <div> 元素,其中包含一个 <div> 和两个 <button> 元素。当用户点击第一个按钮时,我们创建一个新元素并将其作为 <div> 的子元素添加。当用户点击第二个按钮时,我们移除子元素。我们使用

HTML

html
<div class="container">
  <div class="parent">parent</div>
  <button id="add-child" type="button">Add a child</button>
  <button id="remove-child" type="button">Remove child</button>
</div>

CSS

css
.container {
  display: flex;
  gap: 0.5rem;
  flex-direction: column;
}

button {
  width: 100px;
}

div.parent {
  border: 1px solid black;
  padding: 5px;
  width: 100px;
  height: 100px;
}

div.child {
  border: 1px solid red;
  margin: 10px;
  padding: 5px;
  width: 80px;
  height: 60px;
  box-sizing: border-box;
}

JavaScript

js
const parent = document.body.querySelector(".parent");

const addChild = document.body.querySelector("#add-child");
addChild.addEventListener("click", () => {
  // Only add a child if we don't already have one
  // in addition to the text node "parent"
  if (parent.childNodes.length > 1) {
    return;
  }
  const child = document.createElement("div");
  child.classList.add("child");
  child.textContent = "child";
  parent.appendChild(child);
});

const removeChild = document.body.querySelector("#remove-child");
removeChild.addEventListener("click", () => {
  const child = document.body.querySelector(".child");
  parent.removeChild(child);
});

结果

读取和修改树

假设作者想改变什么是 DOM 树?文档的标题,并写两个段落而不是一个。以下脚本可以完成此工作

HTML

html
<html lang="en">
  <head>
    <title>My Document</title>
  </head>
  <body>
    <input type="button" value="Change this document." />
    <h2>Header</h2>
    <p>Paragraph</p>
  </body>
</html>

JavaScript

js
document.querySelector("input").addEventListener("click", () => {
  // document.getElementsByTagName("h2") returns a NodeList of the <h2>
  // elements in the document, and the first is number 0:
  const header = document.getElementsByTagName("h2").item(0);

  // The firstChild of the header is a Text node:
  header.firstChild.data = "A dynamic document";

  // Now header is "A dynamic document".

  // Access the first paragraph
  const para = document.getElementsByTagName("p").item(0);
  para.firstChild.data = "This is the first paragraph.";

  // Create a new Text node for the second paragraph
  const newText = document.createTextNode("This is the second paragraph.");

  // Create a new Element to be the second paragraph
  const newElement = document.createElement("p");

  // Put the text in the paragraph
  newElement.appendChild(newText);

  // Put the paragraph on the end of the document by appending it to
  // the body (which is the parent of para)
  para.parentNode.appendChild(newElement);
});

创建一棵树

您也可以完全用 JavaScript 在什么是 DOM 树?中创建这棵树。

js
const root = document.createElement("html");
root.lang = "en";

const head = document.createElement("head");
const title = document.createElement("title");
title.appendChild(document.createTextNode("My Document"));
head.appendChild(title);

const body = document.createElement("body");
const header = document.createElement("h1");
header.appendChild(document.createTextNode("Header"));
const paragraph = document.createElement("p");
paragraph.appendChild(document.createTextNode("Paragraph"));
body.appendChild(header);
body.appendChild(paragraph);

root.appendChild(head);
root.appendChild(body);

事件传播

此示例以一种非常简单的方式演示了事件在 DOM 中如何触发和处理。当此 HTML 文档的 BODY 加载时,一个事件监听器会注册到 TABLE 的顶行。该事件监听器通过执行函数 stopEvent 来处理事件,该函数会更改表格底部单元格中的值。

但是,stopEvent 也调用了一个事件对象方法 event.stopPropagation,该方法阻止事件进一步向上冒泡到 DOM 中。请注意,表格本身有一个 onclick 事件处理程序,当表格被点击时应该显示一条消息。但 stopEvent 方法已停止传播,因此在表格中的数据更新后,事件阶段实际上已经结束,并显示一个警告框以确认这一点。

html
<table id="t-daddy">
  <tr id="tbl1">
    <td id="c1">one</td>
  </tr>
  <tr>
    <td id="c2">two</td>
  </tr>
</table>
css
#t-daddy {
  border: 1px solid red;
}

#c1 {
  background-color: pink;
}
js
function stopEvent(event) {
  const c2 = document.getElementById("c2");
  c2.textContent = "hello";

  // this ought to keep t-daddy from getting the click.
  event.stopPropagation();
  console.log("event propagation halted.");
}

const elem = document.getElementById("tbl1");
elem.addEventListener("click", stopEvent);

document.getElementById("t-daddy").addEventListener("click", () => {
  console.log("t-daddy clicked");
});

显示事件对象属性

此示例使用 DOM 方法在一个表格中显示 onload event 对象的所有属性及其值。它还展示了一种有用的技术,即使用 for...in 循环遍历对象的属性以获取它们的值。

事件对象的属性在不同浏览器之间差异很大,WHATWG DOM 标准列出了标准属性,但许多浏览器已大大扩展了这些属性。

将以下代码放入一个空白文本文件并将其加载到各种浏览器中,您会惊叹于不同数量和名称的属性。您可能还想在页面中添加一些元素,并从不同的事件处理程序调用此函数。

html
<h1>Properties of the DOM <span id="eventType"></span> Event Object</h1>
css
table {
  border-collapse: collapse;
}
thead {
  font-weight: bold;
}
td {
  padding: 2px 10px;
}

.odd {
  background-color: #efdfef;
}
.even {
  background-color: white;
}
js
function showEventProperties(e) {
  function addCell(row, text) {
    const cell = row.insertCell(-1);
    cell.appendChild(document.createTextNode(text));
  }

  const event = e || window.event;
  document.getElementById("eventType").textContent = event.type;

  const table = document.createElement("table");
  const thead = table.createTHead();
  let row = thead.insertRow(-1);
  const labelList = ["#", "Property", "Value"];
  const len = labelList.length;

  for (let i = 0; i < len; i++) {
    addCell(row, labelList[i]);
  }

  const tbody = document.createElement("tbody");
  table.appendChild(tbody);

  for (const p in event) {
    row = tbody.insertRow(-1);
    row.className = row.rowIndex % 2 ? "odd" : "even";
    addCell(row, row.rowIndex);
    addCell(row, p);
    addCell(row, event[p]);
  }

  document.body.appendChild(table);
}

showEventProperties(event);

规范

规范
DOM

另见