DOM 模型概述

基本概念

DOM

DOM是JavaScript操作网页的接口,全称为”文档对象模型”(Doucment Object Model)。它的作用是将网页装换为一个JavaScript对象,从而可以用脚本进行各种操作。

浏览器会根据DOM模型,将结构化文档(比如HTML和XML)解析成一系列节点,再由这些节点组成一个树状结构(DOM Tree)。所有的节点和最终的树状结构,都有规范的对外接口。所以,DOM可以理解为网页的变成接口。DOM有自己的国际标准,目前通用版本是DOM3。

严格的说,DOM不属于JavaScipt,但是操作DOM是JavaScript最常见的任务,而JavaScript也是最常用与DOM操作的语言。本章介绍的就是JavaScript对DOM标准的实现和用法。

节点

DOM的最小单位叫做节点(node)。文档的树形结构(DOM树),就是由各种不同类型的节点组成。每个节点可以看作文档树的一片叶子。

节点的类型有7种。

Document:整个文档树的顶层节点
DocumentType : doctype标签(比如)
Element : 网页的各种HTML标签(比如.等)
Attribute : 网页元素的属性(比如class = “right”)
Text : 标签之间或标签包含的文本
Comment: 注释
DocumentFragment : 文档的片段

这七中节点都属于浏览器原生提供的节点对象的派生对象,具有共同的属性和方法。

节点树

一个文档的多有节点,按照所在的层级,可以抽象为一种树状结构。这种结构就是DOM。
最顶层的节点就是document节点,它代表了真个文档,文档里面最高一层的HTML标签,一般是,它构成树状结构的根节点(root node),其他HTML标签都是它的下级。

除了根节点以外,其他节点对于周围的节点都存在三种关系。

父节点关系(parentNode) : 直接那个上级节点
子节点关系(childNodes) : 直接的下级节点
同级节点关系(sibling) : 拥有同一个父节点的节点

DOM提供操作接口,用来获取三种关系的节点。其中,子节点接口包括firstChild(第一个子节点)和lastChild(最后一个子节点)等属性,同级节点接口包括nextSibling(紧邻在后面的那个同级节点)和previousSibling(紧跟在前的那个同级节点)属性。

特征相关属性

所有节点对象都是浏览器内置对象的实例,继承了Node属性和方法。这是所有节点共同的特征。以下属性与对象本身的特征相关。

Node.nodeName,Node.nodeType

nodeName属性返回节点名称,nodeType属性返回节点类型的常数值。具体的返回值,可查阅下方的表格。

类型 nodeName nodeType
ELEMENT_NODE 大写的HTML元素名 1
ATTRIBUTE_NODE 等同于Attr.name 2
TEXT_NODE text 3
COMMENT_NODE comment 8
DOCUMENT_NODE document 9
DOCUMENT_FRAGMENT_NODE document-fragment 11
DOCUMENT_TYPE_NODE 等同于DocumentType.name 10

以document节点为例,它的nodeName属性等于#document,nodeType属性等于9。

document.nodeName // "#document"
document.nodeType // 9

如果一个

节点,它的nodeName是P,nodeType是1。文本节点的nodeName是#text,nodeType是3。

通常来说,nodeType属性确定一个节点的类型比较方便。

document.querySelector('a').nodeType === 1
// true
document.querySelector('a').nodeType === Node.ELEMENT_NODE

上面两种写法是等价的。

Node.nodeValue

Node.nodeValue属性返回一个字符串,表示当前节点本身的文本值,该属性可读写。

由于只有Text节点,Comment节点,XML文档的CDATA节点有文本值,因此只有这三类节点的nodeValue可以返回结果,其他类型的节点一律返回null。同样的,也只有这三类节点可以设置nodeValue属性的值。对于那些返回null的节点,设置nodeValue属性是无效的。

Node.textContent

Node.textContent属性返回当前节点和它所有后代节点的文本内容。

// HTML代码为 <div id="divA">This is sometextdiv>
document.getElementById('divA').textContent
// This is some text

textContent属性会忽略当前节点内容的HTML标签,返回所有文本内容。

该属性是可读写的,设置该属性的值,会用一个新的文本节点,替换所有原来的子节点。它还有一个好处,就是对HTML标签转移。这很适合用于用户提供的内容。

document.getElementById('foo').textContent = '

GoodBye

';

上面代码插入文本时,会将

标签解释为文本,而不会当做标签处理。

