DOM模型(一)—— DOM模型概述

一、基本概念


1.1、DOM

DOM是JS操作网页的接口,全称为“文档对象模型”(Document Object Model)。它的作用是将网页转为一个JS对象,从而可以用脚本进行各种操作(比如增删内容)。

文档对象模型(DOM)是网页的编程接口。它给文档(结构树)提供了一个结构化的表述并且定义了一种方式——程序可以对结构树进行访问,以改变文档的结构,样式和内容。

DOM提供了一种表述形式将文档作为一个结构化的节点组以及包含属性和方法的对象。从本质上说,它将web页面和脚本或编程语言连接起来了。

要改变页面的某个东西,JS就需要获得对网页中所有元素进行访问的入口。这个入口,连同对HTML元素进行添加、移动、改变或移除的方法和属性,都是通过DOM来获得的。

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

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

1.2、节点

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

节点的类型有七种。

  • Document:整个文档树的顶层节点,也是访问的入口。
  • DocumentType:doctype标签(比如<!DOCTYPE html>)。
  • Element:网页的各种HTML标签(比如<body>、<a>等)。
  • Attribute:网页元素的属性(比如class="right")。
  • Text:标签之间或标签包含的文本。
  • Comment:注释。
  • DocumentFragment:文档的片段。

这七种节点都属于浏览器原生提供的节点对象(下面要讲的Node对象)的派生对象,具有一些共同的属性和方法。

1.3、节点树

一个文档的所有节点,按照所在的层级,可以抽象成一种树状结构。这种树状结构就是DOM。

DOM

最顶层的节点就是document节点,它代表了整个文档。文档里面最高的HTML标签,一般是<html>,它构成树结构的根节点(root node),其他HTML标签节点都是它的下级。

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

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

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

二、与节点本身特征相关的属性


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

以下属性与节点对象本身的特征相关。这些属性是实例对象(也就是节点对象)继承自Node.prototype对象的。

2.1、Node.nodeName、Node.nodeType

nodeName属性返回节点的名称,nodeType属性返回节点类型的常数值。

七种类型节点

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

验证

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

document.querySelector('p').nodeType === 1
// true

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

上面两种写法等价。

验证

2.2、Node.nodeValue

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

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

2.3、Node.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节点,该属性的值与nodeValue属性相同。对于其他类型的节点,该属性会将每一个子节点的内容连接在一起返回,但是不包括Comment节点。如果一个节点没有子节点,则返回空字符串。

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

2.4、Node.baseURI

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

不同节点都可以调用这个属性(比如document.baseURIelement.baseURI),通常它们的值是相同的。

该属性的值一般由当前网址的URL(即window.location属性)决定,但是可以使用HTML的<base>标签,改变该属性的值。

三、返回当前节点相关节点的属性


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

3.1、Node.ownerDocument

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

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

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

验证

3.2、Node.nextSibling

nextSibling属性返回紧跟在当前节点后面的第一个同级节点。如果当前节点后面没有同级节点,则返回null

注意,该属性还包括文本节点和注释节点。因此如果当前节点后面有空格,该属性会返回一个文本节点,内容为空格。

3.3、Node.previousSibling

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

// html代码如下
// <a><b1 id="b1"/><b2 id="b2"/></a>

document.getElementById("b1").previousSibling // null
document.getElementById("b2").previousSibling.id // "b1"

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

3.4、Node.parentNode

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

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

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

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

3.5、Node.parentElement

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

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

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

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

3.6、Node.childNodes

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

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

3.7、Node.firstChild、Node.lastChild

firstChild属性返回当前节点的第一个子节点,如果当前节点没有子节点,则返回null

//HTML
<p id="para-01"><span>First span</span></p>

//JS
document.getElementById('para-01').firstChild.nodeName
//"span"
结果

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

//HTML
<p id="para-01">
    <span>First span</span>
</p>

//JS
document.getElementById('para-01').firstChild.nodeName
//"#text"

上面的代码中,p元素与span元素之间有空白字符,这导致firstChild返回的是文本节点。

结果

lastChild属性返回当前节点的最后一个子节点,如果当前节点没有子节点,则返回null

