什么是DOM—文档对象模型通俗易懂的解释

235 阅读11分钟

现代网页是动态的。这意味着我们需要一种合适的、方便的方式来修改和操作网络文档的结构。

例如,在HTML文档中的这种修改,通常采取创建、添加或删除文档中的元素的形式。

在这篇文章中,你将了解什么是文档对象模型(DOM),关于它的一些历史,以及如何使用它来操作网络文档,特别是HTML文档。

什么是文档对象模型(DOM)?

DOM是一个网络接口,由万维网联盟(W3C)开发和发布。这个组织的成立是为了建立万维网的标准。

DOM是一个语言中立的网络API。这意味着,你可以在任何编程语言中实现和采用它。

DOM将网络文档的结构部分表示为可以被访问和操作的对象。换句话说,DOM允许你作为一个软件开发者做以下事情:

  • 创建和构建网络文档。
  • 浏览网络文档的结构。
  • 添加、修改或删除网络文档中的元素和内容。

DOM的历史

DOM的历史是相对于作为第一批广泛使用的脚本语言的JavaScript和JScript而言的。这些语言有助于实现网页的交互性。

在W3C制定标准的DOM规范之前,JavaScript和JScript有不同的方法来实现对HTML文档的操作。

这些让你以这种方式操纵HTML文档的有限方法和接口成为DOM 0级

1998年,W3C完成了第一个标准DOM规范的草案,成为所有浏览器的推荐标准。这个标准DOM规范成为DOM 1级。DOM 1级提供了一个操作HTML和XML文档的综合模型。

2000年,W3C发布了DOM二级,引入了诸如getElementById() 等方法,以及一个标准化的事件模型和对XML命名空间和CSS的支持。

2004年发布的DOM 3级,增加了对XPath和键盘事件处理的支持。而在2015年底,最新的DOM规范,即DOM Level 4,成为了一个公布的标准。

什么是DOM树?

由DOM创建的结构表示非常像一棵树。它里面有几个被称为节点的对象。

浏览器使用它从HTML文档中建立的DOM树状表示法来决定在网页上渲染什么。例如,DOM树的视觉表示法是这样的。

DOM-tree

DOM树

上述DOM树的HTML文档看起来是这样的:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>TITLE</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <div>
        <h1>HELLO WORLD</h1>
    </div>
    <a href="link text">Document Object Model</a>
</body>
</html>

DOM中的节点与元素

通常情况下,开发人员会将节点与元素混淆。因此,我们应该在本文中及早区分这两者,以避免混淆。

节点是一个网页文档所由的所有组件。换句话说,一个网页是节点的集合。

一个元素是文档中的一种节点。例如,DOM属性nodes.childNodes ,当在一个父节点上使用时,将返回包含在该指定的父节点中的所有不同节点。

在下面的例子中,childNodes属性被用在上面给出的HTML文档的<body> 元素节点上:

//javascript content

//select the <body> element node with the DOM method querySelector
const body = document.querySelector('body') 
//select the children nodes with the <body> element node with the DOM property node.childNodes
const childrenNodes = body.childNodes
//console log the children nodes
console.log(childrenNodes)//NodeList(5) [text, div, text, a, text]

注意,nodeList 中有五个项目。这是因为我们有另一种类型的节点,即文本节点,与<body> 元素节点中的元素节点不同。

为了进一步研究这个问题,在你的控制台中进行以下步骤:

  1. 点击 "nodeList "前的下拉图标
  2. 通过点击 "test "前的下拉图标选择文本节点
  3. 检查下拉列表选项中的textContent选项

如果你按照上述说明,你会看到第一个文本节点的测试内容是"/n"。这是一个文本节点,表示在<body> 元素节点、<div> 元素节点和<a> 元素节点之后的一个新行:

Screenshot--143--1

DOM树中节点之间的关系

DOM树中的节点在DOM树中相互之间有层次关系。它们是由它们在DOM树中相对于对方的位置来定义的。

