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


节点类型由在Node类型中定义的下列 12 个常量来表示,任何节点类型必居其一:
类型 nodeType常数值 描述
Node.ELEMENT_NODE 1 元素节点
Node.ATTRIBUTE_NODE 2 属性节点
Node.TEXT_NODE 3 文本节点
Node.CDATA_SECTION_NODE 4 字符数据节点(文本不会被解析器解析)
Node.ENTITY_REFERENCE_NODE 5 实体引用节点
Node.ENTITY_NODE 6 实体节点
Node.PROCESSING_INSTRUCTION_NODE 7 处理指令节点
Node.COMMENT_NODE 8 注释节点
Node.DOCUMENT_NODE 9 文档节点(DOM树的根节点)
Node.DOCUMENT_TYPE_NODE 10 向为文档定义的实体提供接口
Node.DOCUMENT_FRAGMENT_NODE 11 表示邻接节点和它们的子树
Node.NOTATION_NODE 12 代表一个符号在DTD中的声明


确定节点类型时,使用 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 text

textContent 属性自动忽略当前节点内部的 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 // true

document 对象本身的 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 // true

上面代码中, d1.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.previousSibling 就是 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() 方法会将其从原来的位置,移动到新位置。

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]);
}

上面代码表示,如果 foo 节点有子节点,就移除第一个子节点。

注意,子节点包括所有类型的节点,并不仅仅是元素节点。哪怕节点只包含一个空格, 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);

该方法有一些使用注意点。

(1)克隆一个节点,会拷贝该节点的所有属性,但是会丧失 addEventListener 方法和 on- 属性(即 node.onclick = fn ),添加在这个节点上的事件回调函数。

(2)该方法返回的节点不在文档之中,即没有任何父节点,必须使用诸如 Node.appendChild 这样的方法添加到文档之中。

(3)克隆一个节点之后,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) // true


Node.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) // 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 的逆方法,可以查看《Text 节点对象》一章,了解更多内容。


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