!转载链接:https://www.w3cplus.com/javascript/dom-tree-and-traversals.html
!!仅供学习,若有侵权,立刻删除
DOM 树
众所周之,HTML 文档的主干就是标记(也就是大家熟悉的 HTML 标签元素)。
根据文档对象模型(即:DOM),每个 HTML 标签事实上都是一个对象。嵌套的标签被称为之元素(或子标签)。除此之外,标签内的文本也是一个对象。而这些对象都可以使用 JavaScript 访问。
那么啥是 DOM 树呢?我们先来看看现实生活中的例子。想象一棵与所有世代有关系的家庭树(大家熟知的族谱),其包括了:祖父母、父母、孩子、兄弟姐妹等等。我们通常以等级的方式组织豪庭树(族谱)。
image上图是一个家族族谱的图。其中Tossico
、Akikazu
、Hitomi
和Takemi
是祖父母。而Toshiaki
和juliana
是父母。另外TK
、Yuji
、Bruno
和Kaio
是父母的孩子(其实也是我的兄弟姐妹们)。
除了家族族谱之外,生活中还有另一个示例,那就是一个组织的结构层次,比如:
image而在 HTML 中,DOM 其实也类似一棵树的,它和前面所举例的家族族谱,组织机构图是类似的,HTML 中 DOM 就是一棵树。
imageDOM 的一个示例
我们来看一个 DOM 的示例,比如下面这样的一个 HTML 文档:
<pre><!DOCTYPE HTML>
<html>
<head>
<title>About elks</title>
</head>
<body>
The truth about elks.
</body>
</html>
</pre>
DOM 将 HMTML 表示为标记的树结构(也就是大家所说的 DOM 树),就如下面这样的样子:
image在上面的图中,你可以单击元素的节点,它们的子节点可以展开或者收缩,如下图所示:
imageHTML 的标签被称为元素(element
)节点(或只是元素)。嵌套标签成为一个子元素(也被称为子)。因此,对于一个 HTML 文档而言,<html>
是一个根节点(也被称为根元素), 然后<head>
和<body>
是<html>
的子元素。
元素内的文本被称这文本节点,标记为#text
。文本节点仅包含一个字符串。它可能没有子元素,也就是说它永远只是树的叶子(没有成为树枝的可能)。
除此之外,要注意文本节点中的两个特殊字符:
- 换行符:
↵
(对应 JavaScript 中的\n
) - 空白符:
␣
空格和换行符是完全有效的字符,它们形成文本节点并成为 DOM 的一部分。因此,例如在<head>
标签之上的示例中,在<title>
这前包含了一些空格,并且该文本成为一个#text
节点(它只包含一条换行符和一些空格)。
不过要注意的是,有两个将会除外:
- 在
<head>
标签之前的空格和换行符由于历史原因将被忽略 - 如果我们将一些东西放在
</body>
之后,那么它就会自动地移到</body>
的前面,正如 HTML 规范要求的一样,所有内容必须在</body>
中一样。因此,在</body>
之后可能没有空格
在其他情况之下,一切都很简单。如果文档中有空格(就像任何字符一样),那么它们就会成为 DOM 中的文本节点,如果我们删除它们,那么就不会有任何东西,也不再会有空格符或换行符的节点。
比如下面这个示例:
<pre><!DOCTYPE HTML>
<html><head><title>About elks</title></head><body>The truth about elks.</body></html>
</pre>
上面的 HTML 结构对应的 DOM 树如下图所示:
image相比上面的截图可以看出来,没有了空格符和换行符的文本节点。
通过上面的示例,可能你对 DOM 树有一定的了解了。但对一些一技术的定义估计还不是非常的了解,接下来花点时间来说一下 DOM 中的一些技术定义。
DOM 中的技术定义
DOM 树 (tree
) 是一个 DOM 节点 (nodes
) 的集合(拿到生活中来说,树是称为节眯的实体集合)。而其中节点由边(edges
)连接。每个节点(node
)都包含一个值(value
)或数据(data
),它可能或有可能没有子节点(child node
)。
tree
的first 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 的文章还涉及到了深度优先遍历和广度优秀遍历,有关于这两个概念的深入介绍,可以阅读:
querySelectorAll
和getElementsByTagName
- Demystifying Depth-First Search
- Breaking Down Breadth-First Search
其实有关于深度优先遍历和广度优秀遍历在 DOM 树中的作用并不明显,对于后续的 DOM 遍历还是有很大的影响。
经过的上面的学习,我们对于 DOM 树有了一定的了解。除此之外,浏览器对于 DOM 还具有自动较正的特性。
自动校正
如果浏览器遇到格式错误的 HTML,它会自动更正它(校正)。
例如,HTML 最顶端的标签总是<html>
。即使它不存在文档中 —— 它将存在 DOM 中,浏览器也会创建它。另外<body>
也是一样。
例如,HTML 文件只包含一个单词Hello
,浏览器将它放置在成<html>
和<body>
中,并且也会添加所需的<head>
。其 DOM 将是:
另外,生成 DOM 时,浏览器会自动处理文档中的错误,比如关闭标签等等。比如下面这样一个无效的文档:
<pre><p>Hello
<li>Mom
<li>and
<li>Dad
</pre>
事实上,浏览器渲染时,它照样会成为一个正常的 DOM,那是因为浏览器读取标签并会自动修复丢失的部分(比如说关闭标签):
image除此之外,还有一个有趣的 “特殊情况”,那就是table
(表格元素)。根据 DOM 规范,它必须有<tbody>
,但是如果你在 HTML 文档中忘记写该标签元素时,浏览器会自动在 DOM 中添加<tbody>
标签。比如:
<pre><table><tr><td>1</td></tr></table>
</pre>
此时浏览器渲染出来的 DOM 结构如下:
image其他节点类型
我们可以在一个 HTML 文档中添加更多的标签和在页面中添加注释,比如:
<pre><!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>
</pre>
对于上面的 HTML 文档,其对应的 DOM 树如下图所示:
image上图中,我们看到了一个新的节点类型 —— 注释节点,标记为 #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
代表换行符:
<pre><!-- My document -->
<html>
<head>
<title>My Document</title>
</head>
<body>
<h1>Header</h1>
<p>
Paragraph
</p>
</body>
</html>
</pre>
对应的 DOM 树,如下图所示:
image这么一来,要使用 DOM 游走于节点结构间又不想要无用的空白符时,会有点困难。
以下的 JavaScript 代码定义了许多函数,能够让你在处理 DOM 中的空白符时轻松点:
<pre>/**
* 以下所谓的“空白符”代表:
* "\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;
}
</pre>
DOM 的遍历
如果你阅读了上面的的内容,或许你已经意识到,DOM 看起来就像一个巨大的树 —— 一棵巨大的树,它的元素挂载在树枝上。为了获得更多的技术,DOM 中的元素被安排在一个层次结构中,它定义了你最终在浏览器中看到的内容:
image这个层次结构用于帮助我们组织 HTML 元素。它还用于帮助你的 CSS 样式规则理解什么样式适用于哪些东西。从 JavaScript 角度来看,这个层次结构确实增加了一点复杂性。你会花相当多的时间去弄清楚你现在所有的 DOM 和你需要去的地方。当我们考虑创建新的元素或移动元素时,这将变得更加明显。这种复杂性是你需要适应的。
找到你的方式
在你找到元素并与它们做一些事情之前,你首先需要了解元素的位置。我解决这个问题,最简单的方法就是从头开始,然后一路向下。这就是我们要做的。
为了更易于帮助在大家理解,先回到上一节中的示例中:
<pre><!DOCTYPE html>
<html>
<head>
<meta content="DOM, JavaScript, W3cplus" />
<meta content="DOM系列,浏览器和DOM!" />
<title>LOL! Sea Otter! Little Kid!</title>
<link href="style.css" rel="stylesheet"/>
</head>
<body>
<div>
<img src="w3cplus_logo.png"/>
<h1>DOM系列学习!</h1>
<p>开始学习DOM,这是一个有关于DOM学习的系列教程...<p>
<div>next</div>
</div>
<script src="main.js"></script>
</body>
</html>
</pre>
来自 DOM 顶部的视图由window
、document
和html
元素组成:
由于这三样东西的重要性,DOM 为你提供了通过window
、document
和document.documentElement
访问它们的方法。
<pre>var windowObject = window;
var documentObject = document;
var htmlElement = document.documentElement;
</pre>
需要注意的一点是,window
和 document
都是全局属性。不必要明确的声明它们,可以直接从容器里拿出来用就行了。
往往,最顶层的树节点可以直接作为document
属性使用,比如:
<pre><html> = document.documentElement
</pre>
顶部文档节点document.documentElement
,其对应的就是<html>
的 DOM 节点。另外一个广泛使用的 DOM 节点是<body>
元素,其对应的是document.body
:
<pre><body> = document.body
</pre>
同样的,<head>
标签可以用document.head
。
不过有一点需要注意:
document.body
有可能为null
。当脚本在访问不存在的元素时,返回的值将会为null
。
比如,当你的脚本在</head>
中运行,比如document.body
是将返回的值将是null
,因为浏览器还没有读取它。但在</body>
中的<script>
中返回的则是<body>
元素:
<pre><html>
<head>
<script>
console.log('From head:', document.body) // => null
</script>
</head>
<body>
<script>
console.log('From body:', document.body) // => HTMLBodyElement
</script>
</body>
</html>
</pre>
上面我们所看到的是html
、head
和body
元素的获取。但事实上,一旦你进入 HTML 元素级别,你的 DOM 将开始分支并变得更有趣。在这一点上,你有几种获取 DOM 的方式。通过使用querySelector()
和querySelectorAll()
可以帮助你精确地获取你想要获取的 DOM 元素。或许你已经在项目中大量使用这两种方法了。但事实上,对于许多实际案例来说,这两种方法太过局限。
有时候,你不知道你想去哪里。querySelector()
和querySelectorAll()
主法在这里无法帮助您。你只想上车然后开车,并想找到你想要去的地方。回到 DOM 的世界当中时,你会发现自己一直处理这个位置。这就是 DOM 提供的各种内置属性,所有的 Motorcycle Diaries 将会帮助你,接下来我们将看看这些属性。
能够帮助你的是知道所有的 DOM 元素都至少有一个组合,包括父母(Parents)、兄弟姐妹(Siblings)和子元素(Children)。为了更直观的帮助大家理解,来看下图,下图中包含div
的script
的一个树形图:
div
和script
是兄弟元素。他们是兄弟元素的原因是他们共有一个相同的父元素body
。script
元素没有子元素,但是div
元素有四个子元素,img
、h1
、p
和div
。这四个元素也相互被称为兄弟元素,同样的是因为他们有相同的父元素。这其实很好理解,如果你阅读了文章前面的 DOM 树相关的内容,你会发现它们就像现实的生活中一样,父母、孩子 和兄弟姐妹的关系基于你所关注的树的位置(对应的就是家族族谱)。几乎每个元素,取决于你看它们的角度,可以扮演多个家庭角色。
为了更好的理解,DOM 中提供了一些对应的属性(这些属性具有一定的依赖关系)。包括:firstChild
、lastChild
、parentNode
、children
、previousSibling
和nextSibling
。从他们的名称上来看,就可以推出这些属性的作用。这几个属性结合在一起将构建一个 DOM 遍历链接图,允许在 DOM 节点间找到你想要找到的 DOM:
为了更好的理解 DOM 遍历相关的知识点,咱们接下来将围绕这几个属性来展开。
兄弟姐妹和父母打交道
在这些 DOM 属性中,最容易处理的是父母和兄弟姐妹。对应的属性有parentNode
、previousSibling
和nextSibling
。下面这张图将帮助你了解这三个属性是如何工作的:
这张图虽然有点零乱,但是你仔细看的话,你可以理清楚它们之间的关系,以及他们之间发生了什么。parentNode
属性指向元素的父元素。previousSibling
和nextSibling
属性允许元素元素它的前一个或下一个兄弟元素。你可以在图中看到箭头的方向指向。最后一行,img
的nextSibling
是div
,相应的div
的previousSibling
是img
元素。不管是通过img
或div
的parentNode
属性都将把我们带入到第二行中的div
(事实上就是img
和div
的元素)。通过上图,大家理解起来是不是很简单。
子元素打交道
上面咱们看到的是如何通过 DOM 的属性来访问兄弟元素和父元素,事实上,除此之外,DOM 还提供一些属性可以访问元素的子元素,比如firstChild
、lastChild
和children
。同样用一图来向大家展示:
firstChild
和lastChild
属性是指父元素的第一个和最后一个子元素。如果父类只有一个子元素,就像例子中的body
元素一样,firstChild
和lastChild
都指向相同的元素div
。如果一个元素没有子元素,那么firstChild
和lastChild
属性将返回一个null
。
与其他属性相比,其中children
属性相对而方要更为复杂一些。当你在父类上访问children
属性时,基本上会得到父元素的子元素集合。这个集合并不是数组,但它确实有一些类似数组的能力,就是大家所说的类数组,其具有length
属性,可以通过[]
或item()
来索引集合中具体的元素。比如上图中的div.children[0]
访问到的是第一个img
元素。
在 DOM 中获取子节点,除了前面提到的三个属性之外,还有一个childNodes
属性,不过它和children
有一个很明显的区别:
children
只获取子节点(即子元素),而childNodes
除了获取的子节点还包括文本节点。
除此之外,还有一个特殊的函数hasChildNodes()
可以用来判断某个元素是否包含子节点。
这个时候,你把它们放在一起,你就可以对 DOM 进行遍历。也可以做一些事情。比如,检查某个元素是否有子元素存在,我们就可以这样做:
<pre>let bodyElement = document.body
if (bodyElement.firstChild) {
// 这里做你想做的事情...
}
</pre>
如果body
没有子节点,那么if
语奖将返回null
。当然,你也可以使用bodyElement.lastChild
或bodyElement.children
做为if
语句的条件。
再来看另一个简单示例,前面提到过了,通过children
可以获取某个元素的所有子节点(前提是这个元素有子元素存在)。这个时候得到的是一个类数组,如查你要获取到该元素中的每个子节点,就需要使用for
循环来处理:
<pre>var bodyElement = document.body;
for (var i = 0; i < bodyElement.children.length; i++) {
var childElement = bodyElement.children[i];
document.writeln(childElement.tagName);
}
</pre>
通过元素遍历 DOM
上面咱们看到的是通过 DOM 节点来遍历 DOM。比如childNodes
属性,除了可以获取元素节点之外,还可以获取文本节点,甚至是注释节点。但很多时候,对于 DOM 的操作,咱们只需要获取想要的 DOM 元素节点,而不需要考虑文本和注释节点。这个时候咱们只需要操作元素节点即可,这对应 DOM 中操作元素节点的一些属性。同样的使用下图来向大家阐述,易于理解:
和前面相比,这里的属性多了Element
这个词,其对应的含义:
-
children
: 元素节点的子元素 -
firstElementChild
、lastElementChild
:元素的第一个或最后一个子元素 -
previousElementSibling
和nextElementSibling
:元素的前一个或后一个相邻元素 -
parentElement
:元素的父元素
总结
这篇文章是 DOM 系列的第二篇文章,主要介绍了 DOM 树和 DOM 的遍历。前一部分只要介绍了 DOM 树,简单的理解,任何一个 HTML 文档都可以类似于家族的族普来绘制对应的 DOM 树。通过 DOM 树可以理清楚每个 DOM 元素(或者说 DOM 节点)之间的关系。比如,父子关系、兄弟关系等。
另外,在 DOM 中找到对应的元素是每位 JavaScript 开发人员都应该需要掌握的技巧之一。这篇文章的后一部分主要向大家介绍了如何对 DOM 进行遍历,其实就是通过 DOM 的属性怎么获取 DOM 的元素或节点。简单的归纳一下,分为:
- 向上获取,比如
parentNode
、parentElement
和closest
; - 向下获取,比如
querySelector()
、querySelectorAll()
、children
、firstChildren
、lastChildren
和childNodes
- 兄弟元素(节点),比如
nextElementSibling
、previousElementSibling
、nextSibling
和previousSibling
网友评论