这些是上面DOM树图示中的节点位置:

  • 根节点:根节点总是位于DOM树的顶端。在一个HTML文档中,根节点总是<html> 元素。
  • 子节点:子节点是嵌入另一个节点内的节点。在上面的插图中,<head><body> 元素是<html> 元素的子女。
  • 下属节点:任何在层次秩序中位于另一个节点下面的节点都是位于它上面的节点的子孙。例如,虽然<h1> 元素不是<body> 元素的直接子女,但它是<body> 和根<html> 元素的后裔。
  • 父节点:任何在它里面有另一个节点的节点都是一个父节点。例如,<body> 元素是上例中<div><a> 元素的父节点。注意,只有元素类型的节点才能成为父节点。
  • 兄弟姐妹节点:在DOM树中处于同一层次的节点是兄弟姐妹节点。例如,上例中的<div><a> 元素是兄弟姐妹。
  • 叶子节点:元素内部的文本是叶子节点。这是因为它们不能有自己的子节点。

HTMLCollection vs nodeList

为了操作DOM树,你需要一种方法来选择其中的单个项目或项目的集合。

你可以使用像JavaScript这样的编程语言,通过使用DOM提供的一些方法来选择DOM树中的一个项目或一个项目集合。

方法getElementById()querySelector() 可以选择单个项目。方法getElementsByClassName(),getElementsByTagName(), 或querySelectorAll() 可以选择一个项目的集合。

在DOM树中,根据选择项目集合的方法,我们可以得到一个HTMLCollection或一个NodeList。getElementsByClassName()getElementsByTagName() 方法返回HTMLCollection,而querySelectorAll 则返回nodeList。

HTMLCollection和nodeList有一些相似和不同之处。它们在以下方面有相似之处:

  • 它们是类似数组的对象
  • 它们是项目的集合
  • 它们可以通过使用Array.from() 方法转换为数组
  • 它们都有一个基于零的索引
  • 它们都可以用for...循环进行迭代
  • 它们有一个长度属性
  • 它们没有可用的数组方法

下面是一个HTML文档和JavaScript代码样本,以强调这些相似之处:

<!-- html documant -->

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
   <ul>
    <li>item one</li>
    <li>item two</li>
    <li>item three</li>
    <li>item four</li>
   </ul>

    <script src="main.js"></script>
</body>
</html>
    
//javascript content

const listItemsHtmlCollection = document.getElementsByTagName("li")
console.log(listItemsHtmlCollection) // HTMLCollection(4) [li, li, li, li]

const listItemsNodeList = document.querySelectorAll("li")
console.log(listItemsNodeList) // NodeList(4) [li, li, li, li]

你可以从上面看到,虽然getElementsByTagName 返回一个HTMLCollection,其中的项目与指定的<li> 标签相匹配,但querySelectorAll 返回一个nodeList。

现在,让我们使用for...循环来迭代两个集合:

for(let i = 0; i < listItemsHtmlCollection.length; i++) {
    listItemsHtmlCollection[i].style.color = 'red'
}

for(let i = 0; i < listItemsHtmlCollection.length; i++) {
    listItemsHtmlCollection[i].style.color = 'red'
}

在这两种情况下,文本的颜色将被改变为红色:

Screenshot--145-

现在让我们删除for...循环的迭代,使用map的数组方法来迭代两个集合:

listItemsHtmlCollection.map( element => element.style.color = 'red' )
listItemsNodeList.map( element => element.style.color = 'red' )

Screenshot--146-

为了成功地使用map数组方法,你必须像这样用Array.from() 方法将两个项目转换为数组:

Array.from(listItemsHtmlCollection).map( element => element.style.color = 'red' )
Array.from(listItemsNodeList).map( element => element.style.color = 'red' )

HTMLCollection和nodeList有两个主要的不同之处。它们是:

  • nodeList有一些内置的方法和属性,在HTMLCollection中是没有的。这些方法包括forEach()entries 方法,用于遍历一个nodeList。属性包括keys属性和value属性。
  • 一个HTMLCollection总是活的,而一个nodeList可以是活的或静态的。如果DOM树中的变化自动更新了一个节点集合,那么这个节点集合就是活的。如果DOM树的变化不影响集合,那么它就是静态的。DOM变化可以是增加一个新的节点或删除一个现有的节点。DOM方法,如getElementById()getElementsByClassName() 返回HTMLCollections,它总是活的。querySelectorAll() 方法 返回一个静态的nodeList。

DOM HTML方法

