DOM 概述
DOM
DOM 是 JavaScript 操作网页的接口,全称为“文档对象模型”(Document Object Model
)。它的作用是将网页转为一个 JavaScript 对象,从而可以用脚本进行各种操作(比如增删内容)。
浏览器会根据 DOM 模型,将结构化文档(比如 HTML 和 XML)解析成一系列的节点,再由这些节点组成一个树状结构(DOM Tree)。所有的节点和最终的树状结构,都有规范的对外接口。
DOM 只是一个接口规范,可以用各种语言实现。所以严格地说,DOM 不是 JavaScript 语法的一部分,但是 DOM 操作是 JavaScript 最常见的任务,离开了 DOM,JavaScript 就无法控制网页。另一方面,JavaScript 也是最常用于 DOM 操作的语言。
节点
DOM 的最小组成单位叫做节点(node
)。文档的树形结构(DOM 树),就是由各种不同类型的节点组成。每个节点可以看作是文档树的一片叶子。
节点的类型有七种。
-
Document
:整个文档树的顶层节点 -
DocumentType
:doctype
标签(比如)
-
Element
:网页的各种HTML标签(比如、
等)
-
Attribute
:网页元素的属性(比如class="right"
) -
Text
:标签之间或标签包含的文本 -
Comment
:注释 -
DocumentFragment
:文档的片段
浏览器提供一个原生的节点对象Node
,上面这七种节点都继承了Node
,因此具有一些共同的属性和方法。
节点树
一个文档的所有节点,按照所在的层级,可以抽象成一种树状结构。这种树状结构就是 DOM 树。它有一个顶层节点,下一层都是顶层节点的子节点,然后子节点又有自己的子节点,就这样层层衍生出一个金字塔结构,倒过来就像一棵树。
浏览器原生提供document
节点,代表整个文档。
文档的第一层有两个节点,第一个是文档类型节点(),第二个是 HTML 网页的顶层容器标签
。后者构成了树结构的根节点(
root node
),其他 HTML 标签节点都是它的下级节点。
除了根节点,其他节点都有三种层级关系。
- 父节点关系(
parentNode
):直接的那个上级节点 - 子节点关系(
childNodes
):直接的下级节点 - 同级节点关系(
sibling
):拥有同一个父节点的节点
DOM提供操作接口,用来获取这三种关系的节点。比如,子节点接口包括firstChild
(第一个子节点)和lastChild
(最后一个子节点)等属性,同级节点接口包括nextSibling
(紧邻在后的那个同级节点)和previousSibling
(紧邻在前的那个同级节点)属性。
Node 接口
所有DOM节点对象都继承了Node
接口,拥有一些共同的属性和方法。这是DOM操作的基础。
属性
Node.prototype.nodeType
nodeType
属性返回一个整数值,表示节点的类型。
document.nodeType // 9
Node
对象定义了几个常量,对应这些类型值。
document.nodeType === Node.DOCUMENT_NODE // true
上面代码中,文档节点的nodeType
属性等于常量Node.DOCUMENT_NODE
。
不同节点的nodeType
属性值和对应的常量如下。
- 文档节点(
document
):9,对应常量Node.DOCUMENT_NODE
- 元素节点(
element
):1,对应常量Node.ELEMENT_NODE
- 属性节点(
attr
):2,对应常量Node.ATTRIBUTE_NODE
- 文本节点(
text
):3,对应常量Node.TEXT_NODE
- 文档片断节点(
DocumentFragment
):11,对应常量Node.DOCUMENT_FRAGMENT_NODE
- 文档类型节点(
DocumentType
):10,对应常量Node.DOCUMENT_TYPE_NODE
- 注释节点(
Comment
):8,对应常量Node.COMMENT_NODE
确定节点类型时,使用nodeType
属性是常用方法。
var node = document.documentElement.firstChild;
if (node.nodeType === Node.ELEMENT_NODE) {
console.log('该节点是元素节点');
}
Node.prototype.nodeName
nodeName
属性返回节点的名称。
// hello world
var div = document.getElementById('d1');
div.nodeName // "DIV"
上面代码中,元素节点 不同节点的 只有文本节点( 上面代码中, 该属性是可读写的,设置该属性的值,会用一个新的文本节点,替换所有原来的子节点。它还有一个好处,就是自动对 HTML 标签转义。这很适合用于用户提供的内容。 GoodBye! 上面代码在插入文本时,会将 对于文本节点( 文档节点( 如果无法读到网页的 URL, 该属性的值一般由当前网址的 URL(即 设置了以后, 注意,该属性还包括文本节点和注释节点。因此如果当前节点后面有空格,该属性会返回一个文本节点,内容为空格。 上面代码遍历 注意,该属性还包括文本节点和注释节点。因此如果当前节点前面有空格,该属性会返回一个文本节点,内容为空格。 上面代码中,通过 文档节点( 由于父节点只可能是三种类型:元素节点、文档节点( First span 注意,
// First span
// 上面代码中, 使用该属性,可以遍历某个节点的所有子节点。 文档节点( 上面代码中,文档节点的第一个子节点的类型是10(即文档类型节点),第二个子节点的类型是1(即元素节点)。 注意,除了元素节点, 上面代码新建一个 如果参数节点是 DOM 已经存在的节点, 上面代码中,插入的是一个已经存在的节点 如果 注意,子节点包括所有类型的节点,并不仅仅是元素节点。哪怕节点只包含一个空格, 判断一个节点有没有子节点,有许多种方法,下面是其中的三种。 上面代码中, 该方法有一些使用注意点。 如果 上面代码中, 注意,如果所要插入的节点是当前 DOM 现有的节点,则该节点将从原有的位置移除,插入新的位置。 由于不存在 上面代码中, 如果要插入的节点是 上面代码移除了 下面是如何移除当前节点的所有子节点。 被移除的节点依然存在于内存之中,但不再是 DOM 的一部分。所以,一个节点移除以后,依然可以使用它,比如插入到另一个节点下面。 如果参数节点不是当前节点的子节点, 上面代码中, 上面代码检查参数节点 注意,当前节点传入 上面代码中,节点 由于 上面代码中, 上面代码使用 该方法是 该方法可用于nodeName
属性就是大写的标签名DIV
。
nodeName
属性值如下。
document
):#document
element
):大写的标签名attr
):属性的名称text
):#text
DocumentFragment
):#document-fragment
DocumentType
):文档的类型Comment
):#comment
Node.prototype.nodeValue
nodeValue
属性返回一个字符串,表示当前节点本身的文本值,该属性可读写。text
)、注释节点(comment
)和属性节点(attr
)有文本值,因此这三类节点的nodeValue
可以返回结果,其他类型的节点一律返回null
。同样的,也只有这三类节点可以设置nodeValue
属性的值,其他类型的节点设置无效。// HTML 代码如下
//
div
是元素节点,nodeValue
属性返回null
。div.firstChild
是文本节点,所以可以返回文本值。Node.prototype.textContent
textContent
属性返回当前节点和它的所有后代节点的文本内容。// HTML 代码为
//
textContent
属性自动忽略当前节点内部的 HTML 标签,返回所有文本内容。document.getElementById('foo').textContent = '
标签解释为文本,而不会当作标签处理。
text
)、注释节点(comment
)和属性节点(attr
),textContent
属性的值与nodeValue
属性相同。对于其他类型的节点,该属性会将每个子节点(不包括注释节点)的内容连接在一起返回。如果一个节点没有子节点,则返回空字符串。document
)和文档类型节点(doctype
)的textContent
属性为null
。如果要读取整个文档的内容,可以使用document.documentElement.textContent
。Node.prototype.baseURI
baseURI
属性返回一个字符串,表示当前网页的绝对路径。浏览器根据这个属性,计算网页上的相对路径的 URL。该属性为只读。// 当前网页的网址为
// http://www.example.com/index.html
document.baseURI
// "http://www.example.com/index.html"
baseURI
属性返回null
。window.location
属性)决定,但是可以使用 HTML 的
标签,改变该属性的值。baseURI
属性就返回
标签设置的值。Node.prototype.ownerDocument
Node.ownerDocument
属性返回当前节点所在的顶层文档对象,即document
对象。var d = p.ownerDocument;
d === document // true
document
对象本身的ownerDocument
属性,返回null
。Node.prototype.nextSibling
Node.nextSibling
属性返回紧跟在当前节点后面的第一个同级节点。如果当前节点后面没有同级节点,则返回null
。// HTML 代码如下
//
nextSibling
属性可以用来遍历所有子节点。var el = document.getElementById('div1').firstChild;
while (el !== null) {
console.log(el.nodeName);
el = el.nextSibling;
}
div1
节点的所有子节点。Node.prototype.previousSibling
previousSibling
属性返回当前节点前面的、距离最近的一个同级节点。如果当前节点前面没有同级节点,则返回null
。//
Node.prototype.parentNode
parentNode
属性返回当前节点的父节点。对于一个节点来说,它的父节点只可能是三种类型:元素节点(element
)、文档节点(document
)和文档片段节点(documentfragment
)。if (node.parentNode) {
node.parentNode.removeChild(node);
}
node.parentNode
属性将node
节点从文档里面移除。document
)和文档片段节点(documentfragment
)的父节点都是null
。另外,对于那些生成后还没插入 DOM 树的节点,父节点也是null
。Node.prototype.parentElement
parentElement
属性返回当前节点的父元素节点。如果当前节点没有父节点,或者父节点类型不是元素节点,则返回null
。if (node.parentElement) {
node.parentElement.style.color = 'red';
}
document
)和文档片段节点(documentfragment
)。parentElement
属性相当于把后两种父节点都排除了。Node.prototype.firstChild,Node.prototype.lastChild
firstChild
属性返回当前节点的第一个子节点,如果当前节点没有子节点,则返回null
。//
firstChild
返回的除了元素节点,还可能是文本节点或注释节点。//
p
元素与span
元素之间有空白字符,这导致firstChild
返回的是文本节点。lastChild
属性返回当前节点的最后一个子节点,如果当前节点没有子节点,则返回null
。用法与firstChild
属性相同。Node.prototype.childNodes
childNodes
属性返回一个类似数组的对象(NodeList
集合),成员包括当前节点的所有子节点。var children = document.querySelector('ul').childNodes;
var div = document.getElementById('div1');
var children = div.childNodes;
for (var i = 0; i < children.length; i++) {
// ...
}
document
)就有两个子节点:文档类型节点(docType
)和HTML根元素节点。var children = document.childNodes;
for (var i = 0; i < children.length; i++) {
console.log(children[i].nodeType);
}
// 10
// 1
childNodes
属性的返回值还包括文本节点和注释节点。如果当前节点不包括任何子节点,则返回一个空的NodeList
集合。由于NodeList
对象是一个动态集合,一旦子节点发生变化,立刻会反映在返回结果之中。Node.prototype.isConnected
isConnected
属性返回一个布尔值,表示当前节点是否在文档之中。var test = document.createElement('p');
test.isConnected // false
document.body.appendChild(test);
test.isConnected // true
方法
Node.prototype.appendChild()
appendChild()
方法接受一个节点对象作为参数,将其作为最后一个子节点,插入当前节点。该方法的返回值就是插入文档的子节点。var p = document.createElement('p');
document.body.appendChild(p);
节点,将其插入
document.body
的尾部。appendChild()
方法会将其从原来的位置,移动到新位置。var div = document.getElementById('myDiv');
document.body.appendChild(div);
myDiv
,结果就是该节点会从原来的位置,移动到document.body
的尾部。appendChild()
方法的参数是DocumentFragment
节点,那么插入的是DocumentFragment
的所有子节点,而不是DocumentFragment
节点本身。返回值是一个空的DocumentFragment
节点。Node.prototype.hasChildNodes()
hasChildNodes
方法返回一个布尔值,表示当前节点是否有子节点。var foo = document.getElementById('foo');
if (foo.hasChildNodes()) {
foo.removeChild(foo.childNodes[0]);
}
hasChildNodes
方法也会返回true
。
node.hasChildNodes()
node.firstChild !== null
node.childNodes && node.childNodes.length > 0
hasChildNodes
方法结合firstChild
属性和nextSibling
属性,可以遍历当前节点的所有后代节点。function DOMComb(parent, callback) {
if (parent.hasChildNodes()) {
for (var node = parent.firstChild; node; node = node.nextSibling) {
DOMComb(node, callback);
}
}
callback(parent);
}
// 用法
DOMComb(document.body, console.log)
DOMComb
函数的第一个参数是某个指定的节点,第二个参数是回调函数。这个回调函数会依次作用于指定节点,以及指定节点的所有后代节点。Node.prototype.cloneNode()
cloneNode
方法用于克隆一个节点。它接受一个布尔值作为参数,表示是否同时克隆子节点。它的返回值是一个克隆出来的新节点。var cloneUL = document.querySelector('ul').cloneNode(true);
addEventListener
方法和on-
属性(即node.onclick = fn
),添加在这个节点上的事件回调函数。Node.appendChild
这样的方法添加到文档之中。id
属性(即id="xxx"
)的网页元素,这时应该修改其中一个元素的id
属性。如果原节点有name
属性,可能也需要修改。Node.prototype.insertBefore()
insertBefore
方法用于将某个节点插入父节点内部的指定位置。var insertedNode = parentNode.insertBefore(newNode, referenceNode);
insertBefore
方法接受两个参数,第一个参数是所要插入的节点newNode
,第二个参数是父节点parentNode
内部的一个子节点referenceNode
。newNode
将插在referenceNode
这个子节点的前面。返回值是插入的新节点newNode
。var p = document.createElement('p');
document.body.insertBefore(p, document.body.firstChild);
insertBefore
方法的第二个参数为null
,则新节点将插在当前节点内部的最后位置,即变成最后一个子节点。var p = document.createElement('p');
document.body.insertBefore(p, null);
p
将成为document.body
的最后一个子节点。这也说明insertBefore
的第二个参数不能省略。insertAfter
方法,如果新节点要插在父节点的某个子节点后面,可以用insertBefore
方法结合nextSibling
属性模拟。parent.insertBefore(s1, s2.nextSibling);
parent
是父节点,s1
是一个全新的节点,s2
是可以将s1
节点,插在s2
节点的后面。如果s2
是当前节点的最后一个子节点,则s2.nextSibling
返回null
,这时s1
节点会插在当前节点的最后,变成当前节点的最后一个子节点,等于紧跟在s2
的后面。DocumentFragment
类型,那么插入的将是DocumentFragment
的所有子节点,而不是DocumentFragment
节点本身。返回值将是一个空的DocumentFragment
节点。Node.prototype.removeChild()
removeChild
方法接受一个子节点作为参数,用于从当前节点移除该子节点。返回值是移除的子节点。var divA = document.getElementById('A');
divA.parentNode.removeChild(divA);
divA
节点。注意,这个方法是在divA
的父节点上调用的,不是在divA
上调用的。var element = document.getElementById('top');
while (element.firstChild) {
element.removeChild(element.firstChild);
}
removeChild
方法将报错。Node.prototype.replaceChild()
replaceChild
方法用于将一个新的节点,替换当前节点的某一个子节点。var replacedNode = parentNode.replaceChild(newChild, oldChild);
replaceChild
方法接受两个参数,第一个参数newChild
是用来替换的新节点,第二个参数oldChild
是将要替换走的子节点。返回值是替换走的那个节点oldChild
。var divA = document.getElementById('divA');
var newSpan = document.createElement('span');
newSpan.textContent = 'Hello World!';
divA.parentNode.replaceChild(newSpan, divA);
Node.prototype.contains()
contains
方法返回一个布尔值,表示参数节点是否满足以下三个条件之一。
document.body.contains(node)
node
,是否包含在当前文档之中。contains
方法,返回true
。nodeA.contains(nodeA) // true
Node.prototype.compareDocumentPosition()
compareDocumentPosition
方法的用法,与contains
方法完全一致,返回一个六个比特位的二进制值,表示参数节点与当前节点的关系。
二进制值
十进制值
含义
000000
0
两个节点相同
000001
1
两个节点不在同一个文档(即有一个节点不在当前文档)
000010
2
参数节点在当前节点的前面
000100
4
参数节点在当前节点的后面
001000
8
参数节点包含当前节点
010000
16
当前节点包含参数节点
100000
32
浏览器内部使用
// HTML 代码如下
//
div
包含节点input
(二进制010000
),而且节点input
在节点div
的后面(二进制000100
),所以第一个compareDocumentPosition
方法返回20
(二进制010100
,即010000 + 000100
),第二个compareDocumentPosition
方法返回10
(二进制001010
)。compareDocumentPosition
返回值的含义,定义在每一个比特位上,所以如果要检查某一种特定的含义,就需要使用比特位运算符。var head = document.head;
var body = document.body;
if (head.compareDocumentPosition(body) & 4) {
console.log('文档结构正确');
} else {
console.log(' 不能在 前面');
}
compareDocumentPosition
的返回值与4
(又称掩码)进行与运算(&
),得到一个布尔值,表示是否在
前面。
Node.prototype.isEqualNode(),Node.prototype.isSameNode()
isEqualNode
方法返回一个布尔值,用于检查两个节点是否相等。所谓相等的节点,指的是两个节点的类型相同、属性相同、子节点相同。var p1 = document.createElement('p');
var p2 = document.createElement('p');
p1.isEqualNode(p2) // true
isSameNode
方法返回一个布尔值,表示两个节点是否为同一个节点。var p1 = document.createElement('p');
var p2 = document.createElement('p');
p1.isSameNode(p2) // false
p1.isSameNode(p1) // true
Node.prototype.normalize()
normalize
方法用于清理当前节点内部的所有文本节点(text
)。它会去除空的文本节点,并且将毗邻的文本节点合并成一个,也就是说不存在空的文本节点,以及毗邻的文本节点。var wrapper = document.createElement('div');
wrapper.appendChild(document.createTextNode('Part 1 '));
wrapper.appendChild(document.createTextNode('Part 2 '));
wrapper.childNodes.length // 2
wrapper.normalize();
wrapper.childNodes.length // 1
normalize
方法之前,wrapper
节点有两个毗邻的文本子节点。使用normalize
方法之后,两个文本子节点被合并成一个。Text.splitText
的逆方法。Node.prototype.getRootNode()
getRootNode()
方法返回当前节点所在文档的根节点document
,与ownerDocument
属性的作用相同。document.body.firstChild.getRootNode() === document
// true
document.body.firstChild.getRootNode() === document.body.firstChild.ownerDocument
// true
document
节点自身,这一点与document.ownerDocument
不同。document.getRootNode() // document
document.ownerDocument // null