JavaScript语言基础(九)DOM(文档对象模型)

205 阅读12分钟

是HTML和XML文档的编程接口,表示由多层节点构成的文档,通过它可以添加、删除和修改页面的各个部分。

节点层级

任何HTML或XML文档都可以用DOM表示为一个由节点构成的层级结构。

Node类型

DOM Level1 描述了Node接口,是所有DOM节点类型都必须实现的, 在JS中被实现为Node类型,在除了IE以外的所有浏览器中都可直接访问这个类型。 在JS中,所有节点类型都继承Node类型,因此所有类型都共享相同的基本属性和方法。

每个节点都有nodeType属性,表示该节点的类型。节点类型由定义在Node类型上的12个数值常量表示:

  • 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)
  • Node.DOCUMENT_TYPE_NODE(10)
  • Node.DOCUMENT_FRAGMENT_NODE(11)
  • Node.NOTATION_NODE(12) 节点类型可通过这些常量比较来确定。
  1. nodeName与nodeValue 保存着有关节点的信息。在使用这两个属性前,最好先检测节点类型。对元素而言,nodeName始终等于元素的标签名,而nodeValue则始终为null。

  2. 节点关系 每个节点都有一个childNodes属性,其中包含一个NodeList的实例,NodeList是一个类数组对象,并不是Array的实例,但可以使用中括号访问它的值,而且也有length属性,表示那一时刻NodeList中节点的数量,通常说NodeList是实时的活动对象

把NodeList对象转换为数组,使用ES6的Array.from()静态方法:

let arrayOfNodes = Array.from(someNode.childNodes);

每个节点都有一个parentNode属性,指向其DOM树中的父元素。

在ChildNode中, 第一个子节点——firstChild,最后一个子节点——lastChild;

前面的兄弟节点——previousSibling,后面的兄弟节点——nextSibling;

hasChildNodes()用于判断节点是否子节点,返回true则说明节点有一个或多个子节点。

ownerDocument属性是一个指向代表整个文档的文档节点的指针。

  1. 操纵节点 appendChild()——用于在childNodes列表末尾添加节点,返回新添加的节点。

insertBefore()——插入节点到childNodes中的特定位置,接收两个参数:要插入的节点和参照节点,要插入的节点会变成参照节点的前一个同胞节点,并被返回。

replaceChild()——用插入节点来替换某个节点,接收两个参数:要插入的节点和要替换的节点,要替换的节点会被返回并从文档树中完全移除,要插入的节点会取而代之。

removeChild——移除某个节点,接收一个参数:要移除的节点,被移除的节点会被返回。

以上四个方法,使用之前必须先取得父节点

cloneNode()——会返回与调用它的节点一模一样的节点,接收一个布尔值参数,表示是否深复制(即复制节点及其整个DOM树),若为false,则只会复制调用该方法的节点。

normalize()——处理文档子树中的文本节点,在节点上调用此方法会检测这个节点的所有后代。若发现空文本节点,则将其删除;若两个同胞节点是相邻的,则将其合并为一个文本节点。

Document类型

是JS中表示文档节点的类型。在浏览器中,文档对象document是HTMLDocument的实例(HTMLDocument继承Document),表示整个HTML页面。

document是window对象的属性,因此是全局对象。最常用的是通过HTMLDocument的实例取得document对象

子节点可以是DocumentType(最多一个)、Element(最多一个)、ProcessingInstruction或Comment类型。

  1. 文档子节点 访问子节点的快捷方式:

1)documentElement属性,始终指向HTML页面中的/元素。

2)body属性,直接指向/元素。

所有主流浏览器都支持document.documentElement和document.body。

Document类型另一种可能的子节点是DocumentType,<!doctype>标签是文档中独立的部分,可以通过doctype属性来获取其信息(在浏览器中是document.doctype)。

appendChild()、removeChild()和replaceChild()不会用在document对象,因为文档类型(如果存在)是只读的,而且只能有一个Element类型的子节点(即<html>,已经存在了)。

  1. 文档信息 title属性——包含<title>元素中的文本,修改title属性不会改变<title>元素。