对于Text节点和Comment节点,该属性的值与nodeValue属性值相同。对于其他类型的节点,该属性会将每个子节点的内容连接在一起返回,但是不包括Comment节点。如果一个节点没有子节点,则返回空字符串。

document节点和doctype节点的textContent属性为null。如果要读取整个文档的内容,可是使用document.documentElement.textContent。

Node.baseURI

Node.baseURI属性返回一个字符串,表示当前网页的绝对路径。如果无法获取这个值,则返回null。浏览器根据这个属性,计算网页上的相对路径的URL。该属性只读。

// 当前网页的网址为
// http://www.example.com/index.html
document.baseURI
// "http://www.example.com/index.html"

不同节点都可以调用这个属性(比如document.baseURI和element.baseURI),通常他们的值都是相同的。
该属性的值一般由当前网址的URL(即window.location属性决定),但是可以使用HTML的标签,该表该属性的值。

<base href="http://www.example.com/page.html">
<base target="_blank" href="http://www.example.com/page.html">

设置了以后,baseURI属性就返回标签设置的值。

相关节点的属性

以下属性返回当前节点的相关节点。

Node.ownerDocument

Node.ownerDocument属性返回当前节点所在的顶层文档对象,即Document对象。

var d = p.ownerDocument;
d === document;

document对象本身的ownerDocument属性,返回null。

Node.nextSibling

Node.nextSibling属性返回紧跟在当前节点后面的第一个同级节点。如果当前节点后面没有同级节点,则返回null。注意,该属性还包括文本节点和评论节点。因此如果当前节点后面有空格,该属性会返回一个文本节点,内容为空格。

var el = document,getElementById('div-01').firstChild;
var i = 1;
while(el){
    console.log(i + '. ' + el.nodeName);
    el = el.nextSiling;
    i++;
}

上面代码遍历div-01节点的所有子节点。
下面两个表达式指向同一个节点。

document.childNodes[0].childNodes[1];
document.firstChild.firstChild.nextSibling;

Node.previousSibling

previousSibling属性返回当前节点前面的、距离最近的一个同级节点。如果当前节点没有同级节点,则返回null。

// html代码如下
// <a><span id="b1">span><span id="b2">span>a>
document.getElementById('b1').previousSibling // null
document.getElementById('b2').previousSibling.id // "b1"

对于当前节点前面有空格,则previousSibiling属性会返回一个内容为空格的文本节点。

Node.parentNode

parentNode属性返回当前节点的父节点。对于一个节点来说,它的父节点只可能是三种类型:element节点、document节点和documentfragment节点。

下面代码是如何从父节点移除指定节点。

if(node.parentNode){
    node.parentNode.removeChild(node);
}

对于document节点和documentfragment节点,它的父节点都是null。另外,对于那些生成了后还没插入DOM树的节点,父节点也是null。

Node.parentElement

parentElement属性返回当前节点的父Element节点。如果当前节点没有父节点,或者父节点的类型不是Element节点,则返回null。

if(node.parentElement){
    node.parentElement.style.color = "red";
}

上面代码设置节点的父Element节点的CSS属性。

在IE浏览器中,只有Element节点才有该属性,其他浏览器则是所有类型的节点都有该属性。

Node.childNodes

childNodes属性返回一个NodeList集合,成员包括当前节点的所有子节点。注意,除了HTML元素节点,该属性返回的还包括Text节点和Comment节点。如果当前节点部包括任何子节点,则返回一个空的NodeList集合。由于NodeList对象是一个动态集合,一旦子节点发生变化,立刻会反映在返回结果之中。

var ulElementChildNodes = document.querySelector('ul').childNodes;

Node.firstChild,Node.lastChild

firstChild属性返回当前节点的第一个子节点,如果当前节点没有子节点,则返回null(注意,不是undefined)。

<p id="para-01"><span>First spanspan>p>
<script type="text/javascript">
    console.log(
        document.getElementById('para-01').firstChild.nodeName
    ) // "SPAN"
script>

上面代码中,p元素的第一个子节点是span元素。

注意,firstChild返回的除了HTML元素子节点,还可能是文本节点或者评论节点。

<p id="para-01">
    <span>First spanspan>
p>
<script type="text/javascript">
    console.log(
        document.getElementById('para-01').firstChild.nodeName
    )// "text"
script>

上面代码中,p元素与span元祖之间有空白符,这导致firstChild返回的是文本节点。
Node.lastChild属性返回当前节点的最后一个子节点,如果当前节点没有子节点,则返回null。

节点对象的方法

Node.appendChild()

