理解DOM

编辑推荐:3月31日前,点击注册激活 Coding.net 立赠30天付费会员 ,体验极速代码托管服务!

特别声明,本文整个思路来源于@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页面的实时更新,bodybackground-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属性。所以在链接中添加了个idnav的值。

这个时候,使用getElementById()方法来访问所需要的元素。比如在控制台中输入下面的内容:

document.getElementById('nav')

通过上面的代码,就获取到了index.htmlidnav的元素:

正如上面所演示的一样,使用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元素节点是父节点。headbody是兄弟节点,也是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相关。对于注释和文本执行相同的操作,它们将分别输出的值是83

当你知道如何访问元素时,你就可以看到对应的节点类型。比如:

document.body.nodeType; // => 1

除了nodeType之外,你还可以使用nodeValue属性获取文本或注释节点的值,也可以使用nodeName属性来获取元素的标签名。

用事件修改DOM

到目前为止,我们只看到了如何在浏览器开发者工具的控制台中修改DOM,这样一来只是暂时的修改,因为只要刷新了页面,每次更改都会丢失。比如前面的示例,在控制台中修改bodybackground-color。那么接下来,我们可以结合我们所学到的内容,在页面中创建一个交互按钮,当点击按钮后再修改bodybackground-color

同样在index.html中,咱们添加一个idchangeBackground<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上的事件,当用户单击它时执行操作,也就是改变bodybackground-color。我们可以在按钮中添加一个click事件来实现这个效果。

首先通过getElementById()方法找到这个button元素,并将其赋值给一个变量:

let button = document.getElementById('changeBackground');

使用addEventListener()方法来监听button上的click事件,并执行一次单击后的函数。

button.addEventListener('click', () => {
    // 单击按钮对应要做的事情
})

最后在函数内部,将使用前面的代码来修改bodybackground-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元素的方法:通过IDclass、标签和查询选择器。

下面是我们将会介绍的五种访问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名为demodiv元素:

<div id="demo">Access me by ID</div>

在控制台中,通过document.getElementById('demo')来获取iddemo的元素,并且将其赋值给demoId变量:

const demoId = document.getElementById('demo')

在控制台通过console.log(demoId)就可以输出我们选中的元素:

为了验证我们选择的元素是对的,可以通过改变元素的borderred来确定:

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()方法来访问classdemo的元素,并将其赋值给一个变量demoClass

const demoClass = document.getElementsByClassName('demo')

此时,你可能认为可以像使用ID那样修改元素。如果我们也像前面介绍ID访问DOM元素的示例一样,给使用classdiv添加一个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类名的元素都添加了border1px 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>元素添加一个blueborder,也要使用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.htmliddemo-query的元素,我们就可以这样操作:

const demoQuery = document.querySelector('#demo-query')

对于多个元素的选择器(比如class或标签),使用querySelector()将返回与查询匹配的第一个元素。如查要选择所有元素,则可以使用querySelectorAll()方法。比如,我们可以使用querySelectorAll()方法访问index.html中所有classdemo-query-all的元素:

const demoQueryAll = document.querySelectorAll('.demo-query-all')

querySelectorAll()方法有点类似于getElementsByClassName()方法,只不过其返回的是一个NodeList的类数组。同样的,如果要给所有的classdemo-query-all元素添加green边框效果,不能直接使用,需要使用类似于forEach()方法对类数组进行遍历:

demoQueryAll.forEach(query => {
    query.style.border = '1px solid green'
})

使用querySelector()方法,在方法中的参数可以使用逗号,做为分隔符,比如querySelector('div, article')将匹配到文档中的第一个div元素以及第一个article元素。使用querySelectorAll()方法也可以如何操作,比如querySelectorAll('div, article')方法将选中文档中所有divarticle元素。

使用查询选择器方法非常强大,因为你可以像在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对象可以访问工具栏、窗口的宽度和高度、提示和警报等信息。documentwindow内的内容组成。

下面是由每个文档所包含的根元素组成的图表。即使将一个空白HTML文件加载到浏览器中,这三个节点也将被添加到DOM中。

属性 节点 节点类型
document #document DOCUMENT_NODE
document.documentElement html ELEMENT_NODE
document.head head ELEMENT_NODE
document.body body ELEMENT_NODE