URL属性——包含当前页面的完整URL(地址栏中的URL)。

domian属性——包含页面的域名。

referrer属性——包含链接到当前页面的那个页面的URL,如果没有当前页面来源,则referrer属性包含空字符串。

只有domain属性是可以设置的,设置的值也是有限制的,不能给这个属性设置URL中不包含的值,如URL包含子域名如p2p.wrox.com,则可设置domain为“wrox.com”。

当页面中包含来自不同子域的窗格(<frame>)或内嵌窗格(<iframe>)时,不同子域的页面无法通过JS通信,可以在每个页面上设置document.domain设置为相同的值,这些页面就可以访问对方的JS对象了。

一旦放松就不能再收紧,document.domain设置为"wrox.com"之后,就不能再设置回“p2p.wrox.com”。

  1. 定位元素 getElementById()——根据ID获取元素,接收一个参数:要获取元素的ID,ID必须与页面中的元素的id属性值完全匹配,包括大小写。若找到多个相同ID的元素,则返回在文档中出现的第一个元素。

getElementsByTagName()——根据标签名获取元素,接收一个参数:要获取元素的标签名,返回包含0个或多个元素的NodeList。 在HTML文档中,返回一个HTMLCollection对象, 与NodeLsit很相似,也可以使用中括号或item()方法从HTMLCollection取得特定的元素,也可使用length属性获取元素的数量。还有一个额外的方法namedItem(),可通过标签的name属性取得某一项的引用。 可传入*,取得文档中的所有元素。

getElementsByName()——返回具有给定name属性的所有元素,也返回HTMLCollection,若使用namedItem(),只会取得第一项(因为所有项的name属性都一样),常用于单选按钮。

  1. 特殊集合 都是HTMLCollection的实例,与所有HTMLCollection对象一样,其内容也会实时更新以符合当前文档内容。

document.anchors——包含文档中所有带name属性的<a>元素。

document.applets【已废弃】——包含文档中所有<applet>元素,但是<applet>元素不建议使用,此集合已经废弃。

document.forms——包含文档中所有<from>元素(与document.getElementsByTagName("from")返回的结果相同)。

document.images——包含文档中所有<img>元素(与document.getElementsByTagName("img")返回的结果相同)。

document.links——包含文档中所有带href属性的<a>元素。

5.文档写入

write()——简单地写入文本.

writeln()——写入文本,还会在字符串末尾追加一个换行符(\n)。

以上两个方法可以用在页面加载期间向页面中动态添加内容,经常用于动态包含外部资源,如JS文件,引用时注意

document.write("<script type =\ "text/javascript" src = "file.js">"+"</script>"); // 最后的"</script>"。

open()——打开网页输出流。

close()——关闭网页输出流。

Element类型

表示XML或HTML元素,对外暴露出访问元素标签名、子节点和属性的能力。

可以通过nodeName或tagName属性来获取元素的标签名,在HTML中,元素标签名始终以全大写表示; 在XML(包括XHTML)中,标签名始终与源代码中的大小写一致。若不确定脚本在哪个文档中运行,最好将标签名转换为小写形式,以便于比较:

element.tagName.toLowerCase()

  1. HTML元素 id,元素在文档中的唯一标识符。

title,包含元素的额外信息,通常以提示条形式展示。

lang,元素内容的语言代码(很少用)。

dir,语言的书写方向,"ltr"—从左到右,"rtl"—从右到左,很少用。

className,相当于class属性,用于指定元素的CSS类。

都可以用来获取对应的属性值,也可用来修改相应的值。

  1. 取得属性 getAttribute()、setAttribute()和removeAttribute(),用于操纵属性,包括在HTMLElement类型上定义的属性。
let div = document.getElementById("myDiv");
alert(div.getAttribute("class"));		// "bd"
// 这里传入"class"而非"className",是因为className是作为对象属性时才这样拼写。

getAttribute()也能取得不是HTML语言正式属性的自定义属性的值:

// 拥有一个元素
<div id = "myDiv" my_special = "hello!"></div>
// 取得自定义属性my_special的值
let value = div.getAttribute("my_special");

