DOM系列:DOM树和遍历DOM

上一节,咱们整理了DOM系列中的第一篇,主要介绍浏览器与DOM相关的知识。从标题中我们可以看出来,今天所要学的东西包含两个部分,第一部分是DOM树,第二部分是遍历DOM。如果你和我一样对于DOM树和遍历DOM是初次接触,那个人建议您花点时间好好看看这两部分的知识。

DOM树

众所周之,HTML文档的主干就是标记(也就是大家熟悉的HTML标签元素)。

根据文档对象模型(即:DOM),每个HTML标签事实上都是一个对象。嵌套的标签被称为元素(或子标签)。除此之外,标签内的文本也是一个对象。而这些对象都可以使用JavaScript访问。

那么啥是DOM树呢?我们先来看看现实生活中的例子。想象一棵与所有世代有关系的家庭树(大家熟知的族谱),其包括了:祖父母、父母、孩子、兄弟姐妹等等。我们通常以等级的方式组织豪庭树(族谱)。

上图是一个家族族谱的图。其中TossicoAkikazuHitomiTakemi是祖父母。而Toshiakijuliana是父母。另外TKYujiBrunoKaio是父母的孩子(其实也是我的兄弟姐妹们)。

除了家族族谱之外,生活中还有另一个示例,那就是一个组织的结构层次,比如:

而在HTML中,DOM其实也类似一棵树的,它和前面所举例的家族族谱,组织机构图是类似的,HTML中DOM就是一棵树

DOM的一个示例

我们来看一个DOM的示例,比如下面这样的一个HTML文档:

<!DOCTYPE HTML>
<html>
    <head>
        <title>About elks</title>
    </head>
    <body>
        The truth about elks.
    </body>
</html>

DOM将HMTML表示为标记的树结构(也就是大家所说的DOM树),就如下面这样的样子:

在上面的图中,你可以单击元素的节点,它们的子节点可以展开或者收缩,如下图所示:

HTML的标签被称为元素(element)节点(或只是元素)。嵌套标签成为一个子元素(也被称为子)。因此,对于一个HTML文档而言,<html>是一个根节点(也被称为根元素),然后<head><body><html>的子元素。

元素内的文本被称这文本节点,标记为#text。文本节点仅包含一个字符串。它可能没有子元素,也就是说它永远只是树的叶子(没有成为树枝的可能)。