node.appendChild方法可以接受一个节点作为参数,将其作为最后一个子节点,插入当前节点。

var p = document.createElement(p);
document.body.appendChild(p);

如果参数是DOM中已经存在的节点,appendChild方法将会将其从原来的位置,移动到新位置。

Node.hasChildNodes()

Node.hasChildNodes方法返回一个布尔值,表示当前节点是否有子节点。

var foo = document.getElementById("foo");
if(hasChildNodes()){
    foo.removeChild(foo.childNodes[0]);
}

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

hasChildNodes方法结合firstChild属性和nextSibling属性,可以遍历当前节点的所有后代节点。

function DOMComb(parent,callback){
    if(parent.hasChildNodes()){
        for(var node = parent.firstChild;node;node = node.nextSibling){
            DOMComb(node,callback);
        }
    }
    callback.call(parent);
}

上面代码的DOMComb函数的第一个参数是某个指定的节点,第二个参数是回调函数。这个回调函数会一次作为指定节点,以及指定节点的所有后代节点。

function printContent(){
    if(this.nodeValue){
        console.log(this.nodeValue);
    }
}
DOMComb(document.body,printContent);

Node.cloneNode()

Node.cloneNode方法用于克隆一个节点。它接受一个布尔值作为参数,表示是否同时克隆子节点,默认是false,即不可隆子节点。

var cloneUL = document.querySelector('ul').cloneNode(true);

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

可能一个节点之后,DOM树有可能出现两个相同的ID属性(即id = “xxx”)的html元素,这是应该修改其中一个HTML元素,这是应该修改其中一个HTML元素的ID属性。

Node.insertBefore()

insertBefore方法用于将某个节点插入当前节点内部的指定位置。它接受两个参数,第一个参数是所要插入的节点,第二个参数是当前节点内部的一个子节点,新的节点将插在这个节点的前面。该方法返回被插入的新节点。

var text1 = document.createTextNode('1');
var li = document.createElement('li');
li.appendChild(text1);

var ul = document.querySelector('ul');
ul.insertBefore(li,ul.firstChild);

上面代码使用当前节点的firstChild属性,在

    节点的最前面插入一个新建的
  • 节点,新节点变成第一个子节点。

parentElement.insertBefore(newElement,parentElement.firstChild);

上面代码中,如果当前节点没有任何子节点,parentElement.firstChild会返回null,则新节点会成为当前节点的唯一节点。

如果insertBefore方法的第二个参数为null,则新节点将插在当前节点内部的最后位置,即变成最后一个子节点。

注意,如果所要插入的节点是当前DOM现有的节点,则该节点将从原位置移除,插入新的位置。

由于不存在insertAfter方法,如果要插入当前某个子节点的子节点后面,可以用insertBefore方法结合nextSibling属性模拟。

parent.insertBefore(s1,s2.nextSibling);

上面代码中,parent是父节点,s1是一个全新的节点,s2是可以将s1节点,插在s2节点的后面,如果s2是当前节点的最后一个子节点,则s2.nextSibling返回null,这是s1节点会插在当前节点的最后,变成当前节点的最后一个子节点,等于紧跟在s2后面。

Node.removeChild()

Node.removeChild方法接受一个子节点作为参数,用于从当前节点移除该子节点。他返回被移除的子节点。

var divA = document.getElementById('divA');
divA.parentNode.removeChild(divA);

上面代码是如何移除一个指定节点。

注意,这个方法是在父节点上调用的,不是在被移除的节点上调用的。

下面是如何移除当前节点的所有子节点。

var element = document.getElementById('top');
while(element.firstChild){
    element.removeChild(element.firstChild);
}

它移除的节点依然存在于内存之中,但不在是DOM的一部分。所以一个节点移除以后,依然可以使用它,比如插入另一个节点下面。

Node.replaceChild()

Node.replaceChild方法用于将一个新的节点,替换当前节点的某一个子节点。他可以接受两个参数,第一个参数是用来替换的新节点,第二个参数将要替换走的子节点。他返回那个被替换走的那个节点。

replaceNode = parentNode.replaceChild(newChild,oldChild);

下面是一个例子。

var divA = document.getElementById('A');
var newSpan = document.createElement('span');
divA.parentNode.replaceChild(newSpan,div);

上面代码是如何替换指定节点。

Node.contains()

Node.contains方法可以接受一个节点作为参数,返回一个布尔值,表示参数节点是否为当前节点的后代节点。

document.body.contains(node);

上面代码检查某个节点,是否包含在当前文档之中。
注意,如果将当前节点传入contains方法,会返回true。虽然意义上说,一个节点不应该包含自身。