属性名不区分大小写。 另外,根据HTML5规范的要求,自定义属性么应该前缀data-以方便验证。

通过DOM对象访问的属性中,有两个返回的值跟使用getAttribute()取得的值不一样,分别是style属性,与事件处理程序(即事件属性)。

在进行DOM编程时通常会放弃使用getAttribute()而只使用对象属性。getAttribute()主要用于取得自定义属性的值。

  1. 设置属性 setAttribute(),接收两个参数:要设置的属性名和属性的值。 若属性已存在,则会以指定的值替换原来的值。适用于HTML属性,也适用于自定义属性。 使用此方法设置的属性名会规范为小写形式

removeAttribute()用于从元素中删除属性,用得不多,但在序列化DOM元素时可以通过它控制要包含的属性。

  1. attributes属性 包含一个NamedNodeMap实例,是一个类似NodeList的"实时"集合。元素的每个属性都表示为一个Attr节点,并保存在这个NamedNodeMap对象中。

NameNodeMap对象包含下列方法:

  • getNamedItem(name)—返回nodeName属性等于name的节点;

  • setNamedItem(node)—向列表中添加node节点,以其nodeName为索引;

  • removeNamedItem(name)—删除nodeName属性等于name的节点;

  • item(pos)—返回索引位置pos处的节点。

attributes属性中的每个节点nodeName是对应属性的名字,nodeValue是属性的值。

let id = element.attributes.getNamedItem("id").nodeValue;
// 使用中括号访问属性的简写形式
let id = element.attributes["id"].nodeValue;

setNameItem()很少使用,接受一个属性节点,然后给元素添加一个新属性。

removeNamedItem()与元素上的removeAttribute()方法相似,也是删除指定名字的属性, 唯一不同之处是,removeNamedItem()返回表示被删除属性的Attr节点。

attributes属性最有用的场景是需要迭代元素上所有属性的时候,这时候往往是要把DOM结构序列化为XML或HTML字符串。

  1. 创建元素 可以使用document.createElement()方法创建新元素,接收一个参数,即要创建元素的标签名。 使用此方法创建的新元素,同时也会将其ownerDocument属性设置为document,此时,可以再为其添加属性、添加更多子元素。

Text类型

Text节点由Text类型表示,包含按字面解释的纯文本,也可能包含转义后的HTML字符,但不包含HTML代码。 Text节点中包含的文本可以通过nodeValue属性访问,也可通过data属性访问,两个属性包含相同的值,修改其中之一的值,也会在另一个属性反映出来。

操作文本的方法:

  • appendData(text)——向节点末尾添加文本text。
  • deleteData(offset,count)——从位置offset开始删除count个字符。
  • insertData(offset,text)——在位置offset插入text。
  • replaceData(offset,count,text)——用text替换从offset到offset+count的文本。
  • splitText(offset)——在位置offset将当前文本节点拆分为两个文本节点。
  • substringData(offset,count)——提取从位置offset到offset+count的文本。 还可以通过length属性获取文本节点中包含的字符数量。 默认情况下,包含文本内容的每个元素最多只能有一个文本节点。

<div>Hello!!</div> // 有内容,因此有一个文本节点

修改文本节点时,大于号、小于号或引号都会被转义。

 div.firstChild.nodeValue = "some \<strong>other\</strong> message";
 // 输出为"some \&lt;strong\$gt;other\&lt;/strong&\gt; message"
  1. 创建文本节点 document.createTextNode(),接收一个参数:要插入节点的文本。 创建新文本节点后,其ownerDocument属性会被设置为document。 一般来说,一个元素只包含一个文本子节点。

  2. 规范化文本节点 使用Node类型中定义的normalize(),所有类型的节点上都有这个方法。在包含两个或多个相邻文本节点的父节点上调用normalize()时, 所有同胞文本节点会被合并为一个文本节点。

  3. 拆分文本节点 Text类型定义了一个与normalize()相反的方法——splitText(),可以在指定的偏移位置拆分nodeValue,将一个文本节点拆分为两个文本节点。 拆分文本节点最常用于从文本节点中提取数据的DOM解析技术。