DOM一级核心、DOM二级核心和DOM三级核心引入了若干方法,允许网络开发者操作DOM树。这些方法中的一些如下:

createElement() DOM方法

createElement() 方法创建一个指定为其参数类型的元素:

//html document

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
   <ul>
    <li>item one</li>
    <li>item two</li>
    <li>item three</li>
    <li>item four</li>
   </ul>

    <script src="main.js"></script>
</body>
</html>
//javascript content

//select the parent element
const list = document.querySelector('ul')
//create a new element
const listItem = document.createElement('li')
//make the newly created element a child of the parent
list.appendChild('listItem')
console.log(list)

现在检查控制台中的列表控制台记录。你会看到在<ul> 父元素中现在有五个<li> 元素:

Screenshot--147--1

createTextNode() DOM方法

createTextNode() 方法用指定的字符串作为参数创建一个文本节点。让我们把文本添加到我们上面创建的<li> 元素中:

//javascript content

const listText = document.createTextNode("item five")
listItem.appendChild(listText)

现在保存你的JavaScript文件并重新加载你的网页:

Screenshot--149-

appendChild() DOM方法

appendChild() 方法在父节点的子节点列表的末尾添加一个节点。

如果指定的子节点是文档中的一个现有节点,appendChild() ,将其从DOM树上的当前位置移动到新的位置。我们在前面使用这个方法使我们新创建的<li> 元素成为<ul> 元素的子节点。

getElementById() DOM方法

该方法选择并返回其ID为参数的元素。如果没有这样的元素存在,该方法返回null。让我们在HTML文档中为我们的<ul> 元素添加一个id属性,并给它一个红色边框:

//html document

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
   <ul id="ulList">
    <li>item one</li>
    <li>item two</li>
    <li>item three</li>
    <li>item four</li>
   </ul>


    <script src="main.js"></script>
</body>
</html>
//javascript content

const ulList = document.getElementById("ulList") 
ulList.style.border = '2px solid red'

Screenshot--150-

getElementsByClassName() DOM方法

getElementsByClassName() 方法选择所有具有指定类名的元素,并按照它们在DOM树上出现的顺序将它们作为HTMLCollection返回。

你可以通过索引号访问HTMLCollection中的单个元素。让我们给我们的HTML文档中的前两个<li> 元素添加一个类属性,并像这样把它们的文本颜色改为红色:

//html document

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
   <ul id="ulList">
    <li class="itemOneAndTwo">item one</li>
    <li class="itemOneAndTwo">item two</li>
    <li>item three</li>
    <li>item four</li>
   </ul>


    <script src="main.js"></script>
</body>
</html>
//javascript content

//select by elements one and two by their class name
const itemOneAndTwo = document.getElementsByClassName("itemOneAndTwo")
//change text color to red with use of index
itemOneAndTwo[0].style.color = 'red'
itemOneAndTwo[1].style.color = 'red'

Screenshot--151-

getElementsByTagName() DOM方法

getElementsByTagName() 方法返回一个HTMLCollection,该HTMLCollection中所有元素的标签名称被指定为其参数,按照它们在DOM树上出现的顺序:

让我们用getElementsBytagName() 方法选择<li> 元素,并将其字体样式改为斜体:

//javascript content

const liTags = document.getElementsByTagName("li") 
for(let i = 0; i < liTags.length; i++) {
    liTags[i].style.fontStyle = 'italic'
}

Screenshot--152-

querySelector() DOM方法

querySelector() 方法接受任何CSS字符串选择器作为一个参数。然后,它使用指定的选择器来选择文档中与该选择器匹配的第一个元素。

让我们用querySelector() 方法选择我们的前两个<li> 元素,并将它们的文本颜色改回为黑色:

//javascript content

const querySelectItem = document.querySelector("itemOneAndTwo")
querySelectItem.style.color = 'black'
 

Screenshot--153-

注意,只有第一个列表项的颜色被改成了黑色。

querySelectorAll() DOM方法

querySelectorAll() 方法,就像querySelector 方法一样,接受任何CSS字符串选择器作为其参数。然后,它使用指定的CSS字符串选择器来选择所有符合该选择器的元素,把它们放在一个nodeList中,并返回该nodeList。