nodeA.contains(nodeA); // true

Node.compareDocumentPosition()

compareDocumentPosition方法的用法,与contains方法完全一致,返回一个7个比特位的二进制值,表示参数节点与当前节点的关系。

二进制值 数值 含义
000000 0 两个节点相同
000001 1 两个节点不在同一个文档
000010 2 参数节点在当前节点前面
000100 4 参数节点在当前节点后面
001000 8 参数节点包含当前节点
010000 16 当前节点包含参数节点
100000 32 浏览器私有用途
// HTML代码为
// 
//
// //
//
var div = document.getElementById('mydiv'); var input = document.getElementById('test'); div.compareDocumentPosition(input) // 20 input.compareDocumentPosition(div) / 10

上面代码中,节点div包含节点input,而且节点input在节点div的后面,所以第一个compareDocumentPosition方法返回20(二进制010100),第二个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.before = function(arg){
    return !!(this.compareDocumentPosition(arg) & 2);
}
nodeA.before(nodeB);

Node.isEqualNode()

isEqualNode方法返回一个布尔值,用于检查两个节点是否相等。所谓相等的节点,指的是两个节点的类型相同、属性相同、子节点相同。

var targetEI = document.getElementById('targetEI');
var firstDiv = document.getElementByTagName("div")[0];
targetEI.isEqualNode(firstDiv);

Node.normalize()

normalize方法用于清理当前节点内部的所有Text节点。他会去除空的文本节点,并且将比邻的文本节点合并成一个。

var wrapper = document.createElement("div");

wrapper.appendChild(document.createTextNode("Part 1 "));
wrapper.appendChild(document.createTextNode("Part 2 "));

wrapper.childNodes.length;
wrapper.normalize();
wrapper.childNodes.length; // 1

上面代码中使用normalize方法之前,wrapper节点有两个Text子节点。使用normalize方法之后,两个Text节点被合并成一个。

NodeList对象,HTMLCollection对象

节点都是单对象,有时需要一种数据结构,能够容纳对个节点。DOM提供两种结合对象,用于实现这种节点集合:NodeList和HTMLCollection。

这两个对象都是构造函数。

typeof NodeList // "function"
typeof HTMLCollection // "function"

但是,一般不把它当做函数使用,甚至都没有直接使用它们的场合。主要是许多DOM属性和方法,返回的结果是NodeList实例或HTMLCollection实例,所以一般只使用它们的实例。

NodeList对象

NodeList实例对象是一个类似数组的对象,它的成员是节点对象。Node.childNodes、document.querySelectorAll()返回的都是NodeList实例对象。

document.childNodes instanceof NodeList // true

NodeList实例对象可能是动态集合,也可能是静态集合。所谓动态及喝酒而是一个活的集合,DOM树删除或新增一个相关节点,都会反映在NodeList接口之中。Node.childNodes返回的,就是一个动态集合。

var parent = document.getElementById('parent');
parent.childNodes.length // 2
parent.appendChild(document.createElement('div'));
parent.childNodes.length // 3

上面代码中,parent.childNodes返回的是一个NodeList实例对象。当parent节点新增一个子节点以后,该对象的成员个数就增加1。

document.querySelectorAll方法返回的是一个静态集合。DOM内部的变化,并不会实时反映在该方法的返回结果之中。

NodeList接口实例对象提供length属性和数字索引,因此可以像数组那样,使用数字索引取出每个节点,但是他本身并不是数组,不能使用pop或push之类数组特有的方法。

// 数组的继承链
myArray --> Array.prototype --> Object.prototype --> null

// NodeList的继承链、
myNodeList --> NodeList.prototype --> Object.prototype --> null

从上面的继承链可以看到,NodeList实例对象并不是继承Array.prototype,因此不具有数组的方法。如果要在NodeList实例对象使用数组方法,可以将NodeList实例转为正真的数组。

var div_list = document.querySelector('div');
var div_array = Array.prototype.slice.call(div_list);

注意,采用上面的方法将NodeList实例转换为真正的数组以后,div_array就是一个静态的集合了。不能再动态的反映DOM的变化。

另一种方法是通过call方法,间接在NodeList实例上使用数组方法。

var forEach = Array.prototype.forEach;
forEach.call(element.childNodes,function(child){
    child.parentNode.style.color = '#0F0';
});

上面代码让数组的forEach方法在NodeList实例对象上调用。

遍历NodeList实例对象的首选方法,是使用for循环。