Comment类型

DOM中的注释通过Comment类型表示。

与Text类型继承同一个基类(CharacterData),因此拥有除splitText()之外Text节点所有的字符串操作方法。

与Textl类型相似,注释内容可以通过nodeValue或data属性获得。

注释节点可以作为父节点的子节点来访问。

可以使用document.createComment()方法创建注释节点,参数为注释文本。

CDATASection类型

表示XML中特有的CDATA区块。此类型继承Text类型,因此拥有除splitText()之外Text节点所有的字符串操作方法。

CDATA区块只在XML文档中有效。在真正的XML文档中,可以使用document.createCDataSection()并传入节点内容来创建CDATA区块。

DocumentType类型

此类型的节点包含文档的文档类型信息。DocumentType对象保存在document.doctype属性中。

DocumentType对象的3个属性:name、entities和notations,name是文档类型的名称,entities是这个文档类型描述的实体的NamedNodeMap,

而notations是这个文档类型描述的表示法的NamedNodeMap。因为浏览器中的文档通常是HTML或XHTML文档类型,所以entities和notations列表为空。 只有name属性是有用的。

DocumentFragment类型

是唯一一个在标记中没有对应表示的类型,能够包含和操作节点,却没有完整文档那样额外的消耗。

不能直接把文档片段添加到文档,可以使用document.createDocumentFragment()创建文档片段。

Attr类型

元素数据在DOM中通过Attr类型表示。Attr类型构造函数和原型在所有浏览器中都可以直接访问。

属性是存在于元素attributes属性中的节点,属性节点尽管是节点,但不被认为是DOM文档树的一部分。

Attr节点很少直接引用,通常使用getAttribute()、setAttribute()和removeAttribute()操作属性

Attr对象上有3个属性:name、value和specified,name和value分别与nodeName、nodeValue一样,而specified是一个布尔值,表示属性使用的是默认值还是被指定的值。 可以使用document.createAttribute()方法创建新的Attr节点,参数为属性名。

DOM编程

动态脚本

<script>元素用于向网页中插入JavaScript代码,可以是src属性包含的外部文件,也可以是作为该元素内容的源代码。 动态脚本就是在页面加载时不存在,之后又通过DOM包含的脚本。

通过<script>动态为网页添加脚本的两种方式:

  1. 引入外部文件

<script src = "foo.js"></script>

可以通过DOM编程创建这个节点,动态加载外部文件:

let script = document.createElement("script");
script.src = "foo.js";
document.body.appendChild(script);	

在最后一行,把<script>元素添加到页面之前,是不会开始下载外部文件的。<script>也可添加到<head>元素上

  1. 直接插入源代码
 <script>
	function sayHi(){
		alert("hi");
	}
</script>

可以通过DOM编程实现以下逻辑:

let script = document.createElement("script");
script.appendChild(document.createTextNode("function sayHi(){ alert("hi"); }"));
document.body.appendChild(script);	

以上代码可以在Firefox、Safari、Chrome和Opera中运行。 通过innerHTML属性创建的<script>元素永远不会执行。

动态样式

CSS样式在HTML中可通过两个元素加载:

<link>——用于包含CSS外部文件。

<style>——用于添加嵌入样式。

与动态脚本类似,动态样式也是在页面加载时不存在,之后才添加到页面中的。

<link rel = "stylesheet" type = "text/css" href = "style.css">

通过DOM编程创建出此元素:

let link = document.createElement("link");
link.rel = "stylesheet";
link.type = "text/css";
link.href = "style.css";
let head = document.getElementByTagName("head")[0];
head.appendChild(link);

以上代码可以在Firefox、Safari、Chrome和Opera中运行。 <style>的DOM编程,与<link>类似,

但IE对<style>节点会加以限制,不允许访问其子节点,这一点与对<script>元素的限制一样。 解决方案:访问元素的styleSheet属性,这个属性有一个cssText属性,然后给这个属性添加CSS代码。

