长文预警!!
一. 什么是文档对象模型
文档对象模型(DOM)是一个对 HTML 布局结构的映射模型,换句话说就是一个描述 HTML 的模型。它使得我们可以通过 JavaScript 代码访问文档,完成修改页面内容,插入数据或修改某个实例的任务。
DOM 是通过树来描述HTML 文档的,相信学过数据结构的同学对树一定不陌生,就是一种组织数据的方式而已。
二. 节点
如果是一个树的话,就肯定有节点(node),每个节点可以有父节点、子节点、兄弟节点。
下图是通过从上到下遍历一个简单的 HTML 文档创建出的表示节点间上下级关系的结构图。
从上图中也可以看出来,DOM 中的节点分为几种不同的类型。表示 HTML 元素的叫做元素节点,表示文本的叫做文本节点,表示属性的叫做属性节点。所有的节点都是上图顶部的那个文档节点的下级节点(这个文档节点很容易被忽视)。了解这几种节点之间的区别是很重要的,因为我们在使用 JS 遍历 DOM 的时候需要以不同的方式处理他们。
1. 元素节点
元素节点其实就是文档标签所对应的节点,这些节点定义了文档的结构,并且我们选哟与之进行交互并对其进行修改的大部分内容在他们之中,它们是遍历文档的基础所在。
2. 文本节点
简单点来说,文本节点存在于标签之间,此外文本节点不能有子节点,
访问文本节点与访问元素节点所使用的 JS 方法是一样的。
3. 属性节点
与文档节点一样,我们在构建 DOM 的时候很容易忽视属性节点,因为它看上去像是元素节点的一部分,实际上却是与之完全不同并且极为重要的一类节点。处理他们的方式也与元素节点和文本节点不同。
与文本节点一样,属性节点也不能拥有自己的子节点,而且对于他们的外围元素来说,属性节点也不是其子节点。这听起来有点奇怪,因为这些节点依附于元素节点而存在,但是他们却不是其子节点,原因是我们需要采用另外一套方式访问其内容,我们可以使用普通的 JS 方法接近这类节点,但是要想访问其中的内容,则需要特有的方法。
我们经常需要增加或删除元素节点的 class 属性,所以会频繁的用到属性节点。
三. 处理元素节点
了解了这些节点之后,我们接下开看看如何访问这些节点。我们可以肯定的说,凡是 DOM 模型里面的东西,我们就有办法将他抓取出来,并使用 JS 对其进行储存修改。
我们接下来将会看到有很多种方式访问节点,在实际中我们应该选择效率最高的一种。
1. 根据 ID 定位元素
HTML 标准要求元素的 id 属性值在文档内必须是唯一的,因此元素的 id 是最理想的定位方式,我们先来看看具体的例子:
html
<!DOCTYPE html>
<html>
<head>
<title>id</title>
</head>
<body>
<div id="header">
<h1><a href="/" id='homelink'>title of your site</a></h1>
</div>
</body>
</html>
js
a = document.getElementById('header');
b = document.getElementById('homelink');
console.info(a)
console.info(b)
这样 console 就显示出了我们访问到的内容:
正如我们之前所说的,获取任何节点都要通过 document 对象才行。当我们定义到节点之后我们就可以使用大量的 JS 内置方法对其进行 储存、修改、获取信息。我们之后再说这些方法。
2. 根据标签名定位元素
由于 id 在文档中是唯一的,所以 getElementById()
只能返回一个元素,有的时候我们需要的元素不止一个,甚至可能是一组。那么我们就可以使用 getElementsByTagName()
方法。
有一个常见的错误就是忘记写 Elements 后面的 s 。
这种方式既可以找到一组元素也可以获取一个元素。
下面是一个使用这种方法获取标签的例子:
html
<!DOCTYPE html>
<html>
<head>
<title>TagName</title>
<meta charset="utf-8">
</head>
<body>
<h1>Using getElementByTagName</h1>
<p>Content Paragraph</p>
<p>Content Paragraph</p>
<p>Content Paragraph</p>
<script type="text/javascript" src="./located_by_id.js"></script>
</body>
</html>
js
a = document.getElementsByTagName('p');
console.info(a)
输出为:
从上面的输出中可以看出来,document.getElementsByTagName('p')
返回了一个 NodeList 的 DOM 对象,NodeList 对象是一个节点的集合,各节点的顺序与其在 HTML 文档中出现的顺序一致。这个对象有一个 length
属性,所以我们可以通过document.getElementsByTagName('p').length
的方法获取这个列表的长度。在 python 中我们是通过对象的方法来获取其长度,而 js 是通过它的一个属性。
当我们定位好一组元素,检查好其长度之后我们就可能需要将每个节点抽取出来单个处理 :
- 使用 item 方法
- 使用数组下标
js 中数组的下标起始也是 0。
js
// 在访问前检查其长度是一个好习惯
if (document.getElementsByTagName('p').length > 0){
// 使用 item 方法访问第一个元素
a = document.getElementsByTagName('p').item(0);
console.info(a);
// 使用数组下标访问第一个元素
b = document.getElementsByTagName('p')[0];
console.info(b);
}
输出为:
3. 根据 class 属性定位元素
getElementsByClassName()
可以通过 class 属性来定位元素,它也同样返回一个 NodeList 对象供开发者访问,
我们还可以结合其他的定位元素的方法结合起来使用,可以实现一些有趣的元素选择功能。
我们直接看看它的例子:
html
<!DOCTYPE html>
<html>
<head>
<title>ClassName</title>
<meta charset="utf-8">
</head>
<body>
<h1>Using getElementByTagName</h1>
<p class="dropcap huge">Content Paragraph</p>
<p id="e" class="dropcap">Content <span class="huge">Paragraph</span> </p>
<p class="dropcap">Content Paragraph</p>
<script type="text/javascript" src="./located_by_id.js"></script>
</body>
</html>
js
// 返回所有 class 为 dropcap 的元素节点
document.getElementsByClassName('dropcap');
// 返回所有 class 同时带有 dropcap 和 huge 的元素节点
document.getElementsByClassName('dropcap huge');
// 返回所有在 id 为 e 的元素节点下的 class 为 huge 的元素
document.getElementById('e').getElementsByClassName('huge')
4. 在 JavaScript 中使用 CSS 选择器来定位节点
你大概也已经发现了,我们定位元素的过程实际上与 CSS 选择器很相似,所以人们就希望可以通过便于记忆的 CSS 选择器来定位节点。所以 querySelector()
与 querySeletorAll()
就产生了。
这两个方法都接受 CSS格式的选择器作为其参数,二者的区别是 querySeletorAll()
返回的是 NodeList 对象,而 querySelector()
只返回匹配的第一个元素。
我们还是看一段代码:
html
<!DOCTYPE html>
<html>
<head>
<title>ClassName</title>
<meta charset="utf-8">
</head>
<body>
<h1>Using getElementByTagName</h1>
<p class="dropcap huge">Content Paragraph</p>
<p id="e" class="dropcap">Content <span class="huge">Paragraph</span> </p>
<p class="dropcap">Content Paragraph</p>
<script type="text/javascript" src="./located_by_id.js"></script>
</body>
</html>
js
// 返回 id 值为 header 的元素
document.querySelector('#header')
// 返回 class 是 dropcap 的第一个元素
document.querySelector('.dropcap')
// 返回所有class 是 dropcap 的元素
document.querySelectorAll('.dropcap')
// 返回所有 属性是 dropcap 和 huge 的元素
document.querySelectorAll('.dropcap', '.huge')
// 返回所有带有 class 的 p 标签
document.querySelectorAll('p[class]')
四. 处理属性节点
上一节说了好多处理元素节点的方法,我们先回顾一下:通过 id 、通过 class 、通过标签名、通过 CSS 选择器。
接下来我们就可以操纵他们之中的属性节点啦。
前面说过,属性节点虽然位于元素节点的内部,但是并不被当作其子节点。所以如果我们想要获取、修改或移除其中的信息,那么我们就需要一些特殊的方法,他们是:
- getAttribute()
- setAttribute()
- removeAttribute()
- hasAttribute()
以上的各个方法都是通过属性名称来定位属性值的。比如说 class = "visible” 为例,根据属性名 class 就可以定位到值为 "visible” 的属性。
许多最为常见的交互效果,比如隐藏/显示,使用 CSS 来做效率会更高,我们只需要简单的添加或者删除某个 class 就好啦。
1. 获取属性
上文中也提到过,在获取属性之前,我们需要先深入到 DOM 中找到元素节点(使用上面讲的那些方法),然后才能访问该元素的属性。
类似于我们检查 NodeList 的长度那样,获取某个属性之前,先检查一下看看元素中有没有我们想要的属性是一个好主意:我们可以通过 hasAttribute() 方法来做,比如下面这个例子:
html
<!DOCTYPE html>
<html>
<head>
<title>attibute node</title>
<meta charset="utf-8">
</head>
<body>
<img src="./img.jpg" class="show" alt="placehoder image" id="pic">
<script type="text/javascript" src="./located_by_id.js"></script>
</body>
</html>
js
if(document.getElementById('pic').hasAttribute('class')){
a = document.getElementById('pic').getAttribute('class')
console.info(a)
}
最后结果打印出来 show ~~
2. 设置属性
设置属性通过方法 setAttribute() 来实现,
这个方法内置了 if 语句,所以我们不需要预先使用条件判断语句,
这个方法需要两个参数,一个是需要设置的属性名称,第二个是将要设置的属性值。
我们还是看一个例子:
html
<!DOCTYPE html>
<html>
<head>
<title>attibute node</title>
<meta charset="utf-8">
<link rel="stylesheet" type="text/css" href="./located_by_id.css">
</head>
<body>
<img src="./img.jpg" class="visible" alt="placehoder image" id="pic" >
<script type="text/javascript" src="./located_by_id.js"></script>
</body>
</html>
css
.visible {
position: static;
width: 300px;
}
.hidden {
position: absolute;
top: -9999px;
left: -9999px;
}
js
document.getElementById('pic').setAttribute('class', 'hidden')
document.getElementById('pic').setAttribute('title', 'we moved this element off screen')
在这个例子中我们将 class 属性从 visible 变为 hidden 我们还增加了一个 title 属性,由于 class 属性本来就有所以新的 class 值取代了旧的,但是我们之前没有 title 属性所以 setAttribute() 方法会自动创建一个。
这意味着我们可以在 DOM 中动态的创建了一个自己的节点。
3. 移除属性
与获取属性一样,先定位到它所在的元素节点,然后使用 removeattribute() 方法移除,同样的我们一般先使用 hasattribute 方法判断一下待移除的属性是否存在。
还是用上面的例子来举例:
if(document.getElementById('pic').hasAttribute('class')) {
document.getElementById('pic').removeAttribute('class')
}
五. 处理文本节点并修改其内容
在处理文本节点的时候我们主要是修改其中的内容,如果想要变更或修改一大块内容我们可以不用层层解析 DOM 直接通过 innerHTML 属性将其一次性设置到位。
直接上例子:
html
<!DOCTYPE html>
<html>
<head>
<title>text node</title>
<meta charset="utf-8">
<link rel="stylesheet" type="text/css" href="./located_by_id.css">
</head>
<body>
<div id="target-area">
<p>This is our text.</p>
</div>
<script type="text/javascript" src="./located_by_id.js"></script>
</body>
</html>
js
a = document.getElementById('target-area').innerHTML;
console.info(a)
document.getElementById('target-area').innerHTML = '<p>hello world</p>'
输出的结果为:
六、 遍历 DOM
我们在前面已经知道了我们可以通过 Id class 标签名或者 NodeList 中特定的位置等手段直接跳进 DOM 对象当中。但是有的时候,我们不会在 HTMl 文档中加入大量的 class 与 Id 属性,所以使用上述手段只能达到一定是深度,之后我们就只能通过一些 JavaScript 原生方法来在其中上下游走。他们是:
- parentNode
- previousSibling
- nextSibling
- firstChild
- lastChild
1. 在DOM 中动态的添加和移除节点
到目前为止我们所访问的都是 DOM 中已经存在的节点。有时我们需要对DOM 进行一些动态的操作,诸如插入节点、创建新元素、向其中添加内容、将他们移动到文档中的其他位置。JavaScript已经内置了此功能:
- createElement()
- createTextNode()
- appendChild()
- removeChild()