由于htmlheadbody三个元素是任何一个HTML文档必有的元素,可谓是非常常见的元素,所以它们在document上都有自己的属性。

正如上图所示,你可以在浏览器的开发者工具中打印出上面表格中的四个属性。当然你也还可以测试h1pul元素。因为在script标记中已将它们指定在对应的变量中,所以将返回元素。

父节点

DOM中的节点被称为父节点、子节点和兄弟节点,这取决于它们与其他节点的关系。任何节点的父节点都位于它之上的一个级别的节点,或者更接近于DOM中的document。DOM中有两个属性可以访问到父节点:parentNodeparentElement

index.html中:

  • htmlheadbodyscript的父节点
  • bodyh1h2pul父节点,但不是li的父节点,那是因为libody第二级节点(按照族谱关系来描述,bodyli的爷爷节点,在这里常常被称为祖先节点)

我们可以通过pparentNode属性来获取到p的父节点。而其中p是一个变量,对应的是通过document.getElementsByTagName('p')[0]获取的第一个p元素。

p的父节点是body,但是我们怎么能得到祖父节点,这是两个层次呢?我们可以通过链接属性来实现:

p.parentNode.parentNode

parentElementparentNode类似,都表示一个元素的父元素,但两者还是有一定的差异性:

  • 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 )没有作为元素的父元素,因此parentElementnull 。 (还有其他更不可能的情况, parentElement可能为null ,但您可能永远不会遇到它们。)

只要记住,名称中带有element的属性总是返回Elementnull 。 没有的属性可以返回任何其他类型的节点。通常,遍历DOM时,parentNode更常用。

子节点

子节点刚好与父节点相反,节点的子节点是在它下面的一个级别的节点。超过一层嵌套的任何节点通常称为后代节点

DOM中子节点对应的属性有:

  • childNodes:子节点
  • firstChild:第一个子节点
  • lastChild:最后一个子节点
  • children:元素的子节点
  • firstElementChild:第一个子元素节点
  • lastElementChild:最后一个子元素节点

childNodes属性返回包含指定节点的子节点的集合,该集合为即时更新的集合(是一个动态集合)。比如,你期望获取到ul元素的子节点li元素,就可以像下面这样操作:

ul.childNodes

输出的结果如下:

正如上图所示,ul.childNodes输出的结果是一个NodeList。除了三个li元素之外,还输出了四个文本节点。这是因为我们的HTML是自己编写的,而不是JavaScript生成的,并且元素之间的缩进被作为文本节点计算在DOM中。这不是很直观,主要是因为浏览器开发者工具将元素标签的空白节点去掉了。

如果我们尝试使用firstChild属性来更改第一个子节点的背景颜色,将会失败的,那是因为第一个节点是文本节点。

childerenfirstElementChildlastElementChild属性将检索到元素节点。比如ul.children将会返回三个li元素。

使用firstElementChild,我们可以改变ul中的第一个libackground-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);
}

childNodeschildren返回的是一个类数组。可以通过索引号访问节点,或者找到它们的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:后一个兄弟元素节点

兄弟属性与子节点的工作方式相同,其中有一组属性遍历所有节点,以及一组仅用于元素节点的属性。previousSiblingnextSibling将会获得前一个和下一个节点;previousElementSiblingnextElementSibling只获得元素节点。来看一个小示例:

从上图就可以看出他们之间的差异性。如果我们要修改tiger元素的前面和后面的元素节点背景颜色,我们就可以使用previousElementSiblingnextElementSibling。比如:

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

上面看到的是使用textContentinnerHTML给元素添加内容,同样的,也可以使用它们来获取元素的内容。只不过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做你自己想做的一些事情。

大漠

常用昵称“大漠”,W3CPlus创始人,目前就职于手淘。对HTML5、CSS3和Sass等前端脚本语言有非常深入的认识和丰富的实践经验,尤其专注对CSS3的研究,是国内最早研究和使用CSS3技术的一批人。CSS3、Sass和Drupal中国布道者。2014年出版《图解CSS3:核心技术与案例实战》。

如需转载,烦请注明出处:https://www.w3cplus.com/javascript/understanding-the-dom.html

返回顶部