除此之外,要注意文本节点中的两个特殊字符:

  • 换行符:(对应JavaScript中的\n
  • 空白符:

空格和换行符是完全有效的字符,它们形成文本节点并成为DOM的一部分。因此,例如在<head>标签之上的示例中,在<title>这前包含了一些空格,并且该文本成为一个#text节点(它只包含一条换行符和一些空格)。

不过要注意的是,有两个将会除外:

  • <head>标签之前的空格和换行符由于历史原因将被忽略
  • 如果我们将一些东西放在</body>之后,那么它就会自动地移到</body>的前面,正如HTML规范要求的一样,所有内容必须在</body>中一样。因此,在</body>之后可能没有空格

在其他情况之下,一切都很简单。如果文档中有空格(就像任何字符一样),那么它们就会成为DOM中的文本节点,如果我们删除它们,那么就不会有任何东西,也不再会有空格符或换行符的节点。

比如下面这个示例:

<!DOCTYPE HTML>
<html><head><title>About elks</title></head><body>The truth about elks.</body></html>

上面的HTML结构对应的DOM树如下图所示:

相比上面的截图可以看出来,没有了空格符和换行符的文本节点。

通过上面的示例,可能你对DOM树有一定的了解了。但对一些一技术的定义估计还不是非常的了解,接下来花点时间来说一下DOM中的一些技术定义。

DOM中的技术定义

DOM树(tree)是一个DOM节点(nodes)的集合(拿到生活中来说,树是称为节眯的实体集合)。而其中节点由边(edges)连接。每个节点(node)都包含一个值(value)或数据(data),它可能或有可能没有子节点(child node)。

treefirst node称为root节点。如果root节点由另一个节点连接,则root节点是父节点,连接的节点是子节点。

所有的树节点(Tree nodes)都被edges连接在一起。它是树(trees)的重要组成部分,因为它管理节点(nodes)之间的关系。

对于一棵树而言,叶子(leaes)是树(tree)上的最后一个节点(nodes)。它们是没有子节点。就像真正的树一样,DOM也是有根(root)、枝(Element)和叶子(文本节点)。

除此之外,其他还需要理解的重要概念是高度(height)和深度(depth)。树的高度是叶子最长路径的长度;节点的深度是路径到其根的长度。用下图来阐述会更形象一些:

简单的总结一些术语:

  • root(根节点)是树(tree)最顶端的节点
  • edge(边缘)是两个节点(node)之间的连接
  • child(子节点)是具有父节点的节点
  • parent(父节点)是一个节点,它具有子节点的边缘
  • leaf(树叶)是树中没有子节点的节点
  • height(高度)是叶子最长路径的长度
  • depth(深度)是路径到其根的长度

有关于这方面更深入的介绍可以阅读@TK的《Everything you need to know about tree data structures》一文。

另外@TK的文章还涉及到了深度优先遍历广度优秀遍历,有关于这两个概念的深入介绍,可以阅读:

其实有关于深度优先遍历广度优秀遍历在DOM树中的作用并不明显,对于后续的DOM遍历还是有很大的影响。

经过的上面的学习,我们对于DOM树有了一定的了解。除此之外,浏览器对于DOM还具有自动较正的特性。

自动校正

如果浏览器遇到格式错误的HTML,它会自动更正它(校正)。

例如,HTML最顶端的标签总是<html>。即使它不存在文档中 —— 它将存在DOM中,浏览器也会创建它。另外<body>也是一样。

例如,HTML文件只包含一个单词Hello,浏览器将它放置在成<html><body>中,并且也会添加所需的<head>。其DOM将是:

另外,生成DOM时,浏览器会自动处理文档中的错误,比如关闭标签等等。比如下面这样一个无效的文档:

<p>Hello
<li>Mom
<li>and
<li>Dad

事实上,浏览器渲染时,它照样会成为一个正常的 DOM,那是因为浏览器读取标签并会自动修复丢失的部分(比如说关闭标签):

除此之外,还有一个有趣的“特殊情况”,那就是table(表格元素)。根据DOM规范,它必须有<tbody>,但是如果你在HTML文档中忘记写该标签元素时,浏览器会自动在DOM中添加<tbody>标签。比如:

<table id="table"><tr><td>1</td></tr></table>

此时浏览器渲染出来的DOM结构如下:

其他节点类型

我们可以在一个HTML文档中添加更多的标签和在页面中添加注释,比如:

<!DOCTYPE HTML>
<html>
    <body>
        The truth about elks.
        <ol>
            <li>An elk is a smart</li>
            <!-- comment -->
            <li>...and cunning animal!</li>
        </ol>
    </body>
</html>

对于上面的HTML文档,其对应的DOM树如下图所示:

上图中,我们看到了一个新的节点类型 —— 注释节点,标记为 #comment

你可能会想,为什么要将注释添加到DOM中呢?它不会以任何方式影响视觉上的效果,但是有一个规则,如果某个东西在HTML中,那么它也必须在DOM树中。

HTML中的一切,甚至是注释,都将成为DOM的一部分。

即使是<!DOCTYPE ...>指令也是一个DOM节点。它在DOM树中,在<html>之前。我们不会去触摸那个节点,我们甚至不会在图上画它,但它却实实大大的存在那里。

document对象也是一个DOM节点,表示整个文档。在DOM中,其有12种节点类型。在实际操作中,我们通常使用4种方法:

  • document:进入DOM的入口点
  • 元素节点:HTML标签,树构建块
  • 文本节点:包含文本
  • 注释:有时候我们可以把信息放在这里,但它不会显示出来,不过JavaScript却可以从DOM中读取它

或许你和我一样,希望能对每个HTML文档对应的DOM结构能实时的查看,我们希望有对应的工具能帮助我们。事实上是有类似这样的工具,比如 Live DOM Viewer。只要输入文档,它就会立即显示DOM树结构。

DOM中的空白符

DOM 中的空白符会让处理节点结构时增加不少麻烦。在Mozilla 的软件中,原始文件里所有空白符都会在 DOM 中出现(不包括标签内含的空白符)。这样的处理方式有其必要之处,一方面编辑器中可迳行排列文字、二方面 CSS 里的 white-space: pre 也才能发挥作用。 如此一来就表示:

  • 有些空白符会自成一个文本节点。
  • 有些空白符会与其他文本节点合成为一个文本节点。

换句话说,下面这段 HTML 代码对应的 DOM 节点结构会如附图所示,其中\n代表换行符:

<!-- My document -->
<html>
    <head>
        <title>My Document</title>
    </head>
    <body>
        <h1>Header</h1>
        <p>
            Paragraph
        </p>
    </body>
</html>

对应的DOM树,如下图所示:

这么一来,要使用 DOM 游走于节点结构间又不想要无用的空白符时,会有点困难。

以下的 JavaScript 代码定义了许多函数,能够让你在处理 DOM 中的空白符时轻松点:

/**
* 以下所谓的“空白符”代表:
*  "\t" TAB \u0009 (制表符)
*  "\n" LF  \u000A (换行符)
*  "\r" CR  \u000D (回车符)
*  " "  SPC \u0020 (真正的空格符)
*
* 不包括 JavaScript 的“\s”,因为那代表如不断行字符等其他字符。
*/

/**
* 测知某节点的文字内容是否全为空白。
*
* @参数   nod  |CharacterData| 类的节点(如  |Text|、|Comment| 或 |CDATASection|)。
* @传回值      若 |nod| 的文字内容全为空白则传回 true,否则传回 false。
*/
function is_all_ws( nod ) {
    // Use ECMA-262 Edition 3 String and RegExp features
    return !(/[^\t\n\r ]/.test(nod.data));
}


/**
* 测知是否该略过某节点。
*
* @参数   nod  DOM1 |Node| 对象
* @传回值      若 |Text| 节点内仅有空白符或为 |Comment| 节点时,传回 true,
*              否则传回 false。
*/

function is_ignorable( nod ) {
    return ( nod.nodeType == 8) || // 注释节点
        ( (nod.nodeType == 3) && is_all_ws(nod) ); // 仅含空白符的文字节点
}

/**
* 此为会跳过空白符节点及注释节点的 |previousSibling| 函数
* ( |previousSibling| 是 DOM 节点的特性值,为该节点的前一个节点。)
*
* @参数   sib  节点。
* @传回值      有两种可能:
*               1) |sib| 的前一个“非空白、非注释”节点(由 |is_ignorable| 测知。)
*               2) 若该节点前无任何此类节点,则传回 null。
*/
function node_before( sib ) {
    while ((sib = sib.previousSibling)) {
        if (!is_ignorable(sib)) return sib;
    }
    return null;
}

/**
* 此为会跳过空白符节点及注释节点的 |nextSibling| 函数
*
* @参数   sib  节点。
* @传回值      有两种可能:
*               1) |sib| 的下一个“非空白、非注释”节点。
*               2) 若该节点后无任何此类节点,则传回 null。
*/
function node_after( sib ) {
    while ((sib = sib.nextSibling)) {
        if (!is_ignorable(sib)) return sib;
    }
    return null;
}

/**
* 此为会跳过空白符节点及注释节点的 |lastChild| 函数
* ( lastChild| 是 DOM 节点的特性值,为该节点之中最后一个子节点。)
*
* @参数   par  节点。
* @传回值      有两种可能:
*               1) |par| 中最后一个“非空白、非注释”节点。
*               2) 若该节点中无任何此类子节点,则传回 null。
*/
function last_child( par ){
    var res=par.lastChild;
    while (res) {
        if (!is_ignorable(res)) return res;
        res = res.previousSibling;
    }
    return null;
}

/**
* 此为会跳过空白符节点及注释节点的 |firstChild| 函数
*
* @参数   par  节点。
* @传回值      有两种可能:
*               1) |par| 中第一个“非空白、非注释”节点。
*               2) 若该节点中无任何此类子节点,则传回 null。
*/
function first_child( par ){
    var res=par.firstChild;
    while (res) {
        if (!is_ignorable(res)) return res;
        res = res.nextSibling;
    }
    return null;
}

/**
* 此为传回值不包含文字节点资料的首尾所有空白符、
* 并将两个以上的空白符缩减为一个的 |data| 函数。
*( data 是 DOM 文字节点的特性值,为该文字节点中的资料。)
*
* @参数   txt 欲传回其中资料的文字节点
* @传回值     文字节点的内容,其中空白符已依前述方式处理。
*/
function data_of( txt ) {
    var data = txt.data;
    // Use ECMA-262 Edition 3 String and RegExp features
    data = data.replace(/[\t\n\r ]+/g, " ");
    if (data.charAt(0) == " ")
        data = data.substring(1, data.length);
    if (data.charAt(data.length - 1) == " ")
        data = data.substring(0, data.length - 1);
    return data;
}

DOM的遍历

如果你阅读了上面的的内容,或许你已经意识到,DOM看起来就像一个巨大的树 —— 一棵巨大的树,它的元素挂载在树枝上。为了获得更多的技术,DOM中的元素被安排在一个层次结构中,它定义了你最终在浏览器中看到的内容:

这个层次结构用于帮助我们组织HTML元素。它还用于帮助你的CSS样式规则理解什么样式适用于哪些东西。从JavaScript角度来看,这个层次结构确实增加了一点复杂性。你会花相当多的时间去弄清楚你现在所有的DOM和你需要去的地方。当我们考虑创建新的元素或移动元素时,这将变得更加明显。这种复杂性是你需要适应的。

找到你的方式

在你找到元素并与它们做一些事情之前,你首先需要了解元素的位置。我解决这个问题,最简单的方法就是从头开始,然后一路向下。这就是我们要做的。

为了更易于帮助在大家理解,先回到上一节中的示例中

<!DOCTYPE html> 
<html> 
    <head> 
        <meta content="DOM, JavaScript, W3cplus" name="keywords" /> 
        <meta content="DOM系列,浏览器和DOM!" name="description" /> 
        <title>LOL! Sea Otter! Little Kid!</title> 
        <link href="style.css" rel="stylesheet"/> 
    </head> 
    <body> 
        <div id="container"> 
            <img src="w3cplus_logo.png"/> 
            <h1>DOM系列学习!</h1> 
            <p class="bodyText">开始学习DOM,这是一个有关于DOM学习的系列教程...<p> 
            <div class="submitButton">next</div> 
        </div> 
    <script src="main.js"></script> 
    </body> 
</html>

来自DOM顶部的视图由windowdocumenthtml元素组成:

由于这三样东西的重要性,DOM为你提供了通过windowdocumentdocument.documentElement访问它们的方法。

var windowObject = window; 
var documentObject = document;  
var htmlElement = document.documentElement;

需要注意的一点是,windowdocument都是全局属性。不必要明确的声明它们,可以直接从容器里拿出来用就行了。

往往,最顶层的树节点可以直接作为document属性使用,比如:

<html> = document.documentElement

顶部文档节点document.documentElement,其对应的就是<html>的DOM节点。另外一个广泛使用的DOM节点是<body>元素,其对应的是document.body

<body> = document.body

同样的,<head>标签可以用document.head

不过有一点需要注意:

document.body有可能为null。当脚本在访问不存在的元素时,返回的值将会为null

比如,当你的脚本在</head>中运行,比如document.body是将返回的值将是null,因为浏览器还没有读取它。但在</body>中的<script>中返回的则是<body>元素:

<html>
    <head>
        <script>
            console.log('From head:', document.body) // => null
        </script>
    </head>
    <body>
        <script>
            console.log('From body:', document.body) // => HTMLBodyElement
        </script>
    </body>
</html>

上面我们所看到的是htmlheadbody元素的获取。但事实上,一旦你进入HTML元素级别,你的DOM将开始分支并变得更有趣。在这一点上,你有几种获取DOM的方式。通过使用querySelector()querySelectorAll()可以帮助你精确地获取你想要获取的DOM元素。或许你已经在项目中大量使用这两种方法了。但事实上,对于许多实际案例来说,这两种方法太过局限。

有时候,你不知道你想去哪里。querySelector()querySelectorAll()主法在这里无法帮助您。你只想上车然后开车,并想找到你想要去的地方。回到DOM的世界当中时,你会发现自己一直处理这个位置。这就是DOM提供的各种内置属性,所有的Motorcycle Diaries将会帮助你,接下来我们将看看这些属性。

能够帮助你的是知道所有的DOM元素都至少有一个组合,包括父母(Parents)兄弟姐妹(Siblings)子元素(Children)。为了更直观的帮助大家理解,来看下图,下图中包含divscript的一个树形图:

divscript兄弟元素。他们是兄弟元素的原因是他们共有一个相同的父元素bodyscript元素没有子元素,但是div元素有四个子元素,imgh1pdiv。这四个元素也相互被称为兄弟元素,同样的是因为他们有相同的父元素。这其实很好理解,如果你阅读了文章前面的DOM树相关的内容,你会发现它们就像现实的生活中一样,父母、孩子 和兄弟姐妹的关系基于你所关注的树的位置(对应的就是家族族谱)。几乎每个元素,取决于你看它们的角度,可以扮演多个家庭角色。

为了更好的理解,DOM中提供了一些对应的属性(这些属性具有一定的依赖关系)。包括:firstChildlastChildparentNodechildrenpreviousSiblingnextSibling。从他们的名称上来看,就可以推出这些属性的作用。这几个属性结合在一起将构建一个DOM遍历链接图,允许在DOM节点间找到你想要找到的DOM:

为了更好的理解DOM遍历相关的知识点,咱们接下来将围绕这几个属性来展开。

兄弟姐妹和父母打交道

在这些DOM属性中,最容易处理的是父母和兄弟姐妹。对应的属性有parentNodepreviousSiblingnextSibling。下面这张图将帮助你了解这三个属性是如何工作的:

这张图虽然有点零乱,但是你仔细看的话,你可以理清楚它们之间的关系,以及他们之间发生了什么。parentNode属性指向元素的父元素。previousSiblingnextSibling属性允许元素元素它的前一个或下一个兄弟元素。你可以在图中看到箭头的方向指向。最后一行,imgnextSiblingdiv,相应的divpreviousSiblingimg元素。不管是通过imgdivparentNode属性都将把我们带入到第二行中的div(事实上就是imgdiv的元素)。通过上图,大家理解起来是不是很简单。

子元素打交道

上面咱们看到的是如何通过DOM的属性来访问兄弟元素和父元素,事实上,除此之外,DOM还提供一些属性可以访问元素的子元素,比如firstChildlastChildchildren。同样用一图来向大家展示:

firstChildlastChild属性是指父元素的第一个和最后一个子元素。如果父类只有一个子元素,就像例子中的body元素一样,firstChildlastChild都指向相同的元素div。如果一个元素没有子元素,那么firstChildlastChild属性将返回一个null

与其他属性相比,其中children属性相对而方要更为复杂一些。当你在父类上访问children属性时,基本上会得到父元素的子元素集合。这个集合并不是数组,但它确实有一些类似数组的能力,就是大家所说的类数组,其具有length属性,可以通过[]item()来索引集合中具体的元素。比如上图中的div.children[0]访问到的是第一个img元素。

在DOM中获取子节点,除了前面提到的三个属性之外,还有一个childNodes属性,不过它和children有一个很明显的区别:

children只获取子节点(即子元素),而childNodes除了获取的子节点还包括文本节点

除此之外,还有一个特殊的函数hasChildNodes()可以用来判断某个元素是否包含子节点。

这个时候,你把它们放在一起,你就可以对DOM进行遍历。也可以做一些事情。比如,检查某个元素是否有子元素存在,我们就可以这样做:

let bodyElement = document.body

if (bodyElement.firstChild) {
    // 这里做你想做的事情...
}

如果body没有子节点,那么if语奖将返回null。当然,你也可以使用bodyElement.lastChildbodyElement.children做为if语句的条件。

再来看另一个简单示例,前面提到过了,通过children可以获取某个元素的所有子节点(前提是这个元素有子元素存在)。这个时候得到的是一个类数组,如查你要获取到该元素中的每个子节点,就需要使用for循环来处理:

var bodyElement = document.body;

for (var i = 0; i < bodyElement.children.length; i++) {
    var childElement = bodyElement.children[i];

    document.writeln(childElement.tagName);
}

通过元素遍历DOM

上面咱们看到的是通过DOM节点来遍历DOM。比如childNodes属性,除了可以获取元素节点之外,还可以获取文本节点,甚至是注释节点。但很多时候,对于DOM的操作,咱们只需要获取想要的DOM元素节点,而不需要考虑文本和注释节点。这个时候咱们只需要操作元素节点即可,这对应DOM中操作元素节点的一些属性。同样的使用下图来向大家阐述,易于理解:

和前面相比,这里的属性多了Element这个词,其对应的含义:

  • children:元素节点的子元素
  • firstElementChildlastElementChild:元素的第一个或最后一个子元素
  • previousElementSiblingnextElementSibling:元素的前一个或后一个相邻元素
  • parentElement:元素的父元素

总结

这篇文章是DOM系列的第二篇文章,主要介绍了DOM树和DOM的遍历。前一部分只要介绍了DOM树,简单的理解,任何一个HTML文档都可以类似于家族的族普来绘制对应的DOM树。通过DOM树可以理清楚每个DOM元素(或者说DOM节点)之间的关系。比如,父子关系、兄弟关系等。

另外,在DOM中找到对应的元素是每位JavaScript开发人员都应该需要掌握的技巧之一。这篇文章的后一部分主要向大家介绍了如何对DOM进行遍历,其实就是通过DOM的属性怎么获取DOM的元素或节点。简单的归纳一下,分为:

  • 向上获取,比如parentNodeparentElementclosest
  • 向下获取,比如querySelector()querySelectorAll()childrenfirstChildrenlastChildrenchildNodes
  • 兄弟元素(节点),比如nextElementSiblingpreviousElementSiblingnextSiblingpreviousSibling

如果文中有不对之处,或者你有更好的经验,欢迎在下面的评论中与我们一起分享。最后要说明的是,文章中有些图片来自互联网,如涉及侵权,烦请告之。

扩展阅读

大漠

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

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

返回顶部