for(var i = 0;i < myNodeList.length;++i){
    var item = myNodeList[i];
}

不要使用for…in循环去遍历NodeList实例对象,因为for…in循环会将非数字索引的length属性和下面要讲到的item方法,也遍历进去,而且不保证各个成员遍历的顺序。

ES6新增for…of循环,也可以正确遍历NodeList实例对象。

var list = document.querySelectorAll('input[type=checkbox]');
for(var item of list){
    item.checked = true;
}

NodeList实例对象的item方法,接受一个数字索引作为参数,返回该索引对应的成员。如果取不到成员或者成员不合法,则返回null。

nodeItem = nodeList.item(index);

// 实例
var divs = document.getElementByTagName("div");
var secondDiv = divs.item(1);

上面代码中,由于数字索引从零开始计数,所以出去第二个成员,要使用数字索引1。

所有类似于数组的对象,都可以使用方括号运算符取出成员,多以一般情况下,都是使用下面的写法,而不使用item方法。

nodeItem = nodeList[index];

HTMLCollection对象

HTMLCollection实例对象与NodeList实例对象类似,也是节点的集合,返回一个类似数组的对象。document.links、document.forms、document.images等属性,返回都是HTMLColletion实例对象。

HTMLCollection与NodeList对象的区别有以下几点:

  1. HTMLCollection实例对象的成员只能是Element节点,NodeList实例对象的成员可以包含其他节点。
  2. HTMLCollection实例对象都是动态集合,节点的变化会实时反映在集合中。NodeList实例对象可以是动态集合。
  3. HTMLCollection实例对象可以用id属性或者name属性引用节点元素,NodeList只能使用数字索引引用。
    HTMLCollection实例的item方法,可以根据成员的位置参数(从0开始),返回该成员。如果取不到成员挥着数字索引不合法,则返回null。
var c = document.images;
var img1 = c.item(1);

// 等价下面写法
var img1 = c[0];

HTMLCollection实例的nameItem方法蜂聚成员的ID属性或者name属性,返回该成员。如果没有对应的成员,则返回null。这个方法是NodeLsit实例不具有的。

// HTML代码为
// 
var elem = document.forms.namedItem('myForm'); // 等价于下面的写法 var elem = document.forms['myForm'];

由于item方法和nameItem方法,都可以用方括号运算符代替,所以建议一律使用方括号运算符。

ParentNode接口,ChildNode接口

不同的节点除了继承Node接口以外,还会继承其他接口。ParentNode接口用于获取当前节点的Element子节点,ChildNode接口用于处理当前节点的子节点。(包含但不限于Element子节点)

ParentNode接口

ParentNode接口用于获取Element子节点。Element节点、Document节点和DocumentFragment节点,部署了ParentNode接口。凡是这三类节点,都具有一下四个属性,用于获取Element子节点。

children

children属性返回一个动态的HTMLCollection集合,由当前节点的所有Element子节点组成。

下面代码遍历指定节点的所有Element子节点。

if(el.children.length){
    for(var i=0;i...
    }
}

firstElementChild

firstElementChild属性返回当前节点的第一个Element子节点,如果不存在任何Element子节点,则返回null。

document.firstElementChild.nodeName

上面代码中,document节点的第一个Element子节点是。

lastElementChild

lastElementChild属性返回当前节点的最后一个Element子节点,如果不存在任何Element子节点,则返回null。

document.lastElementChild.nodeName
// "HTML"

childElementCount

childElementCount属性返回当前节点的所有Element子节点数目。

ChildNode接口

ChildNode接口用于处理子节点(包括但不限于Element子节点)。Element节点、DocumentType节点和CharacterData接口。凡是这三类节点(接口),都可以使用下面三种方法。

remove

remove方法用于移除当前节点。

el.remove()

上面方法在DOM中移除了el节点。注意,调用这个方法的节点,是被移除节点的本身,而不是它的父节点。

before()

before方法用于在当前节点的前面,插入同一级节点。如果参数是节点对象,插入DOM的就是该节点对象;如果是文本,插入DOM的就是参数对应的文本节点。

after()

after方法用于在当前节点的后面,插入同一级节点。如果参数是节点对象,插入DOM的就是该节点对象;如果参数是文本,插入DOM的就是参数对应的文本节点。

replaceWith

replaceWith方法使用参数指定的节点,替换当前节点。如果参数世界店对象,替换当前节点的计时该节点对象;如果参数是文本,替换当前节点就是参数对应的文本节点。

你可能感兴趣的:(js)