现在,让我们用它来改变列表项中的所有文本为绿色:

//javascript content

const querySelectAllItems = document.querySelectorAll("li")
for(let i = 0; i < querySelectAllItems.length; i++) {
    querySelectAllItems[i].style.color = 'green'
}  

Screenshot--154-

setAttribute() DOM方法

setAttribute() 方法为一个元素添加一个新的属性名称。如果该元素中已经存在一个具有该名称的属性,其值将改变为参数中设置的值。

该方法接受两个参数。第一个参数是你想要创建的属性的名称。第二个参数是要在该属性上设置的值,它总是一个字符串。

让我们用它来给我们的第三个项目一个类属性,并将文本颜色改为黑色:

//javascript content

const itemThree = querySelectAllItems[2] 
itemThree.setAttribute("class", "attributeValue")  
const attributeValue = document.querySelector('.attributeValue')
attributeValue.style.color = 'black'

Screenshot--155-

removeAttribute() DOM方法

removeAttribute() 方法删除了一个指定的属性。它接受要删除的属性的名称作为参数。让我们从<ul> 父元素中移除id属性,并使用移除的id来移除其红边。

//javascript content

//remove attribute
ul.removeAttribute('id')
ul.style.border = 'none'

现在保存你的JavaScript文件并重新加载你的网页。注意,边框仍然存在。如果你检查控制台,你会看到一个错误信息,说明ul不再被定义。

contains() DOM方法

如果一个节点是一个节点的后代,contains() 方法返回真,否则返回假:

<--HTML document--> 
<body>   
    <h1>Heading</h1> 
</body>
//javascript content
const body = document.querySelector('body')
const h1Element = document.querySelector('h1')
console.log( body.contains(h1Element) ) // true

item() DOM方法

item() 方法在一个集合上使用时,会返回在其参数中指定的索引上的项目:

<--HTML document--> 
<body>   
    <p>Paragraph</p> 
    <p>Paragraph</p> 
</body>
//javascript content
const pElements = document.querySelectorAll("p") 
console.log(pElements.item(0)) // <p></p>

hasChildNodes() DOM方法

hasChildNodes 方法如果被调用的元素在其内部有子节点,则返回true,否则返回false:

<--HTML document--> 
<body>   
    <p>Paragraph</p> 
    <p>Paragraph</p> 
</body>
//javascript content 
const pElements = document.querySelector("body") 
console.log(body.hasChildNodes()) // true

什么是DOM事件?

为了使我们的网页通过启动自动响应或网页上的事件来实现逻辑上的互动,我们需要事件。

DOM事件是:

在你编程的系统中发生的动作或事件,系统会告诉你,以便你的代码可以对它们做出反应。(来源:MDN)

事件的一个常见的例子是,当用户点击表单中的提交按钮时,作为对点击的回应,它将提交用户输入的数据。

另一个例子是,当用户点击一个菜单图标,然后触发一个下拉导航或选项。

你可以使用脚本语言,如JavaScript,在DOM树内的元素上注册事件处理程序或监听器,当指定的事件发生时运行。

一个事件处理程序是一个:

当事件发生时运行的代码块(通常是你作为程序员创建的一个JavaScript函数)。当这样一个代码块被定义为响应一个事件而运行时,我们说我们正在注册一个事件处理程序。 (来源:MDN)

在DOM树中用于元素的事件的例子包括:

  • 点击:点击事件是指在网页上的一个元素上的鼠标下移或鼠标上移。
  • 按键:当键盘上的按键被按下时,就会发生一个按键事件。
  • mouseover:当指向性设备移动到一个元素上时,就会发生鼠标移位事件。
  • dblclick(双击):当网页上的一个元素上出现双击事件时,就会发生双击事件。
  • 提交:当一个表单被提交时,会发生一个提交事件。

总结

DOM是现代网络动态的骨干。它将网页文档的每一块都表示为一个对象,并为编程语言提供必要的方法来操作和修改每一块。

参考资料和进一步阅读

  1. dom.spec.whatwg.org/
  2. www.w3.org/TR/1998/REC…
  3. www.w3.org/TR/1998/REC…
  4. www.w3.org/TR/DOM-Leve…
  5. www.w3.org/TR/DOM-Leve…