style.styleSheet.cssText = "body{background-color : red}" 小心使用styleSheet.cssText,若重用同一个<style>元素并设置该属性超过一次,可能导致浏览器崩溃。

使用NodeList

理解NodeList对象和相关的NamedNodeMap、HTMLCollection,三个集合类型都是“实时的”。 NodeList是基于DOM文档的实时查询。

任何时候要迭代NodeList,最好再初始化一个变量保存当时查询时的长度:

let divs = document.getElementsByTagName("div");
for(let i = 0, len = divs.length; i < len; ++i){
	let div = document.createElement("div");
	document.body.appendChild(div);
}

若不想初始化一个变量,也可以反向迭代集合。 最好限制操作NodeList的次数,因为每次查询都会搜索整个文档,所以最好把查询到的NodeList缓存起来。

MutationObserver接口

可以在DOM被修改时异步执行回调。使用MutationObserver可以观察整个文档、DOM树的一部分,或某个元素。 此外还可以观察元素属性、子节点、文本,或者前三者任意组合的变化。

基本用法

实例需要通过调用MutationObserver构造函数并传入一个回调函数来创建:

let observer = new MutationObserver(() => console.log('DOM was mutated!'));

  1. observer() 把observer实例与DOM关联起来,使用此方法。接受两个必需的参数:要观察其变化的DOM节点,以及一个MutationObserverInit对象。

MutationObserverInit对象用于控制观察哪方面的变化,是一个键/值对形式的字典。

let observer = new MutationObserver(() => console.log('<body> attributes changed!'));
observer.observe(document.body, { attributes : true });

document.body.className = 'foo';
console.log('change body class');

// change body class
// <body> attributes changed!

实例发现body元素上的任何属性发生变化,就会异步执行注册的回调函数

  1. 回调与MutationRecord 每个回调都会收到一个MutationRecord实例的数组,MutationRecord实例包含的信息包括发生的变化,以及DOM的哪部分受到影响。
let observer = new MutationObserver ((mutationRecords) => console.log (mutationRecords)) ;
observer.observe (document.body, { attributes: true }) ;
document.body.setAttribute(' foo', 'bar') ;
// [
//	{
//	addedNodes: NodeList [] ,
//	attributeName: "foo",
//	attributeNamespace: null,
//	nextsibling: null,
//	nextSibling : null,
//	previoussibling: null,
//	removedNodes: NodeList [] ,
//	target: body
//	type: "attributes”
// 	}
// ]

MutationRecord实例的属性

图片.png

图片.png 传给回调函数的第二个参数是观察变化的MutationObserver的实例:

let observer = new MutationObserver ((mutationRecords, mutationObserver) => console.log (mutationRecords, mutationObserver)) ;
observer.observe (document.body, { attributes: true }) ;
document.body.className = 'foo';
// [MutationRecord], MutationObserver
  1. disconnect()方法 用于要提前终止执行回调。默认情况下,只要被观察的元素不被垃圾回收, MutationObserver的回调就会响应DOM变化事件,从而被执行。若想提前终止执行回调,可以调用此方法。

同步调用disconnect()之后,不仅会停止此后变化事件的回调,也会抛弃已经加入任务队列要异步执行的回调。

let observer = new MutationObserver (() => console.log ('<body> attributes changed!')) ;
observer.observe (document.body, { attributes: true }) ;
document.body.className = 'foo';

observer.disconnect();
document.body.className = 'bar';
// (没有日志输出)

要想让已经加入任务队列的回调执行,可以使用setTimeout()让已经入列的回调执行完毕再调用disconnect()。

document.body.className = 'foo';
setTimeout(() => {
	observer.disconnect();
	document.body.className = 'bar';	// 不会触发变化事件
}, 0);
// <body> attributes changed!
  1. 复用MutationObserver 多次调用observer()方法,可以复用一个MutationObserver对象观察多个不同的目标节点。

此时,MutationRecord的target属性可以标识发生变化事件的目标节点。

