# Node 接口

网道（WangDoc.com），互联网文档计划

所有 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 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

本教程采用知识共享 署名-相同方式共享 3.0协议。

分享本文