四、节点对象的方法


4.1、Node.appendChild()

appendChild方法接受一个节点对象作为参数,将其作为最后一个节点,插入当前节点。

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

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

4.2、Node.hasChildNodes()

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

var foo = document.getElementById("foo");

if (foo.hasChildNodes()) {
    foo.removeChild(foo.childNodes[0]);
}

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

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

4.3、Node.cloneNode()

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

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

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

克隆一个节点之后,DOM树有可能出现两个有相同ID属性的HTML元素,这时应该修改其中一个HTML元素的ID属性。

4.4、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属性,在<ul>节点的最前面插入一个新建的<li>节点,新节点变成第一个子节点。

parentElement.insertBefore(newElement, parentElement.firstChild);

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

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

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

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

parentDiv.insertBefore(s1, s2.nextSibling);

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

4.5、Node.removeChild()

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

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

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

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

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

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

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

4.6、Node.replaceChild()

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

replacedNode = parentNode.replaceChild(newChild, oldChild);

下面是一个例子。

var divA = document.getElementById('A');
var newSpan = document.createElement('span');
newSpan.textContent = 'Hello World!';
divA.parentNode.replaceChild(newSpan, divA);

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

4.7、Node.contains()

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

document.body.contains(node)

上面代码检查某个节点,是否包含在当前文档之中。

注意,如果将当前节点传入contains方法,会返回true。虽然从意义上说,一个节点不应该包含自身。

nodeA.contains(nodeA) // true

4.8、Node.compareDocumentPosition()

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

4.9、Node.isEqualNode()

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

var targetEl = document.getElementById("targetEl");
var firstDiv = document.getElementsByTagName("div")[0];

targetEl.isEqualNode(firstDiv)

4.10、Node.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节点有两个Text子节点。使用nomalize方法之后,两个Text子节点被合成一个。

该方法是Text.splitText的逆方法。

五、NodeList对象,HTMLCollection对象


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

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

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

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

5.1、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属性和数字索引,因此可以像数组那样,使用数字索引取出每个节点,但是它们本身并不是数组,不能使用数组特有的方法。

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

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

5.2、HTMLCollection实例对象

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

HTMLCollectionNodeList的区别有以下几点。
(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[1];

HTMLCollection实例的namedItem方法根据成员的ID属性或name属性,返回该成员。如果没有对应的成员,则返回null。这个方法是NodeList实例不具有的。

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

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

六、ParentNode接口,ChildNode接口


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

6.1、ParentNode接口

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

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

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

document.firstElementChild.nodeName
// "HTML"

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

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

document.lastElementChild.nodeName
// "HTML"

上面代码中,document节点的最后一个Element子节点是<HTML>

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

6.2、ChildNode接口

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

(1)、remove()
remove方法用于移除当前节点。

el.remove()

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

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

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

(4)、replaceWith()
replaceWith方法使用参数指定的节点,替换当前节点。如果参数是节点对象。替换当前节点的就是该节点对象;如果参数是文本,替换当前节点的就是参数对应的文本节点。

(本系列下一节为 — Document节点)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 141,232评论 1 296
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 60,610评论 1 254
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 93,049评论 0 210
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 40,691评论 0 171
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 48,375评论 1 250
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 38,571评论 1 169
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 30,262评论 2 266
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 29,036评论 0 163
  • 想象着我的养父在大火中拼命挣扎,窒息,最后皮肤化为焦炭。我心中就已经是抑制不住地欢快,这就叫做以其人之道,还治其人...
    爱写小说的胖达阅读 28,781评论 6 225
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 32,394评论 0 211
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,154评论 2 212
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 30,470评论 1 222
  • 白月光回国,霸总把我这个替身辞退。还一脸阴沉的警告我。[不要出现在思思面前, 不然我有一百种方法让你生不如死。]我...
    爱写小说的胖达阅读 24,151评论 0 31
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 27,023评论 2 210
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 31,376评论 3 201
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,579评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 25,926评论 0 163
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 33,342评论 2 227
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 33,466评论 2 229

推荐阅读更多精彩内容