let observer = new MutationObserver ((mutationRecords) => console.log (mutationRecords.map((x) => x.target) ;

// 观察两个子节点
observer.observe(childA, { attributes : true });
observer.observe(childB, { attributes : true });

childA.setAttributes('foo', 'bar');
childB.setAttributes('foo', 'bar');
// [<div>,<span>]

disconnect()方法是一个“一刀切”的方案,调用它会停止观察所有目标。

  1. 重用MutationObserver 调用disconnect(),并不会结束MutationObserver的生命,还可以重新使用这个观察者,再将它关联到新的目标节点。
let observer = new MutationObserver (() => console.log ('<body> attributes changed!')) ;
observer.observe (document.body, { attributes: true }) ;
document.body.setAttributes('foo', 'bar');		// 触发变化事件

setTimeout(() => {
	observer.disconnect();			// 中断关联
	document.body.setAttributes('bar', 'baz');	// 不会触发变化事件
}, 0);

setTimeout(() => {
	observer.observe(document.body, { attributes : true });	// 重新关联
	document.body.setAttributes('baz', 'qux');	// 触发变化事件
}, 0);

// <body> attributes changed!
// <body> attributes changed!

MutationObserverInit与观察范围

MutationObserverInit对象用于控制对目标节点的观察范围,可以观察的事件包括属性变化、文本变化和子节点变化。

MutationObserverInit对象的属性

图片.png

图片.png 在调用observe()时,MutationObserverInit对象中的attribute、characterData和childList属性,必须至少有一项为true (无论是直接设置这几个属性,还是通过设置attributeOldValue等属性间接导致它们的值转换为true)。否则会抛出错误,因为没有任何变化事件可能触发回调。

  1. 观察属性 MutationObserver可以观察节点属性的添加、移除和修改。

要为属性变化注册回调,需要在MutationObserverInit对象中将attributes属性设置为true。

observer.observe(document.body, { attributes : true }); 如果想观察某个或某几个属性,可以使用attributeFilter属性来设置白名单,即一个属性名字符串数组:

observer.observe(document.body, { attributeFilter : ['foo'] });
// 添加白名单属性
document.body.setAttributes('foo', 'bar');
// 添加被排除的属性
document.body.setAttributes('baz', 'qux');
// 只有foo属性的变化被记录了
// [MutationRecord]

如果想在变化记录中保存属性原来的值,可以将attributeOldValue属性设置为true。

  1. 观察字符数据 MutationObserver可以观察文本节点(如Text、Comment或ProcessingInstruction节点)中字符的添加、删除和修改。

要为字符数据注册回调,需要在MutationObserverInit对象中将characterData属性设置为true。

如果想在变化记录中保存原来的字符数据,可以将characterDataOldValue属性设置为true。

  1. 观察子节点 MutationObserver可以观察目标节点子节点的添加和移除。

要观察子节点,需要在MutationObserverInit对象中将childList属性设置为true。

对子节点重新排序(尽管调用一个方法即可实现)会报告两次变化事件,因为从技术上会涉及先移除和再添加

  1. 观察子树 默认情况下,MutationObserver将观察的范围限定为一个元素及其子节点的变化。

可以把观察的范围扩展到这元素的子树(所有后代节点),需要在MutationObserverInit对象中将subtree属性设置为true。

被观察子树中的节点,被移出子树之后仍然能够触发变化事件

异步回调与记录队列

为了在大量变化事件发生时不影响性能,每次变化的信息(由观察者实例决定)会保存在MutationRecord实例中,然后添加到记录队列 。 这个队列对每个MutationObserver实例都是唯一的,是所有DOM变化事件的有序列表。

调用MutationObserver实例的takeRecords()方法,可以清空记录队列,取出并返回其中的所有MutationRecord实例。 适用于在希望断开与观察目标的联系,但又希望处理由于调用disconnect()而被抛弃的记录队列中的MutationRecord实例。

性能

MutationObserver的引用

MutationObserver拥有对观察的目标节点的弱引用,不会妨碍垃圾回收程序回收目标节点。

然而,目标节点拥有对MutationObserver的强引用,若目标节点从DOM中被移除,随后被垃圾回收,则关联的MutationObserver也会被垃圾回收。