JS 回顾 6
约 7958 字大约 27 分钟
2026-02-01
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标签(比如<!DOCTYPE html>)Element:网页的各种 HTML 标签(比如<body>、<a>等)Attr:网页元素的属性(比如class="right")Text:标签之间或标签包含的文本Comment:注释DocumentFragment:文档的片段
浏览器提供一个原生的节点对象 Node,上面这七种节点都继承了 Node,因此具有一些共同的属性和方法。
节点树
一个文档的所有节点,按照所在的层级,可以抽象成一种树状结构。
这种树状结构就是 DOM 树,它有一个顶层节点,下一层都是顶层节点的子节点,然后子节点又有自己的子节点,就这样层层衍生出一个金字塔结构,又像一棵树。
浏览器原生提供 document 节点,代表整个文档。
文档的第一层有两个节点,第一个是文档类型节点(<!doctype html>),第二个是 HTML 网页的顶层容器标签 <html>。
后者构成了树结构的根节点(root node),其他 HTML 标签节点都是它的下级节点。
除了根节点,其他节点都有三种层级关系。
- 父节点关系(parentNode):直接的那个上级节点
- 子节点关系(childNodes):直接的下级节点
- 同级节点关系(sibling):拥有同一个父节点的节点
DOM 提供操作接口,用来获取这三种关系的节点。
比如,子节点接口包括 firstChild(第一个子节点)和 lastChild(最后一个子节点)等属性,同级节点接口包括 nextSibling(紧邻在后的那个同级节点)和 previousSibling(紧邻在前的那个同级节点)属性。
Node 接口
所有 DOM 节点对象都继承了 Node 接口,拥有一些共同的属性和方法,这是 DOM 操作的基础。
属性
Node.prototype.nodeType
nodeType 属性返回一个整数值,表示节点的类型。
document.nodeType // 9上面代码中,文档节点的类型值为 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 属性返回节点的名称。
// HTML 代码如下
// <div id="d1"> hello world </div>
var div = document.getElementById('d1');
div.nodeName // "DIV"元素节点 <div> 的 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 id="d1"> hello world </div>
var div = document.getElementById('d1');
div.nodeValue // null
div.firstChild.nodeValue // "hello world"div 是元素节点,nodeValue 属性返回 null,div.firstChild 是文本节点,所以可以返回文本值。
Node.prototype.textContent
textContent 属性返回当前节点和它的所有后代节点的文本内容。
// HTML 代码为
// <div id="divA"> This is <span> some </span> text </div>
document.getElementById('divA').textContent
// This is some texttextContent 属性自动忽略当前节点内部的 HTML 标签,返回所有文本内容。
该属性是可读写的,设置该属性的值,会用一个新的文本节点,替换所有原来的子节点。它还有一个好处,就是自动对 HTML 标签转义。这很适合用于用户提供的内容。
document.getElementById('foo').textContent = '<p>GoodBye!</p>';上面代码在插入文本时,会将 <p> 标签解释为文本,而不会当作标签处理。
对于文本节点(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"如果无法读到网页的 URL,baseURI 属性返回 null。
该属性的值一般由当前网址的 URL(即 window.location 属性)决定,但是可以使用 HTML 的 <base> 标签,改变该属性的值。
<base href="http://www.example.com/page.html">设置了以后,baseURI 属性就返回 <base> 标签设置的值。
Node.prototype.ownerDocument
Node.ownerDocument 属性返回当前节点所在的顶层文档对象,即 document 对象。
var d = p.ownerDocument;
d === document // truedocument 对象本身的 ownerDocument 属性,返回 null。
Node.prototype.nextSibling
Node.nextSibling 属性返回紧跟在当前节点后面的第一个同级节点。如果当前节点后面没有同级节点,则返回 null。
// HTML 代码如下
// <div id="d1"> hello </div> <div id="d2"> world </div>
var d1 = document.getElementById('d1');
var d2 = document.getElementById('d2');
d1.nextSibling === d2 // trued1.nextSibling 就是紧跟在 d1 后面的同级节点 d2。
注意:该属性还包括文本节点和注释节点(<!-- comment -->)。因此如果当前节点后面有空格,该属性会返回一个文本节点,内容为空格。
nextSibling 属性可以用来遍历所有子节点。
var el = document.getElementById('div1').firstChild;
while (el !== null) {
console.log(el.nodeName);
el = el.nextSibling;
}上面代码遍历 div1 节点的所有子节点。
Node.prototype.previousSibling
previousSibling 属性返回当前节点前面的、距离最近的一个同级节点。如果当前节点前面没有同级节点,则返回 null。
// HTML 代码如下
// <div id="d1"> hello </div> <div id="d2"> world </div>
var d1 = document.getElementById('d1');
var d2 = document.getElementById('d2');
d2.previousSibling === d1 // true上面代码中,d2.e 就是 d2 前面的同级节点 d1。
注意:该属性还包括文本节点和注释节点。因此如果当前节点前面有空格,该属性会返回一个文本节点,内容为空格。
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。
// HTML 代码如下
// <p id="p1"> <span> First span </span> </p>
var p1 = document.getElementById('p1');
p1.firstChild.nodeName // "SPAN"上面代码中,p 元素的第一个子节点是 span 元素。
注意:firstChild 返回的除了元素节点,还可能是文本节点或注释节点。
// HTML 代码如下
// <p id="p1">
// <span> First span </span>
// </p>
var p1 = document.getElementById('p1');
p1.firstChild.nodeName // "#text"上面代码中,p 元素与 span 元素之间有空白字符,这导致 firstChild 返回的是文本节点。
lastChild 属性返回当前节点的最后一个子节点,如果当前节点没有子节点,则返回 null,用法与 firstChild 属性相同。
Node.prototype.childNodes
childNodes 属性返回一个类似数组的对象(NodeList 集合),成员包括当前节点的所有子节点。
var children = document.querySelector('ul').childNodes;上面代码中,children 就是 ul 元素的所有子节点。
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上面代码中,文档节点的第一个子节点的类型是 10(即文档类型节点),第二个子节点的类型是 1(即元素节点)。
注意:除了元素节点,childNodes 属性的返回值还包括文本节点和注释节点。如果当前节点不包括任何子节点,则返回一个空的 NodeList 集合。由于 NodeList 对象是一个动态集合,一旦子节点发生变化,立刻会反映在返回结果之中。
Node.prototype.isConnected
isConnected 属性返回一个布尔值,表示当前节点是否在文档之中。
var test = document.createElement('p');
test.isConnected // false
document.body.appendChild(test);
test.isConnected // true上面代码中,test 节点是脚本生成的节点,没有插入文档之前,isConnected 属性返回 false,插入之后返回 true。
方法
Node.prototype.appendChild()
appendChild() 方法接受一个节点对象作为参数,将其作为最后一个子节点,插入当前节点。该方法的返回值就是插入文档的子节点。
var p = document.createElement('p');
document.body.appendChild(p);上面代码新建一个 <p> 节点,将其插入 document.body 的尾部。
如果参数节点是 DOM 已经存在的节点,appendChild() 方法会将其从原来的位置,移动到新位置。
如果 appendChild() 方法的参数是 DocumentFragment 节点,那么插入的是 DocumentFragment 的所有子节点,而不是 DocumentFragment 节点本身。返回值是一个空的 DocumentFragment 节点。
Node.prototype.hasChildNodes()
hasChildNodes 方法返回一个布尔值,表示当前节点是否有子节点。
var foo = document.getElementById('foo');
if (foo.hasChildNodes()) {
foo.removeChild(foo.childNodes[0]);
}上面代码表示,如果 foo 节点有子节点,就移除第一个子节点。
注意:子节点包括所有类型的节点,并不仅仅是元素节点。哪怕节点只包含一个空格,hasChildNodes 方法也会返回 true。
判断一个节点有没有子节点,有许多种方法,下面是其中的三种。
node.hasChildNodes()node.firstChild !== nullnode.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这样的方法添加到文档之中。克隆一个节点之后,DOM 有可能出现两个有相同
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);上面代码中,新建一个 <p> 节点,插在 document.body.firstChild 的前面,也就是成为 document.body 的第一个子节点。
如果 insertBefore 方法的第二个参数为 null,则新节点将插在当前节点内部的最后位置,即变成最后一个子节点。
var p = document.createElement('p');
document.body.insertBefore(p, null);上面代码中,p 将成为 document.body 的最后一个子节点。这也说明 insertBefore 的第二个参数不能省略。
注意:如果所要插入的节点是当前 DOM 现有的节点,则该节点将从原有的位置移除,插入新的位置。
由于不存在 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);
}被移除的节点依然存在于内存之中,但不再是 DOM 的一部分。所以,一个节点移除以后,依然可以使用它,比如插入到另一个节点下面。
如果参数节点不是当前节点的子节点,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);上面代码是如何将指定节点 divA 替换走。
Node.prototype.contains()
contains 方法返回一个布尔值,表示参数节点是否满足以下三个条件之一。
- 参数节点为当前节点。
- 参数节点为当前节点的子节点。
- 参数节点为当前节点的后代节点。
document.body.contains(node)上面代码检查参数节点 node,是否包含在当前文档之中。
注意,当前节点传入 contains 方法,返回 true。
nodeA.contains(nodeA) // trueNode.prototype.compareDocumentPosition()
compareDocumentPosition 方法的用法,与 contains 方法完全一致,返回一个六个比特位的二进制值,表示参数节点与当前节点的关系。
| 二进制值 | 十进制值 | 含义 |
|---|---|---|
| 000000 | 0 | 两个节点相同 |
| 000001 | 1 | 两个节点不在同一个文档(即有一个节点不在当前文档) |
| 000010 | 2 | 参数节点在当前节点的前面 |
| 000100 | 4 | 参数节点在当前节点的后面 |
| 001000 | 8 | 参数节点包含当前节点 |
| 010000 | 16 | 当前节点包含参数节点 |
| 100000 | 32 | 浏览器内部使用 |
// HTML 代码如下
// <div id="mydiv">
// <form> <input id="test" /> </form>
// </div>
var div = document.getElementById('mydiv');
var input = document.getElementById('test');
div.compareDocumentPosition(input) // 20
input.compareDocumentPosition(div) // 10上面代码中,节点 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('<body> 不能在 <head> 前面');
}上面代码中,compareDocumentPosition 的返回值与 4(又称掩码)进行与运算(&),得到一个布尔值,表示 <head> 是否在 <body> 前面。
Node.prototype.isEqualNode()
Node.prototype.isSameNode()
isEqualNode 方法返回一个布尔值,用于检查两个节点是否相等。所谓相等的节点,指的是两个节点的类型相同、属性相同、子节点相同。
var p1 = document.createElement('p');
var p2 = document.createElement('p');
p1.isEqualNode(p2) // trueisSameNode 方法返回一个布尔值,表示两个节点是否为同一个节点。
var p1 = document.createElement('p');
var p2 = document.createElement('p');
p1.isSameNode(p2) // false
p1.isSameNode(p1) // trueNode.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节点都是单个对象,有时需要一种数据结构,能够容纳多个节点。DOM 提供两种节点集合,用于容纳多个节点:NodeList 和 HTMLCollection。
这两种集合都属于接口规范。许多 DOM 属性和方法,返回的结果是 NodeList 实例或 HTMLCollection 实例。主要区别是,NodeList 可以包含各种类型的节点,HTMLCollection 只能包含 HTML 元素节点。
NodeList 接口
NodeList 实例是一个类似数组的对象,它的成员是节点对象。通过以下方法可以得到 NodeList 实例。
Node.childNodesdocument.querySelectorAll()等节点搜索方法
document.body.childNodes instanceof NodeList // trueNodeList 实例很像数组,可以使用 length 属性和 forEach 方法,但是,它不是数组,不能使用 pop 或 push 之类数组特有的方法。
var children = document.body.childNodes;
Array.isArray(children) // false
children.length // 34
children.forEach(console.log)上面代码中,NodeList 实例 children 不是数组,但是具有 length 属性和 forEach 方法。
如果 NodeList 实例要使用数组方法,可以将其转为真正的数组。
var children = document.body.childNodes;
var nodeArr = Array.prototype.slice.call(children);除了使用 forEach 方法遍历 NodeList 实例,还可以使用 for 循环。
var children = document.body.childNodes;
for (var i = 0; i < children.length; i++) {
var item = children[i];
}注意:NodeList 实例可能是动态集合,也可能是静态集合。所谓动态集合就是一个活的集合,DOM 删除或新增一个相关节点,都会立刻反映在 NodeList 实例。目前,只有 Node.childNodes 返回的是一个动态集合,其他的 NodeList 都是静态集合。
var children = document.body.childNodes;
children.length // 18
document.body.appendChild(document.createElement('p'));
children.length // 19上面代码中,文档增加一个子节点,NodeList 实例 children 的 length 属性就增加了 1。
NodeList.prototype.length
length 属性返回 NodeList 实例包含的节点数量。
document.querySelectorAll('xxx').length
// 0上面代码中,document.querySelectorAll 返回一个 NodeList 集合。对于那些不存在的 HTML 标签,length 属性返回 0。
NodeList.prototype.forEach()
forEach 方法用于遍历 NodeList 的所有成员。它接受一个回调函数作为参数,每一轮遍历就执行一次这个回调函数,用法与数组实例的 forEach 方法完全一致。
var children = document.body.childNodes;
children.forEach(function f(item, i, list) {
// ...
}, this);上面代码中,回调函数 f 的三个参数依次是当前成员、位置和当前 NodeList 实例。forEach 方法的第二个参数,用于绑定回调函数内部的 this,该参数可省略。
NodeList.prototype.item()
item 方法接受一个整数值作为参数,表示成员的位置,返回该位置上的成员。
document.body.childNodes.item(0)上面代码中,item(0) 返回第一个成员。
如果参数值大于实际长度,或者索引不合法(比如负数),item 方法返回 null。如果省略参数,item 方法会报错。
所有类似数组的对象,都可以使用方括号运算符取出成员。一般情况下,都是使用方括号运算符,而不使用 item 方法。
document.body.childNodes[0]NodeList.prototype.keys()
NodeList.prototype.values()
NodeList.prototype.entries()
这三个方法都返回一个 ES6 的遍历器对象,可以通过 for...of 循环遍历获取每一个成员的信息。区别在于,keys() 返回键名的遍历器,values() 返回键值的遍历器,entries() 返回的遍历器同时包含键名和键值的信息。
var children = document.body.childNodes;
for (var key of children.keys()) {
console.log(key);
}
// 0
// 1
// 2
// ...
for (var value of children.values()) {
console.log(value);
}
// #text
// <script>
// ...
for (var entry of children.entries()) {
console.log(entry);
}
// Array [ 0, #text ]
// Array [ 1, <script> ]
// ...HTMLCollection 接口
HTMLCollection 是一个节点对象的集合,只能包含元素节点(element),不能包含其他类型的节点。它的返回值是一个类似数组的对象,但是与 NodeList 接口不同,HTMLCollection 没有 forEach 方法,只能使用 for 循环遍历。
返回 HTMLCollection 实例的,主要是一些 Document 对象的集合属性,比如 document.links、document.forms、document.images 等。
document.links instanceof HTMLCollection // trueHTMLCollection 实例都是动态集合,节点的变化会实时反映在集合中。
如果元素节点有 id 或 name 属性,那么 HTMLCollection 实例上面,可以使用 id 属性或 name 属性引用该节点元素。如果没有对应的节点,则返回 null。
// HTML 代码如下
// <img id="pic" src="http://example.com/foo.jpg">
var pic = document.getElementById('pic');
document.images.pic === pic // true上面代码中,document.images 是一个 HTMLCollection 实例,可以通过 <img> 元素的 id 属性值,从 HTMLCollection 实例上取到这个元素。
HTMLCollection.prototype.length
length 属性返回 HTMLCollection 实例包含的成员数量。
document.links.length // 18HTMLCollection.prototype.item()
item 方法接受一个整数值作为参数,表示成员的位置,返回该位置上的成员。
var c = document.images;
var img0 = c.item(0);上面代码中,item(0) 表示返回 0 号位置的成员。由于方括号运算符也具有同样作用,而且使用更方便,所以一般情况下,总是使用方括号运算符。
如果参数值超出成员数量或者不合法(比如小于 0),那么 item 方法返回 null。
HTMLCollection.prototype.namedItem()
namedItem 方法的参数是一个字符串,表示 id 属性或 name 属性的值,返回当前集合中对应的元素节点。如果没有对应的节点,则返回 null。
// HTML 代码如下
// <img id="pic" src="http://example.com/foo.jpg">
var pic = document.getElementById('pic');
document.images.namedItem('pic') === pic // trueCollection.namedItem('value') 等同于 Collection['value']。
节点对象除了继承 Node 接口以外,还拥有其他接口。ParentNode 接口表示当前节点是一个父节点,提供一些处理子节点的方法。ChildNode 接口表示当前节点是一个子节点,提供一些相关方法。
ParentNode 接口
如果当前节点是父节点,就会混入了(mixin)ParentNode 接口。由于只有元素节点(element)、文档节点(document)和文档片段节点(documentFragment)拥有子节点,因此只有这三类节点会拥有 ParentNode 接口。
ParentNode.children
children 属性返回一个 HTMLCollection 实例,成员是当前节点的所有元素子节点。该属性只读。
下面是遍历某个节点的所有元素子节点的示例。
for (var i = 0; i < el.children.length; i++) {
// ...
}注意:children 属性只包括元素子节点,不包括其他类型的子节点(比如文本子节点)。如果没有元素类型的子节点,返回值 HTMLCollection 实例的 length 属性为 0。
另外,HTMLCollection 是动态集合,会实时反映 DOM 的任何变化。
ParentNode.firstElementChild
firstElementChild 属性返回当前节点的第一个元素子节点。如果没有任何元素子节点,则返回 null。
document.firstElementChild.nodeName
// "HTML"上面代码中,document 节点的第一个元素子节点是 <HTML>。
ParentNode.lastElementChild
lastElementChild 属性返回当前节点的最后一个元素子节点,如果不存在任何元素子节点,则返回 null。
document.lastElementChild.nodeName
// "HTML"上面代码中,document 节点的最后一个元素子节点是 <HTML>(因为 document 只包含这一个元素子节点)。
ParentNode.childElementCount
childElementCount 属性返回一个整数,表示当前节点的所有元素子节点的数目。如果不包含任何元素子节点,则返回 0。
document.body.childElementCount // 13ParentNode.append()
append() 方法为当前节点追加一个或多个子节点,位置是最后一个元素子节点的后面。
该方法不仅可以添加元素子节点(参数为元素节点),还可以添加文本子节点(参数为字符串)。
var parent = document.body;
// 添加元素子节点
var p = document.createElement('p');
parent.append(p);
// 添加文本子节点
parent.append('Hello');
// 添加多个元素子节点
var p1 = document.createElement('p');
var p2 = document.createElement('p');
parent.append(p1, p2);
// 添加元素子节点和文本子节点
var p = document.createElement('p');
parent.append('Hello', p);该方法没有返回值。
注意:该方法与 Node.prototype.appendChild() 方法有三点不同。
append()允许字符串作为参数,appendChild()只允许子节点作为参数。append()没有返回值,而appendChild()返回添加的子节点。append()可以添加多个子节点和字符串(即允许多个参数),appendChild()只能添加一个节点(即只允许一个参数)。
ParentNode.prepend()
prepend() 方法为当前节点追加一个或多个子节点,位置是第一个元素子节点的前面。它的用法与 append() 方法完全一致,也是没有返回值。
ChildNode 接口
如果一个节点有父节点,那么该节点就拥有了 ChildNode 接口。
ChildNode.remove()
remove() 方法用于从父节点移除当前节点。
el.remove()上面代码在 DOM 里面移除了 el 节点。
ChildNode.before()
before() 方法用于在当前节点的前面,插入一个或多个同级节点。两者拥有相同的父节点。
注意:该方法不仅可以插入元素节点,还可以插入文本节点。
var p = document.createElement('p');
var p1 = document.createElement('p');
// 插入元素节点
el.before(p);
// 插入文本节点
el.before('Hello');
// 插入多个元素节点
el.before(p, p1);
// 插入元素节点和文本节点
el.before(p, 'Hello');ChildNode.after()
after() 方法用于在当前节点的后面,插入一个或多个同级节点,两者拥有相同的父节点。用法与 before 方法完全相同。
ChildNode.replaceWith()
replaceWith() 方法使用参数节点,替换当前节点。参数可以是元素节点,也可以是文本节点。
var span = document.createElement('span');
el.replaceWith(span);上面代码中,el 节点将被 span 节点替换。
Document 节点
document 节点对象代表整个文档,每张网页都有自己的 document 对象。window.document 属性就指向这个对象。只要浏览器开始载入 HTML 文档,该对象就存在了,可以直接使用。
document 对象有不同的办法可以获取。
- 正常的网页,直接使用
document或window.document。 iframe框架里面的网页,使用iframe节点的contentDocument属性。- Ajax 操作返回的文档,使用
XMLHttpRequest对象的responseXML属性。 - 内部节点的
ownerDocument属性。
document 对象继承了 EventTarget 接口和 Node 接口,并且混入(mixin)了 ParentNode 接口。这意味着,这些接口的方法都可以在 document 对象上调用。除此之外,document 对象还有很多自己的属性和方法。
属性
快捷方式属性
以下属性是指向文档内部的某个节点的快捷方式。
document.defaultView
document.defaultView 属性返回 document 对象所属的 window 对象。如果当前文档不属于 window 对象,该属性返回 null。
document.defaultView === window // truedocument.doctype
对于 HTML 文档来说,document对象一般有两个子节点。第一个子节点是document.doctype,指向<DOCTYPE>节点,即文档类型(Document Type Declaration,简写DTD)节点。HTML 的文档类型节点,一般写成<!DOCTYPE html>。如果网页没有声明 DTD,该属性返回null。
var doctype = document.doctype;
doctype // "<!DOCTYPE html>"
doctype.name // "html"document.firstChild通常就返回这个节点。
document.documentElement
document.documentElement属性返回当前文档的根元素节点(root)。它通常是document节点的第二个子节点,紧跟在document.doctype节点后面。HTML网页的该属性,一般是<html>节点。
document.body
document.head
document.body属性指向<body>节点,document.head属性指向<head>节点。
这两个属性总是存在的,如果网页源码里面省略了<head>或<body>,浏览器会自动创建。另外,这两个属性是可写的,如果改写它们的值,相当于移除所有子节点。
document.scrollingElement
document.scrollingElement属性返回文档的滚动元素。也就是说,当文档整体滚动时,到底是哪个元素在滚动。
标准模式下,这个属性返回的文档的根元素document.documentElement(即<html>)。兼容(quirk)模式下,返回的是<body>元素,如果该元素不存在,返回null。
// 页面滚动到浏览器顶部
document.scrollingElement.scrollTop = 0;document.activeElement
document.activeElement属性返回获得当前焦点(focus)的 DOM 元素。通常,这个属性返回的是<input>、<textarea>、<select>等表单元素,如果当前没有焦点元素,返回<body>元素或null。
document.fullscreenElement
document.fullscreenElement属性返回当前以全屏状态展示的 DOM 元素。如果不是全屏状态,该属性返回null。
if (
document.fullscreenElement &&
document.fullscreenElement.nodeName == 'VIDEO'
) {
console.log('全屏播放视频');
}上面代码中,通过document.fullscreenElement可以知道<video>元素有没有处在全屏状态,从而判断用户行为。