理解DOM
特别声明,本文整个思路来源于@Tania Rascia的系列文章《Understanding the DOM》。
DOM是Document Object Model的简称,是网站具有交互性的重要组成部分。它是一个接口,允许编程语言操作网站的内容、结构和样式。JavaScript是浏览器中连接到DOM的客户端脚本语言。
欲要更好的操作好Web网站,我们就很有必要的理解DOM。而且这也是学习JavaScript很重要的部分之一。接下来我们将从以下几个部分来展开对DOM的理解和学习。
- DOM简介
- 理解DOM树和节点
- 如何访问DOM中的元素
- 如何遍历DOM
- 如何更改DOM
那我们开始吧!
DOM简介
Web网站都会有一些事件交互的行为,比如说图像幻灯片之间进行旋转,当用户试图提交不完整表单时显示错误信息,或者切换导航菜单等,这一切的一切其实都是JavaScript访问和操作DOM的结果。这样一来,我们就很有必要去了解DOM是什么,如果处理DOM,以及HTML源代码和DOM之间的区别等等。
尽管DOM与语言并没有太大关系,或者说其是创建独立于特定的编程语言,但在我们这篇文章中所要聊的都是关于JavaScript对HTML中DOM的操作。
先决条件
为了有效地了解DOM以及它与Web工作的关系,建议你对HTML和CSS有一定的了解,而且熟悉基本的JavaScript语法和代码结构。这样有益于帮助大家更好的理解后续的内容。
DOM是什么
在最基本的层面上来说,Web网站是由一个HTML文档组成。我们使用的浏览网站的浏览器是一个解释HTML和CSS的程序,它把样式、内容和结构呈现给你 —— 也就是你在浏览器看到的样子。
对于浏览器打下一个网址,它所经历的事情和所涉及到的知识可多了,因此也有一个最经典的面试题:浏览器打开一个网页时都发生了什么?。
除了解析HTML结构和CSS样式这外,浏览器还创建了文档对象模型,也就是我们要说的DOM。该模型允许JavaScript以对象的形式访问网站文档的文本内容和元素。
JavaScript是一种交互式语言,通过它可以更容易地理解新概念。让我们来创建一个最简单的Web页面。创建一个index.html
文件,并将其保存在一个新的项目目录中。
<!DOCTYPE html>
<html lang="en">
<head>
<title>Learning the DOM</title>
</head>
<body>
<h1>Document Object Model</h1>
</body>
</html>
熟悉HTML的都知道,上面这个代码是Web页面最基础的架子。它包含了网站文档的最基本东西:文档类型(DOCTYPE
)、带有<head>
和<body>
的<html>
标签。
出于我们的目的,我将使用Chrome浏览器,当然你也可以使用你自己喜欢的浏览器来获得类似的输出。使用Chrome浏览器,打开刚才创建的index.html
文件。在浏览器看到的Web页面将是一个最简单的页面,就只有一个标题 —— “Document Object Model” 。通过浏览器的开发者工具,在“Elements”选项卡下,你将看到index.html
页面对应的DOM。比如下图所示:
在这种情况下,它看起来与我们刚刚编写的HTML源代码完全相同 —— 一个DOCTYPE
,以及几个HTML标签元素。浏览器开发者工具中,将鼠标悬停在元素上时,Web页面中将会突出显示相应的元素内容。HTML元素左侧的小箭头,允许您切换嵌套元素的视图(将折叠的部分展开显示):
document
对象
document
对象是一个内置对象,它有许多属性和方法,可以用来访问和修改Web页面。为了理解如何使用DOM,你必须了解对象如何在JavaScript中工作。如果你对对象的概念一点都不了解,那么可以先查阅一些JavaScript中对象相关的知识。
在浏览器中访问前面创建的index.html
页面,并且使用开发者工具进入到Console
选项卡中。然后在开发者工具中输入document
,并按回车键。将看到的输出内容与Elements
选项卡中看到的内容相同。
这里键入document
只是为了更好的帮助大家巩固document
对象是什么以及如何修改它。后续的内容你将会发现我在此处所说的。
DOM和HTML源代码的区别是什么?
目前,在这个示例中,HTML源代码和DOM似乎是完全相同的。在两个实例中,浏览器生成的DOM将与HTML源代码不同:
- DOM被客户端(可能是浏览器)JavaScript修改
- 浏览器会自动修复源代码中的错误
接下来简单的演示如何通过客户端JavaScript修改DOM。比如在开发者工具中输入document.body
,得到的结果如下图所示:
document
是一个对象,body
是我们用点符号(.
)访问的对象属性。输入document.body
将会在控制台中输入body
元素自身和它里面的一切。
在控制台中,我们可以在这个Web页面上改变body
对象的一些属性。比如修改style
的属性,将页面的背景颜色改变紫红色。只需要控制台中输入:
document.body.style.backgroundColor = 'fuchsia'
控制台上输入上面的代码回车后,将看到Web页面的实时更新,body
的background-color
也将变成fuchsia
。结果如下:
这个时候在键入document.body
之后,你将看到DOM发生了变化:
上面输入的是JavaScript代码,将fuchsia
分配给body
元素的background-color
,而且现在变成是DOM的其中一部分。
但是,你现在在页面中用鼠标右键单击,并在出来的菜单项中选择“查看页面源代码”选项。你会注意到,网站源代码并不包含通过JavaScript添加的新样式属性。网站的来源不会改变,也不会受到客户端JavaScript的影响。如果你刷新页面,我们在控制台添加的新代码将会消失。
当源代码中出现错误时,DOM可能具有不同于HTML源代码的另一个实例。其中一个常见的例子是table
标记 —— 就是需要一个tbody
标签,但很多开发人员,往往在写HTML代码时忘了添加。浏览器将会自动更正错误并添加tbody
标签。另外DOM还将修复未关闭的HTML标签。
经过上面的学习,知道怎么访问文档对象,以及如何在浏览器的开发者工具中使用JavaScript来更新文档对象的属性。另外知道了HTML源代码和DOM之间的区别之处。有关于DOM更深入的信息,还可以查看Mozilla上有关于文档对象模型(DOM)资料。
理解DOM树和节点
DOM通常被称为DOM树,它由称为节点的对象树组成。在介绍DOM的过程中,了解到了文档对象是什么,如何访问文档对象并使用控制台修改其属性,以及HTML源代码和DOM之间的区别。
接下来将会回顾一些HTML相关的术语,这对于理解如何使用JavaScript处理DOM非常重要。除此之外,接下来的内容将帮助我们了解DOM树,DOM节点是什么,以及如何识别最常见的节点类型。
HTML术语
理解HTML和JavaScript术语对于理解如何处理DOM非常重要。让我们简要的回顾一下一些HTML相关的术语。
先来看一下这个HTML元素:
<a href="index.html">Home</a>
这是一个锚元素,它指向index.html
。
a
是一个HTML标签href
是一个HTML标签属性index.html
是属性值Home
是一个文本
在开始和闭合标签之间的所有内容组合生成整个HTML元素。把这个链接代码放到index.html
中之后就变成下面这样:
<!DOCTYPE html>
<html lang="en">
<head>
<title>Learning the DOM</title>
</head>
<body>
<h1>Document Object Model</h1>
<a id="nav" href="index.html">Home</a>
</body>
</html>
使用JavaScript访问元素的最简单方法是id
属性。所以在链接中添加了个id
为nav
的值。
这个时候,使用getElementById()
方法来访问所需要的元素。比如在控制台中输入下面的内容:
document.getElementById('nav')
通过上面的代码,就获取到了index.html
中id
为nav
的元素:
正如上面所演示的一样,使用getElementById()
可以选择到我们想要的元素。如果我们想多次访问这个nav
链接时就得多次输入那个对象和方法,因此为了更容易的使用它,我们可以将其放入一个变量中。比如:
let navLink = document.getElementById('nav')
navLink
变量包含我们的锚元素。从这里,我们可以很容易的修改它的属性和值。例如,我们可以通过更改href
属性来更改链接的位置:
navLink.href = 'https://wwww.w3cplus.com'
我们也可以使用textContent
属性来改变锚元素的文本内容:
navLink.textContent = '点击这里访问W3cplus'
这个时候在控制台中输入navLink
之后,你看到效果如下,同时对应的Web页面也有所更改:
同样的,如果你刷新页面,这一切都将恢复到最初的状态。
此时,你应该了解了如何使用document
的方法来访问元素,如何将选到的元素赋值给一个变量,以及如何修改元素中的属性和值。
DOM树和节点
在DOM中,所有的项都定义为节点。DOM中的节点类型有很多种,但有三个主要的节点类型是我们经常使用的:
- **元素(
Element
)**节点 - **文本(
Text
)**节点 - **注释(
Comment
)**节点
HTML中的元素被称为DOM中的元素节点。元素之外的任何单独的文本都是文本节点,而HTML注释则是一个注释节点。除了这三种节点类型之外,document
也是一个文档节点,而且它是其他所有节点的根节点。
DOM是由嵌套节点的树结构组成,它通常被称为DOM树。你可能熟悉是族谱,它由父母、孩子和兄弟姐妹组成。DOM中的节点也被称为父节点、子节点和兄弟节点,这一切都取决于它们与其他节点的关系。
为了演示,咱们修改一下前面的index.html
文件。将添加文本、注释和元素节点。
<!DOCTYPE html>
<html lang="en">
<head>
<title>Learning About Nodes</title>
</head>
<body>
<h1>An element node</h1>
<!-- A comment node -->
A text node.
</body>
</html>
html
元素节点是父节点。head
和body
是兄弟节点,也是html
的子节点。body
包含三个子节点,它们都是兄弟节点 —— 节点的类型不会改变其嵌套的级别。
在使用HTML生成的DOM时,HTML源代码的缩进会创建许多空的文本节点,这在开发者工具中的
Elements
选项卡中是不可见的。更详细的内容可以阅读DOM中的空白相关的资料。
确定节点类型
文档中的每个节点都有一个节点类型,通过nodeType
属性可以访问该节点类型。Mozilla提供了所有节点类型常量的最新列表。下面是我们最常见节点类型:
节点类型 | 节点常量值 | 示例 |
---|---|---|
ELEMENT_NODE |
1 |
比如 <body> 元素 |
TEXT_NODE |
3 |
不属于元素的文本 |
COMMENT_NODE |
8 |
<!-- A comment node --> |
在开发者工具中的Elements
选项卡中,你可能会注意到,每当单击并高亮显示DOM中的任一行时,将会在它的旁边出现==$0
的值。
这是一种非常方便的方法,可以在控制台中输入$0
来访问当前元素。
在控制台中,使用nodeType
属性可以获取当前选定节点的节点类型,比如:
选择body
元素后,$0.nodeType
输出的值是1
,可以看到它与ELEMENT_NODE
相关。对于注释和文本执行相同的操作,它们将分别输出的值是8
和3
。
当你知道如何访问元素时,你就可以看到对应的节点类型。比如:
document.body.nodeType; // => 1
除了nodeType
之外,你还可以使用nodeValue
属性获取文本或注释节点的值,也可以使用nodeName
属性来获取元素的标签名。
用事件修改DOM
到目前为止,我们只看到了如何在浏览器开发者工具的控制台中修改DOM,这样一来只是暂时的修改,因为只要刷新了页面,每次更改都会丢失。比如前面的示例,在控制台中修改body
的background-color
。那么接下来,我们可以结合我们所学到的内容,在页面中创建一个交互按钮,当点击按钮后再修改body
的background-color
。
同样在index.html
中,咱们添加一个id
为changeBackground
的<button>
元素。并且在</body>
前添加一个<script></script>
标签,主要用来放置操作DOM所需要的JavaScript代码。
<body>
<h1>Document Object Model</h1>
<button id="changeBackground">Change Background Color</button>
<script>
// JavaScript操作DOM的代码写在这里
</script>
</body>
JavaScript中的事件是用户所采取的操作。当用户将鼠标悬浮在某个元素上,或者单击某个元素,或者按下键盘上的特定键时,这些都是事件类型。在我们这个示例中,希望监听button
上的事件,当用户单击它时执行操作,也就是改变body
的background-color
。我们可以在按钮中添加一个click
事件来实现这个效果。
首先通过getElementById()
方法找到这个button
元素,并将其赋值给一个变量:
let button = document.getElementById('changeBackground');
使用addEventListener()
方法来监听button
上的click
事件,并执行一次单击后的函数。
button.addEventListener('click', () => {
// 单击按钮对应要做的事情
})
最后在函数内部,将使用前面的代码来修改body
的background-color
:
document.body.style.backgroundColor = 'fuchsia';
这样把JavaScript的代码一起放置在<script>
标签中:
let button = document.getElementById('changeBackground');
button.addEventListener('click', () => {
document.body.style.backgroundColor = 'fuchsia';
})
保存你的index.html
文件,然后在浏览器中重新打开这个页面。当你点击按钮时,将会触发事件发生 —— 即修改body
的背景色为fuchsia
:
如何访问DOM元素
为了能够熟练地访问DOM中的元素,有必要了解CSS选择器、语法和术语以及对HTML元素的理解。接下来将介绍几种访问DOM元素的方法:通过ID
、class
、标签和查询选择器。
下面是我们将会介绍的五种访问DOM元素的概述。
获取方式 | 选择器语法 | 方法 |
---|---|---|
ID |
#demo |
getElementById() |
class |
.demo |
getElementsByClassName() |
Element |
h1 |
getElementsByTagName() |
选择器(单个) | querySelector() |
|
选择器 (所有) | querySelectorAll() |
在学习DOM时,在你自己的电脑上键入你想要的示例很重要,这样可以确保你理解并保留你所学习的信息。
为了更好的帮助大家理解如何访问DOM中的元素,把index.html
文件做一下修改。修改后的代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Accessing Elements in the DOM</title>
<style>
html { font-family: sans-serif; color: #333; }
body { max-width: 500px; margin: 0 auto; padding: 0 15px; }
div, article { padding: 10px; margin: 5px; border: 1px solid #dedede; }
</style>
</head>
<body>
<h1>Accessing Elements in the DOM</h1>
<h2>ID (#demo)</h2>
<div id="demo">Access me by ID</div>
<h2>Class (.demo)</h2>
<div class="demo">Access me by class (1)</div>
<div class="demo">Access me by class (2)</div>
<h2>Tag (article)</h2>
<article>Access me by tag (1)</article>
<article>Access me by tag (2)</article>
<h2>Query Selector</h2>
<div id="demo-query">Access me by query</div>
<h2>Query Selector All</h2>
<div class="demo-query-all">Access me by query all (1)</div>
<div class="demo-query-all">Access me by query all (2)</div>
<script>
</script>
</body>
</html>
在index.html
文件中的<script>
中将使用不同的document
方法来访问DOM中的元素。具体的方法先不看,保存上面的代码之后,在浏览器中看到的页面效果有点类似下图这样:
我们将使用上面表格中的不同方法来访问index.html
中的元素。
使用ID
访问元素
在DOM中访问单个元素的最简单方法是通过它唯一的ID来访问。可以使用document
对象中的getElementById()
方法来访问元素中的ID
:
document.getElementById();
为了通过访问ID
获取元素,所以HTML元素必须具有ID属性。在我们的示例中,有一个id
名为demo
的div
元素:
<div id="demo">Access me by ID</div>
在控制台中,通过document.getElementById('demo')
来获取id
为demo
的元素,并且将其赋值给demoId
变量:
const demoId = document.getElementById('demo')
在控制台通过console.log(demoId)
就可以输出我们选中的元素:
为了验证我们选择的元素是对的,可以通过改变元素的border
为red
来确定:
demoId.style.border = '1px solid red'
上面的代码我们都是在浏览器控制台中输入的,前面也提到过了,刷新浏览器就会丢失,如果你想不让其丢失,你可以把相关的代码放置在<script>
标签内。
通过ID
访问元素是在DOM中快速获取元素的一种有效方法。然而,它也有缺点的。ID
必须始终是唯一的,因此getElementById()
方法只能访问到单个元素。如果你想要在整个页面中添加一个功能,那么你的代码将很快变得非常令人讨厌。
使用class
来访问DOM元素
使用class
属性用于访问DOM中的一个或多个指定元素。可以使用getElementsByClassName()
方法获取给定类名的所有元素。
比如我们的index.html
文件中有两个div
都带有类名demo
:
<div class="demo">Access me by class (1)</div>
<div class="demo">Access me by class (2)</div>
同样的,我们在控制台中使用getElementsByClassName()
方法来访问class
为demo
的元素,并将其赋值给一个变量demoClass
:
const demoClass = document.getElementsByClassName('demo')
此时,你可能认为可以像使用ID
那样修改元素。如果我们也像前面介绍ID
访问DOM元素的示例一样,给使用class
的div
添加一个border
样式,比如我们这里将其添加一个orange
的边框。那么结果将会如何呢?
demoClass.style.border = '1px solid orange'
运行的结果并不如你所期待的,控制台将会报出错误信息:
会报错是因为使用getElementsByClassName()
方法并没有得到一个元素,而是得到一个类数组一样的元素。
从上图中可以看出来,我们得到的是一个HTMLCollection
。
在学习《JavaScript中的DOM动态集合》一节中,我们知道,HTMLCollection
是一个类数组,可以使用[]
或者item()
来访问。而且从上图中我们可以看到控制台中输出的结果带有一个length
值。那么我们想要访问到其中的元素就必须使用索引号来访问JavaScript数组。也就是说,我们可以使用[0]
的方式来访问到第一个元素,比如使用demoClass[0]
:
既然通过[0]
的方式能访问到具体元素,那么就可以像ID
选择器中一样,给元素添加样式。比如:
demoClass[0].style.border = '1px solid orange'
通常我们使用class
访问元素时,希望选中文档中的所有指定类的元素,而不像上图一样仅仅是其中的一个。刚才也提到了getElementsByClassName()
方法返回的是一个HTMLCollection
,是一个类数组,那么我们可以通过for
循环来遍历数组中的每一项,让我们不再只选择一个元素:
for (let i = 0, len = demoClass.length; i < len; i++){
demoClass[i].style.border = '1px solid orange'
}
现面index.html
页面中含有demo
类名的元素都添加了border
为1px solid orange
的样式。正如上图所示的效果。
通过标签访问元素
访问页面上多个元素除了上面提到的getElementsByClassName()
方法之外,还可以使用getElementsByTagName()
方法来访问。不同的是前者通过访问指定的class
来获取元素,而后者是通过访问指定的HTML标签来访问元素。比如我们使用getElementsByTagName()
方法来访问index.html
中所有的<article>
标签,并将其赋值给变量demoTag
:
const demoTag = document.getElementsByTagName('article')
console.log(demoTag)
同样的,getElementsByTagName()
类似于getElementsByClassName()
返回的也是一个HTMLCollection
类数组。也就是说,如果我们要给index.html
中所有<article>
元素添加一个blue
的border
,也要使用for
循环来遍历:
for (let i = 0, len = demoTag.length;i < len; i++) {
demoTag[i].style.border = '1px solid blue'
}
查询选择器
如果你有使用jQuery API的相关经验的话,你可能会熟悉jQuery使用CSS选择器访问DOM的方法。
$('#demo');
$('.demo');
$('article');
时至今日,我们在JavaScript中可以使用querySelector()
和querySelectorAll()
方法执行类似jQuery API选择DOM元素的方法.
如果我们要访问单个元素,可以使用querySelector()
方法。比如我们要访问index.html
中id
为demo-query
的元素,我们就可以这样操作:
const demoQuery = document.querySelector('#demo-query')
对于多个元素的选择器(比如class
或标签),使用querySelector()
将返回与查询匹配的第一个元素。如查要选择所有元素,则可以使用querySelectorAll()
方法。比如,我们可以使用querySelectorAll()
方法访问index.html
中所有class
为demo-query-all
的元素:
const demoQueryAll = document.querySelectorAll('.demo-query-all')
querySelectorAll()
方法有点类似于getElementsByClassName()
方法,只不过其返回的是一个NodeList
的类数组。同样的,如果要给所有的class
为demo-query-all
元素添加green
边框效果,不能直接使用,需要使用类似于forEach()
方法对类数组进行遍历:
demoQueryAll.forEach(query => {
query.style.border = '1px solid green'
})
使用querySelector()
方法,在方法中的参数可以使用逗号,
做为分隔符,比如querySelector('div, article')
将匹配到文档中的第一个div
元素以及第一个article
元素。使用querySelectorAll()
方法也可以如何操作,比如querySelectorAll('div, article')
方法将选中文档中所有div
和article
元素。
使用查询选择器方法非常强大,因为你可以像在CSS文件中一样访问DOM中的任何元素或元素组。有关选择器的完整列表,可以查看Mozilla提供的CSS选择器相关资料。
上面的代码都是在浏览器的控制台中输入的,一旦你刷新浏览器,所有的JavaScript将失效。如果你想让效果永久保留,你可以在index.html
的<script>
标签中输入上述的JavaScript代码,或者是单独创建一个.js
文件,然后在<script>
标签中使用src
属性引入你创建的.js
文件。这个已经是JavaScript的一些基础知识,想必你应该很清楚了,这里不再阐述。
如何遍历DOM元素
接下来咱们学习如何在DOM树上移动DOM,比如向上、向下移动DOM,比如从一个分支移到另一个分支。学习这些知识是理解如何使用JavaScript和HTML的关键。
下面我们就围绕着JavaScript如何通过父、子和兄弟属性遍历DOM。
为了能更好的阐述我们要学习的内容,先把index.html
文件的代码改成下面这样:
<!DOCTYPE html>
<html>
<head>
<title>Learning About Nodes</title>
<style>
* { border: 2px solid #dedede; padding: 15px; margin: 15px; }
html { margin: 0; padding: 0; }
body { max-width: 600px; font-family: sans-serif; color: #333; padding: 10px;}
</style>
</head>
<body>
<h1>Shark World</h1>
<p>The world's leading source on <strong>shark</strong> related information.</p>
<h2>Types of Sharks</h2>
<ul>
<li>Hammerhead</li>
<li>Tiger</li>
<li>Great White</li>
</ul>
</body>
<script>
const h1 = document.getElementsByTagName('h1')[0];
const p = document.getElementsByTagName('p')[0];
const ul = document.getElementsByTagName('ul')[0];
</script>
</html>
浏览器刷新之后,看到的效果如下:
根节点
document
对象是DOM中每个节点的根。这个对象实际上是window
对象的一个属性,它是表示浏览器中全局顶级对象。window
对象可以访问工具栏、窗口的宽度和高度、提示和警报等信息。document
由window
内的内容组成。
下面是由每个文档所包含的根元素组成的图表。即使将一个空白HTML文件加载到浏览器中,这三个节点也将被添加到DOM中。
属性 | 节点 | 节点类型 |
---|---|---|
document |
#document |
DOCUMENT_NODE |
document.documentElement |
html |
ELEMENT_NODE |
document.head |
head |
ELEMENT_NODE |
document.body |
body |
ELEMENT_NODE |
由于html
、head
和body
三个元素是任何一个HTML文档必有的元素,可谓是非常常见的元素,所以它们在document
上都有自己的属性。
正如上图所示,你可以在浏览器的开发者工具中打印出上面表格中的四个属性。当然你也还可以测试h1
、p
和ul
元素。因为在script
标记中已将它们指定在对应的变量中,所以将返回元素。
父节点
DOM中的节点被称为父节点、子节点和兄弟节点,这取决于它们与其他节点的关系。任何节点的父节点都位于它之上的一个级别的节点,或者更接近于DOM中的document
。DOM中有两个属性可以访问到父节点:parentNode
和parentElement
。
在index.html
中:
html
是head
、body
和script
的父节点body
是h1
、h2
、p
和ul
父节点,但不是li
的父节点,那是因为li
是body
第二级节点(按照族谱关系来描述,body
是li
的爷爷节点,在这里常常被称为祖先节点)
我们可以通过p
的parentNode
属性来获取到p
的父节点。而其中p
是一个变量,对应的是通过document.getElementsByTagName('p')[0]
获取的第一个p
元素。
p
的父节点是body
,但是我们怎么能得到祖父节点,这是两个层次呢?我们可以通过链接属性来实现:
p.parentNode.parentNode
而parentElement
和parentNode
类似,都表示一个元素的父元素,但两者还是有一定的差异性:
parentNode
是W3C标准规范定义的一个属性,用节点的形式返回一个节点的父节点parentElement
最早是IE浏览器才支持的一个属性,功能基本和parentNode
一致,其也是Firefox 9和DOM4的新功能- 当一个节点的父节点的
nodeType !==1
的时候,即父节点不是Element
的时候,通过parentElement
得到的父节点会是null
比如:
document.body.parentNode; // => the <html> element
document.body.parentElement; // => the <html> element
document.documentElement.parentNode; // => the document node
document.documentElement.parentElement; // => null
由于<html>
元素( document.documentElement
)没有作为元素的父元素,因此parentElement
为null
。 (还有其他更不可能的情况, parentElement
可能为null
,但您可能永远不会遇到它们。)
只要记住,名称中带有
element
的属性总是返回Element
或null
。 没有的属性可以返回任何其他类型的节点。通常,遍历DOM时,parentNode
更常用。
子节点
子节点刚好与父节点相反,节点的子节点是在它下面的一个级别的节点。超过一层嵌套的任何节点通常称为后代节点。
DOM中子节点对应的属性有:
childNodes
:子节点firstChild
:第一个子节点lastChild
:最后一个子节点children
:元素的子节点firstElementChild
:第一个子元素节点lastElementChild
:最后一个子元素节点
childNodes
属性返回包含指定节点的子节点的集合,该集合为即时更新的集合(是一个动态集合)。比如,你期望获取到ul
元素的子节点li
元素,就可以像下面这样操作:
ul.childNodes
输出的结果如下:
正如上图所示,ul.childNodes
输出的结果是一个NodeList
。除了三个li
元素之外,还输出了四个文本节点。这是因为我们的HTML是自己编写的,而不是JavaScript生成的,并且元素之间的缩进被作为文本节点计算在DOM中。这不是很直观,主要是因为浏览器开发者工具将元素标签的空白节点去掉了。
如果我们尝试使用firstChild
属性来更改第一个子节点的背景颜色,将会失败的,那是因为第一个节点是文本节点。
childeren
、firstElementChild
和lastElementChild
属性将检索到元素节点。比如ul.children
将会返回三个li
元素。
使用firstElementChild
,我们可以改变ul
中的第一个li
的background-color
,比如:
ul.firstElementChild.style.backgroundColor = 'yellow'
在浏览器控制台上执行上面的代码之后,你可以看到ul
中的第一个li
的背景色已经变成了黄色:
在执行基本的DOM操作时,特定的元素属性非常有用。在JavaScript生成的Web应用程序中,选择所有节点的属性更有可能被使用,因为在这种情况下,空白和缩进将不存在。
children
返回的是一个HTMLCollection
,也是一个类数组。可以使用for...of
循环来遍历所有的children
元素:
for (let element of ul.children) {
element.style.backgroundColor = 'yellow'
}
如此一来,ul
下面的所有li
元素的背景色都变成了黄色。
由于我们的p
元素包含了文本和元素,所以childNodes
属性有助于访问这些信息:
for (let element of p.childNodes) {
console.log(element);
}
childNodes
和children
返回的是一个类数组。可以通过索引号访问节点,或者找到它们的length
属性。
document.body.children[3].lastElementChild.style.background = 'fuchsia';
上面的代码找到了body
中第四个子元素ul
下的最后一个子元素li
:
document.body.children[3]
:选择body
下的第四个子元素ul
document.body.children[3].lastElementChild
:选择ul
下的最后一个子元素li
也就是说执行上面的代码之后,ul
中的最后一个li
的背景颜色变成了fuchsia
:
兄弟节点
节点的兄弟节点是DOM中同一级别上的任何节点。兄弟姐妹不必是同一个类型的节点 —— 文本、元素和注释节点都可以是兄弟节点。
previousSibling
:前一个兄弟节点nextSibling
:下一个兄弟节点previousElementSibling
:前一个兄弟元素节点nextElementSibling
:后一个兄弟元素节点
兄弟属性与子节点的工作方式相同,其中有一组属性遍历所有节点,以及一组仅用于元素节点的属性。previousSibling
和nextSibling
将会获得前一个和下一个节点;previousElementSibling
和nextElementSibling
只获得元素节点。来看一个小示例:
从上图就可以看出他们之间的差异性。如果我们要修改tiger
元素的前面和后面的元素节点背景颜色,我们就可以使用previousElementSibling
和nextElementSibling
。比如:
tiger.nextElementSibling.style.background = 'coral';
tiger.previousElementSibling.style.background = 'aquamarine';
最终效果如下:
咱们可以用张图来描述它们之间的一些关系
如何更改DOM
通过前面的学习,对DOM有了一定的了解,知道怎么访问和遍历DOM。但要更加精通DOM,下一步就要学习如何添加、更改、替换和删除节点。待办事项列表(Todo-List)是JavaScript中一个典型的示例。可以通过将要学习的DOM知识来对Todo-List做创建、修改和删除等操作。
在接下来的内容中,将进一步的讨论JavaScript中的DOM是如何增、删、改等事项,因为查在前面已学习过。学习这些知识之后,我们就很容易的操作Todo-List。
创建节点
在静态网站中,是通过在.html
文件中编写HTML代码,将元素添加到Web页面中。在动态Web应用程序中,元素和文本通常是使用JavaScript来添加的。其中createElement()
和createTextNode()
方法就是用于在DOM中创建新节点。
createElement()
:创建一个新的元素节点createTextNode()
:创建一个新的文本节点node.textContent
:获取或设置元素节点的文本内容node.innerHTML
:获取或设置元素的HTML内容
在开始之前,先修改index.html
文件的代码:
<!DOCTYPE html>
<html lang="en">
<head>
<title>Learning the DOM</title>
</head>
<body>
<h1>Document Object Model</h1>
</body>
</html>
老规矩,在浏览器中打开开发者工具,并且使用document.createElement()
方法创建一个p
元素。同时将创建的p
元素赋值给一个变量paragraph
:
const paragraph = document.createElement('p')
这个时候我们创建了p
元素,使用console.log(paragraph)
时,可以看到控制台中输入一个空的<p></p>
元素,但在HTML结构中并看不到这个新创建的p
元素,如下图所示:
paragraph
变量输出一个空的p
元素,它在没有任何文本的情况下是不太有用的。为了向元素添加文本,可以通过textContent
属性来完成:
paragraph.textContent = '我是一个段落'
执行console.log(paragraph)
命令时,可以看到控制台上输入的p
元素带有了“我是一个段落”的内容:
虽然通过textContent
属性给新创建的p
元素添加了指定的文本内容,但新创建的p
元素和对应的文本内容并没有添加到HTML中。那么怎么添加到HTML中,后面我们会介绍相关的方法。
除了textContent
属性之外,还可以使用innerHTML
属性给元素设置内容,这个属性允许你把THML元素和文本添加到指定的元素中。比如:
paragraph.innerHTML = '我是一个<strong>段落元素</strong>'
console.log(paragraph)
注意,使用
innerHTML
方法给元素添加内容时会引起XSS风险,因为内联JavaScript可以添加到元素中。因此,建议使用textContent
代替innerHTML
。
上面看到的是使用textContent
和innerHTML
给元素添加内容,同样的,也可以使用它们来获取元素的内容。只不过textContent
只获取元素中的文本内容,而innerHTML
将元素中的文本内容和子元素一起将会获得。如下所示:
在JavaScript中还可以使用createTextNode()
方法创建文本节点。
const text = document.createTextNode('我是新创建的文本节点')
console.log(text)
正如前面提到的,虽然这几个方法和属性已经创建了新的元素和文本节点,但是它们并没有插入到文档中,浏览器中并看不到任何的效果。主要是原因是我们只是创建了元素和文本节点,但没有插入到文档中。接下来的要介绍的就是怎么把新创建的元素和文本节点插入到文档中,让你在浏览器中能看到JavaScript动态创建的元素和文本。
将节点插入到DOM中
为了能在页面上看到创建的新文本节点和元素,我们需要将它们插入到document
中。在JavaScript中可以使用appendChild()
和insertBefore()
等方法将新创建的项目添加到父元素的开始、中间或后面。也可以使用replaceChild()
来替换掉一个旧节点。
node.appendChild()
:添加一个节点作为父元素的最后一个子元素node.insertBefore()
:在指定的同级节点前将节点插入父元素node.replaceChild()
:用一个新节点替换现有节点
为了实践这些方法,让我们在index.html
中添加一个无序列表:
<ul>
<li>Buy groceries</li>
<li>Feed the cat</li>
<li>Do laundry</li>
</ul>
刷新浏览器,看到的效果如下:
为了将新创建的元素和文本节点添加到ul
的末尾,我们需要使用上面所了解的方法document.createElement()
和node.textContent
先创建一个新的li
元素,并为新创建的元素添加任何你想要的内容。
// 访问ul元素
const todoList = document.querySelector('ul')
// 创建一个新的li元素
const newTodo = document.createElement('li')
// 给新创建的li元素添加文本节点“Do homework”
newTodo.textContent = 'Do homework'
接下来我们可以使用parentNode.appendChild(newNode)
将新创建的newTodo
(也就是li
)添加到todoList
(也就是ul
)中,并且成为最后一个子元素:
todoList.appendChild(newTodo)
这个时候你可以看到新创建的li
元素已经成功的插入到ul
中了,并且成为其最后一个子元素,而且在document
可以看到,如下图所示:
使用appendChild()
是向指定的父节点添加最后一个子元素,但很多时候我们需要将新创建的元素添加到前面。这个时候我们就可以使用insertBefore()
方法。该方法接受两个参数,第一个是要添加的新子节点,第二个是要跟踪新节点的同级节点(也就新节点要添加到哪个节点的前面)。换句话说,你正在将新节点插入到下一个同级节点之前。比如像下面这样:
parentNode.insertBefore(newNode, nextSibling);
回到我们示例中来,咱们使用document.createElement()
重新创建一个新的li
,并将其赋值给anotherTodo
这个变量,同时给这个新创建的元素添加文本“Pay bills”。那么我们还需要一个参考节点,这里我们可以使用前面学习的知识,比如firstElementChild
,将ul
的第一个li
作为要跟踪的目标节点。为了易于理解,你也可以将其赋值给一个变量,比如targetTodo
:
const anotherTodo = document.createElement('li')
anotherTodo.textContent = 'Pay bills'
const targetTodo = todoList.firstElementChild
todoList.insertBefore(anotherTodo, targetTodo)
你在浏览器看到的效果如下:
现在我们知道了怎么给元素添加子元素,接下来我们要做的是用一个新节点替找现有的节点。同样的,先创建一个新元素li
:
const modifiedTodo = document.createElement('li')
modifiedTodo.textContent = 'Feed the dog'
与insertBefore()
一样,replaceChild()
接受两个参数 —— 新节点和要替换的节点:
parentNode.replaceChild(newNode, oldNode);
回到我们的实例中,替换前面新创建的第一个元素:
todoList.replaceChild(modifiedTodo, todoList.firstElementChild)
在JavaScript中,我们可以通过appendChild()
、insertBefore()
和replaceChild()
的组合,可以在DOM中的任何地方插入节点和元素。
从DOM中删除节点
现在我们知道了如何在DOM中创建元素以及将创建的元素插入到DOM中,并且修改现有的DOM元素。最后一步来学习如何从DOM中删除现有节点。可以使用removeChild()
从父节点中删除一个子节点,并且可以使用remove()
删除节点本身。
node.removeChild()
:删除子节点node.remove()
:删除节点
同样的以前面的ul
为例,使用removeChild()
来删除任何你想要的子节点(li
)。
// 删除最后一个子节点
todoList.removeChild(todoList.lastElementChild)
// 删除第一个子节点
todoList.removeChild(todoList.firstElementChild)
// 删除指定的子节点
todoList.removeChild(todoList.children[1])
另外一种方法是直接在子节点上使用remove()
方法来删除节点本身。
// 删除ul中的第一个li
todoList.firstElementChild.remove()
// 删除ul中的最后一个li
todoList.lastElementChild.remove()
正如上面所演示的一样,使用removeChild()
和remove()
可以从DOM中删除任何节点。除此之外还有一个更诡异的方法可以删除子节点,那就是将父元素的innerHTML
属性值设置为空字符串(' '
)。使用这种方法将会一次性将父元素的所有节点删除,不过这不是首选方法,因为你无法删除指定的节点。
总结
这篇文章主要介绍了JavaScript中DOM的基本知识,通过前面的学习,知道了如何访问DOM中的任何元素,遍历DOM中的任何节点,并修改DOM本身,也可以根据自己需要删除想要删除的DOM节点。这些DOM API可以帮助我们动态的对DOM进行增、删、改、查相关的事情。言外之意,使用上面的API可以对DOM做你自己想做的一些事情。
如需转载,烦请注明出处:https://www.fedev.cn/javascript/understanding-the-dom.htmljordans for sale pink