第16章 DOM2&3
第16章 DOM2&3
DOM1专注于定义HTML和XML文档的基础结构。DOM2和 3 引入了更多的交互性并支持更高级的XML特性。DOM2和 3 实际上由几个模块组成,尽管这些模块相关,但它们描述了DOM的非常具体的子集。这些模块是:
DOM Core 以DOM1 Core为基础,添加方法和属性到节点。
DOM Views 根据样式信息为文档定义不同的视图。
DOM Events 说明如何使用事件将交互性与DOM文档绑定。
DOM Style 定义如何以编程方式访问和更改CSS样式信息。
DOM Traversal and Range 引入了用于遍历DOM文档并选择文档特定部分的新接口。
DOM HTML 以level1 HTML为基础,添加属性、方法和新接口。
DOM Mutation Observers 允许在DOM更改时定义回调。 DOM4规范中定义了“突变观察者”以替换“突
变事件”。
DOM的变化
DOM2和3 Core的目的是扩展DOM API以涵盖XML的所有要求,并提供更好的错误处理和功能检测。在大
多数情况下,这意味着支持XML命名空间的概念。 DOM2 Core没有引入任何新类型;它只是增加了DOM1中定义
的类型,以包括新的方法和属性。 DOM3 Core进一步增强了现有类型并引入了几个新类型。
XML命名空间
XML命名空间允许将来自不同的基于XML的语言的元素混合到一个格式正确的文档中,而不必担心元素命名冲突。从技术上讲,HTML不支持XML命名空间,但XHTML支持。因此,本节中的示例在XHTML中。
命名空间是使用xmlns特性指定的 。XHTML的命名空间是http:// http://www.w3.org/1999/xhtml,应包含在任何格式良好的XHTML页面的元素中,如下所示:
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Example XHTML page</title>
</head>
<body>
Hello world!
</body>
</html>
默认情况下,上例所有元素都被视为XHTML命名空间的一部分。可以使用xmlns加冒号为XML命名空间显式创建前缀。如下所示:
<xhtml:html xmlns:xhtml="http://www.w3.org/1999/xhtml">
<xhtml:head>
<xhtml:title>Example XHTML page</xhtml:title>
</xhtml:head>
<xhtml:body>
Hello world!
</xhtml:body>
</xhtml:html>
在这里,XHTML的命名空间是用xhtml前缀定义的,这要求所有XHTML元素都以该前缀开头。特性名也要如此,以避免语言之间的混淆,如下:
<xhtml:html xmlns:xhtml="http://www.w3.org/1999/xhtml">
<xhtml:head>
<xhtml:title>Example XHTML page</xhtml:title>
</xhtml:head>
<xhtml:body xhtml:class="home">
Hello world!
</xhtml:body>
</xhtml:html>
当文档中仅使用一种基于XML的语言时,没有必要使用命名空间;但是,将两种语言混合在一起时,它非常有用。如下包含XHTML和SVG的文档:
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Example XHTML page</title>
</head>
<body>
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 100 100"
style="width:100%; height:100%">
<rect x="0" y="0" width="100" height="100" style="fill:red" />
</svg>
</body>
</html>
在这个例子中,<svg>的所有孩子,被认为是在http://www.w3.org/2000/svg命名空间中。即使该文档从技术上来说是XHTML文档,但由于使用了命名空间,该SVG代码仍被认为是有效的。
节点的变化
Node类型在DOM2中演变为包括以下特定命名空间的属性:
localName 没有命名空间前缀的节点名。
namespaceURI 节点的命名空间URI,未指定则为null。
prefix 命名空间的前缀,未指定则为null。
当节点使用命名空间前缀时,nodeName等于前缀+“:” + localName。如下所示:
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Example XHTML page</title>
</head>
<body>
<s:svg xmlns:s="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 100 100" style="width:100%; height:100%">
<s:rect x="0" y="0" width="100" height="100" style="fill:red" />
</s:svg>
<script>
let html = document.documentElement;
console.log(html.tagName);//HTML
console.log(html.nodeName);//HTML
console.log(html.localName);//html
console.log(html.prefix);//null
console.log(html.namespaceURI);//http://www.w3.org/1999/xhtml
//let svg = html.lastChild.firstChild.nextSibling;
//console.log(svg.nodeName);//S:SVG
let svg = document.getElementsByTagNameNS("http://www.w3.org/1999/xhtml", "s:svg");
console.log(svg[0].nodeName);//S:SVG
</script>
</body>
</html>
DOM3更进一步,并引入了以下方法来使用命名空间(后两个存在bugMDN):
isDefaultNamespace(namespaceURI) 当指定的namespaceURI是该节点的默认命名空间时,返回
true。
lookupNamespaceURI(prefix) 返回给定前缀的命名空间URI。
lookupPrefix(namespaceURI) 返回给定命名空间的前缀。
文档的变化
Document类型在DOM2中包含了如下命名空间方法:
createElementNS(namespaceURI, tagName) 使用给定的参数创建新元素。
createAttributeNS(namespaceURI, attributeName) 使用给定参数创建一个特性节点,返回值为Attr类
型。
getElementsByTagNameNS(namespaceURI, tagName) 返回具有给定tagName的元素的NodeList,这些元素也属于namespaceURI指代的命名空间。
如下所示:
// 创建一个新SVG元素
let svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
// 在某个命名空间创建一个新特性
let att = document.createAttributeNS("http://www.somewhere.com", "random");
// 获取所有XHTML元素
let elems = document.getElementsByTagNameNS("http://www.w3.org/1999/xhtml",
"*");
仅当给定文档中有两个或多个命名空间时,才需要使用这些方法。
元素的变化
DOM2 Core中Element的变化主要为特性相关。引入了如下新方法:
getAttributeNS(namespaceURI, localName) 从namespaceURI命名空间获取名为localName的特性。
getAttributeNodeNS(namespaceURI, localName) 从namespaceURI命名空间获取名为localName的特性节点。
getElementsByTagNameNS(namespaceURI, tagName) 从namespaceURI命名空间获取名为tagName的所有元素的NodeList。
hasAttributeNS(namespaceURI, localName) 确定元素是否有给定命名空间的特性。
removeAttributeNS(namespaceURI, localName) 移除给定命名空间的特性。
setAttributeNS(namespaceURI, qualifiedName, value) 从命名空间URI表示的命名空间中设置特性,其键为qualifiedName,值为value。
setAttributeNodeNS(attNode) 从namespaceURI命名空间设置特性节点。
NamedNodeMap的变化
NamedNodeMap类型还引入了以下用于处理命名空间的方法。由于特性由NamedNodeMap表示,因此这些方法主要适用于特性:
getNamedItemNS(namespaceURI, localName) 获取namespaceURI命名空间中名为localName的项。
removeNamedItemNS(namespaceURI, localName) 移除namespaceURI命名空间中名为localName的 项。
setNamedItemNS(node) 添加已经应用了命名空间信息的节点。
其他变化
在DOM2 Core中,对DOM的各个部分进行了一些小的更改。这些更改与XML命名空间无关,而更多地用于确保API的健壮性和完整性。
DocumentType的变化
DocumentType类型添加了三个新属性:publicId,systemId和internalSubset。 publicId和systemId属性表示在doctype中容易获得但使用DOM1无法访问的数据。
Document的变化
Document上唯一与命名空间无关的新方法是importNode()。此方法的目的是从另一个文档中获取节点并将其导入到新文档中,以便可以将其添加到文档结构中。请记住,每个节点都有一个ownerDocument属性,该属性指示其所属的文档。如果调用诸如appendChild()之类的方法,并传入具有不同ownerDocument的节点,则会发生错误。从其他文档节点上调用importNode()会返回正确文档归属的节点的新版本。
importNode()方法类似于元素上的cloneNode()方法。它接受两个参数:要克隆的节点和指示是否还应复制子节点的布尔值。结果是适合在文档中使用的节点的副本。如下例伪代码:
// 导入节点和其所有子节点
let newNode = document.importNode(oldNode, true);
document.body.appendChild(newNode);
DOM2 Views添加了一个名为defaultView的属性,该属性是指向拥有给定文档的窗口(或框架)的指针。
除了上述一方法和属性以外,还以两种新方法的形式对DOM2 Core中指定的document.implementation对象进行了一些更改:createDocumentType()和createDocument()。createDocumentType()方法用于创建新的DocumentType节点,并接受三个参数:doctype的名称,publicId和systemId。createDocument()接受三个参数:文档元素的namespaceURI,文档元素的标签名称以及新文档的doctype。
DOM2 HTML模块还向document.implementation添加了一个名为createHTMLDocument()的方法。此方法的目的是创建一个完整的HTML文档,包含<html>, <head>, <title>, 和 <body>元素。此方法接受一个参数,即新创建的文档的标题。并返回HTML文档,如下所示:
let htmldoc = document.implementation.createHTMLDocument("New Doc");
console.log(htmldoc.title); // "New Doc"
console.log(typeof htmldoc.body); // "object"
通过对createHTMLDocument()的调用创建的对象是HTMLDocument类型的实例,因此包含与之关联的所有属性和方法,包括title和body属性。
Node的变化
DOM3引入了两种方法来帮助比较节点:isSameNode()和isEqualNode()。两种方法都接受单个节点作为参数,如果该节点与参考节点相同或相等,则返回true。当两个节点引用相同的对象时,它们是相同的。当两个节点具有相同的类型并且具有相等的属性(nodeName,nodeValue等),并且它们的attributes和childNodes属性相等时(在相同位置包含相等值),两个节点相等。如下所示:
let div1 = document.createElement("div");
div1.setAttribute("class", "box");
let div2 = document.createElement("div");
div2.setAttribute("class", "box");
console.log(div1.isSameNode(div1)); // true
console.log(div1.isEqualNode(div2)); // true
console.log(div1.isSameNode(div2)); // false
DOM3还引入了将附加数据添加到DOM节点的方法。 setUserData()方法将数据分配给节点并接受三个参数:要设置的键,实际数据(可以是任何数据类型)和处理程序函数。可以使用以下代码将数据分配给节点:
document.body.setUserData("name", "Ciri", function() {});
然后,可以使用getUserData()并传递相同的key来获取信息,如下所示:
let value = document.body.getUserData("name");
当节点的数据被复制、移除、重命名或导入到其他文档时将调用setUserData()的处理程序函数。该函数接受五个参数:一个代表操作类型的数字( 1 为克隆, 2 为导入, 3 为删除, 4 是重命名),数据键,数据值,源节点和目标节点。删除节点时,源节点为null,除非要克隆该节点,否则目标节点为null。如下所示:
let div = document.createElement("div");
div.setUserData("name", "Ciri", function(operation, key, value, src, dest)
{
if (operation == 1) {
dest.setUserData(key, value, function() {});
}
});
let newDiv = div.cloneNode(true);
console.log(newDiv.getUserData("name")); // "Ciri"
iframes的变化
由HTMLIFrameElement表示的iframe在DOM Level 2 HTML中具有一个名为contentDocument的新属性。此属性包含一个指向表示iframe内容的文档对象的指针:
let iframe = document.getElementById("myIframe");
let iframeDoc = iframe.contentDocument;
contentDocument属性是Document的实例,可以像任何其他HTML文档一样使用,包括所有属性和方法。还有一个名为contentWindow的属性,该属性返回框架的window对象,该对象具有document属性。contentDocument和contentWindow属性在所有现代浏览器中均可用。
样式
在HTML中有三种定义样式的方式:通过<link>包含外部样式单,使用<style>元素定义内联样式,使用style特性定义元素相关样式。
访问元素样式
任何支持style特性的HTML元素也具有在JavaScript中公开的style属性。style对象是CSSStyleDeclaration的实例,其包含相应HTML的sytle特性的所有信息。任何CSS的style特性的属性都表示为style对象的属性。因为
css属性使用破折号,在Javascript中需转换为驼峰命名法:
CSS属性 | Javascript属性 |
---|---|
background-image | style.backgroundImage |
color | style.color |
display | style.display |
font-family | style.fontFamily |
可使用JavaScript设置css样式:
let myDiv = document.getElementById("myDiv");
// 设置背景色
myDiv.style.backgroundColor = "red";
// 改变尺寸
myDiv.style.width = "100px";
myDiv.style.height = "200px";
// 边界赋值
myDiv.style.border = "1px solid black";
如果元素未指定style特性,则style对象的属性为空值。
DOM样式属性和方法
DOM2样式规范还定义了style对象上的几个属性和方法。这些属性和方法可提供有关元素的style特性内容以及更改:
cssText 访问style特性的css代码。
length css属性个数。
parentRule 表示CSS信息的CSSRule对象。
getPropertyPriority(propertyName) 如果使用important设置了给定的属性,则返回 “important”。否则,它返回一个空字符串。
getPropertyValue(propertyName) 返回给定属性的字符串值。
item(index) 返回给定位置的CSS属性的名称。
removeProperty(propertyName) 从样式中删除给定的属性。
setProperty(propertyName, value, priority) 将给定属性设置为具有优先级的给定值(“important”或空字符串)。
cssText属性用法如下:
let myDiv = document.getElementById("myDiv");
myDiv.style.cssText = "width: 25px; height: 100px; background-color: green";
console.log(myDiv.style.cssText);
//width: 25px; height: 100px; background-color: green;
item(),lenght和getPropertyValue():
let prop, value, i, len;
for (i = 0, len = myDiv.style.length; i < len; i++) {
prop = myDiv.style[i]; // 也可用myDiv.style.item(i)
value = myDiv.style.getPropertyValue(prop);
console.log(`prop: ${value}`);
}
getPropertyValue()方法始终获取CSS属性值的字符串表示形式。
计算样式
style对象提供任何元素的style特性的信息,但是不包括影响该元素的其他样式单(如内联样式)。DOM2 Style为document.defaultView提供了一个叫getComputedStyle()的方法。该方法接受两个参数:需要获取计算样式的元素和一个伪元素(pseudo-element)字符串(如":after")。第二个参数可为空。getComputedStyle()方法返回一个CSSStyleDeclaration对象(与style属性相同的类型),其中包含该元素的所有计算出的样式:
<!DOCTYPE<html>
<head>
<title>Computed Styles Example</title>
<style type="text/css">
#myDiv {
background-color: blue;
width: 100px;
height: 200px;
}
</style>
</head>
<body>
<div id="myDiv" style="background-color: red; border: 1px solid black">
</div>
</body>
</html>
注意:按照规范,应该使用双冒号(::)而不是单个冒号(😃,以便区分伪类和伪元素。
在此例中,div的样式来自内联样式单和style特性,style对象具有backgroundColor和border的值,而宽度和高度则没有。下例代码可获取元素的计算样式:
let myDiv = document.getElementById("myDiv");
let computedStyle = document.defaultView.getComputedStyle(myDiv, null);
console.log(computedStyle.backgroundColor); // rgb(255, 0, 0)
console.log(computedStyle.width); // "100px"
console.log(computedStyle.height); // "200px"
console.log(computedStyle.border); // 1.25px solid rgb(0, 0, 0)不是blue
所有浏览器的计算样式都是只读的,计算样式包含浏览器内部样式单的信息。
使用样式单
CSSStyleSheet类型表示CSS样式单,这包括<link>元素和<style>元素里的样式。他们分别通过HTMLLinkElement类型和HTMLStyleElement类型表示。
CSSStyleSheet类型继承自StyleSheet,以下属性继承自StyleSheet:
disabled 一个布尔值,指示是否禁用样式单。此属性是可读/写的,因此将其值设置为true将禁用样式单。
href 若样式单来自<link>元素,则为其URI,否则为null。
media 样式单支持的媒体类型的集合。与所有DOM集合一样,该集合有length属性和item()方法。
ownerNode 指向拥有该样式单的节点,在HTML中为<link>元素或<style>元素(在XML中可能是处理指令)。如果该样式单在其他样式单中通过@import引入则该属性为null。
parentStyleSheet 当样式单通过@import引入时,该属性为导入它的样式单的指针。
title ownerNode的title特性。
type 表示样式单类型的字符串,CSS样式单为"text/css"。
href即:Hypertext Reference
除了disabled外,其他属性都是只读的。
CSSStyleSheet类型支持上面所有属性以及下面的属性和方法:
cssRules 样式单中包含的规则集合。
ownerRule 如果样式单使用@import导入,则该属性是指向导入者规则的指针,否则为null。
deleteRule(index) 删除cssRules集合中指定位置的规则。
insertRule(rule, index) 在cssRules集中index位置插入字符串形式的rule。
样式单的列表可通过document.styleSheets集合获取。可使用length获取文档样式单数量,每个样式单可通过item()方法或方括号获取。如下:
<html>
<head>
<title></title>
<style type="text/css">
#myDiv {
background-color: blue;
width: 100px;
height: 200px;
}
</style>
</head>
<body>
<div id="myDiv" style="background-color: red; border: 1px solid black">
<script>
let sheet = null;
console.log(document.styleSheets.length); //1
for (let i = 0, len = document.styleSheets.length; i < len; i++) {
sheet = document.styleSheets[i];
console.log(sheet); //只有<link>元素有href,<style>元素没有
//CSSStyleSheet {ownerRule: null, cssRules: CSSRuleList, rules:
CSSRuleList, type: "text/css", href: null, ...
}
}
</script>
</body>
</html>
document.styleSheets返回的样式单因浏览器而异,但所有浏览器都包含rel属性设置为"stylesheet"的<link>和<style>样式单。IE和Opera包含rel属性设置为"alternate stylesheet"的样式单。
CSS规则
CSSRule对象代表样式单中的每个规则,CSSRule实际上是其他几种类型的基类,但最常用的是CSSStyleRule,其代表样式信息。(其他规则包括@ import,@ font-face,@ page和@charset,尽管这些规则很少需要从脚本中访问)。下列是CSSStyleRule对象的属性:
cssText 返回整个规则的文本。该文本可能与样式单中的实际文本有所不同,因为浏览器在内部处理样式单的方式不同。 Safari始终将所有内容都转换为小写。
parentRule 如果此规则是导入的,则该属性代表导入者的规则,否则为null。
parentStyleSheet 此规则所属的样式单。
selectorText 返回规则的选择器文本。该文本可能与样式单中的实际文本不同,这是因为浏览器在内部处理样式单的方式不同。此属性在Firefox,Safari,Chrome和Internet Explorer中是只读的(会引发错误)。 Opera允许更改selectorText。
style 一个CSSStyleDeclaration对象,该对象允许设置和获取规则的具体样式值。
type 指示规则类型的常数。对于样式规则,始终为 1 。
三个最常用的属性是cssText,selectorText和style。 cssText属性类似于style.cssText属性,但不完全相同。前者包括选择器文本和围绕样式信息的花括号;后者仅包含样式信息(类似于元素上的style.cssText)。此外,cssText是只读的,而style.cssText可能会被重写。
大多数情况下,只需使用style属性操纵样式规则。如下css规则:
<style>
div.box {
background-color:width: 100px;
height: 200px;
}
</style>
可使用如下代码获取相应信息:
let sheet = document.styleSheets[0];
let rules = sheet.cssRules || sheet.rules;
let rule = rules[0];
console.log(rule.selectorText); // div.box
console.log(rule.cssText); //div.box { background-color: blue; width: 100px; height:
200px; }
console.log(rule.style.cssText); // background-color: blue; width: 100px; height:
200px;
console.log(rule.style.backgroundColor); // blue
console.log(rule.style.width); // 100px
console.log(rule.style.height); // 200px
创建规则
DOM声明使用insertRule()方法将新规则添加到现有样式单中。此方法需要两个参数:规则的文本和索引。如下:
sheet.insertRule("body { background-color: silver }", 0); // DOM method
尽管可以以这种方式添加规则,但是当添加的规则数量很大时,它将很快变得繁重。在这种情况下,最好使用“文档对象模型”一章中讨论的动态样式加载技术。
删除规则
从样式单中删除规则的DOM方法是deleteRule(),它接受一个参数:要删除的规则的索引:
sheet.deleteRule(0); // DOM method
元素尺寸
以下属性和方法不属于DOM Level 2样式规范,但与HTML元素上的样式相关。 DOM缺乏描述确定页面元素实际尺寸的方法。 IE首先引入了一些属性,以向开发人员公开尺寸信息。这些属性现已合并到所有主要浏览器中。
Offset尺寸
第一组属性处理偏移尺寸,其合并了元素在屏幕上占据的所有可视空间。元素在页面上的可视空间由其高度和宽度组成,包括所有填充,滚动条和边框(但不包括边距)。以下四个属性获取偏移尺寸:
offsetHeight 元素占据的垂直空间像素高度(以像素为单位),包括其高度,水平滚动条的高度(如果可见),顶部边框高度和底部边框高度。
offsetWidth 元素占用的水平空间宽度,包括元素的宽度,垂直滚动条的宽度(如果可见),左边框宽度和右边框宽度。
offsetLeft 元素左边框外部与外部元素左边框内部的距离。
offsetTop 元素顶边框外部与外部元素顶边框内部的距离。
offsetLeft和offsetTop属性与包含该元素的元素有关,该元素存储在offsetParent属性中。offsetParent
不一定与parentNode一样。如图所示:
注意:上述四个属性均是只读的,并在每次访问它们时进行计算。因此,应避免避免多次调用这些属性中的任何一个。而是将所需的值存储在局部变量中,以避免造成性能损失。
客户端尺寸
元素的客户端尺寸包括元素内容及其填充所占用的空间。客户端尺寸只有两个属性:clientWidth和clientHeight。 clientWidth属性是内容区域的宽度加上左侧和右侧填充的宽度。 clientHeight属性是内容区域的高度加上顶部和底部填充的高度。如图所示:
客户尺寸实际上是元素内部空间的尺寸,因此不计算滚动条占用的空间。这些属性最常见的用途是确定浏览器视口大小,这是通过使用document.documentElement的clientWidth和clientHeight属性完成的。
滚动尺寸
最后是滚动尺寸(scroll dimensions),它提供元素滚动内容的信息。一些元素如<html>,不用添加代码就能自己滚。而其他元素可以通过使用CSS overflow属性进行滚动。滚动尺寸的四个属性如下:
scrollHeight 没有滚动条时内容的高度。
scrollWeight 没有滚动条时内容的宽度。
scrollLeft 内容区域左侧隐藏的像素数。可以设置此属性以更改元素的滚动位置。
scrollTop 隐藏在内容区域顶部的像素数。可以设置此属性来更改元素的滚动位置。
scrollWidth和scrollHeight属性对于确定给定元素中内容的实际尺寸非常有用。
对于没有滚动条的文档,scrollWidth和scrollHeight与clientWidth和clientHeight之间的关系尚不清楚。这些属性在document.documentElement上因浏览器而异:
Firefox使这些属性保持相等,但是大小与文档内容的实际大小有关,而不是视口的大小。
Opera,Safari和Chrome保持这些属性不同,scrollWidth和scrollHeight等于视口的大小,clientWidth和clientHeight等于文档的内容。
确定元素尺寸
浏览器为每个元素提供一个getBoundingClientRect()的方法,该方法返回一个DOMRect对象,该对象具有六个属性:left,top,right,bottom,height和width。这些属性提供页面上元素相对于视口的位置。
遍历
DOM2 Traversal and Range 模块定义了两个类型用于连续遍历DOM结构:NodeIterator和TreeWalker,在给定具体起点的情况下执行DOM结构的深度优先遍历。
如前所述,DOM遍历是DOM结构的深度优先遍历,它允许在至少两个方向上移动(取决于所使用的类型)。遍历以给定根定节点开始,并且遍历DOM树的深度不能超过该根。如下:
<!DOCTYPE html>
<html>
<head>
<title>Example</title>
</head>
<body>
<p><b>Hello</b> world!</p>
</body>
</html>
此页面的DOM树如图所示:
任何节点都可以是遍历的根节点,假如<body>元素为遍历根节点,则该遍历可访问<p>元素,<b>元素,和它的两个文本节点。然而,遍历不可能到达<html>元素,<head>元素,或其他不在<body>元素子树中的节点。下图描述了以document为根节点的DOM树的深度优先遍历:
NodeIterator
可使用document.createNodeIterator()创建NodeIterator实例,该方法接受以下四个参数:
root 树上开始搜索的节点。
whatToShow 应访问哪些节点的数字代码。
filter 一个NodeFilter对象或一个函数,指示是否应接受或拒绝特定节点。
entityReferenceExpansion 一个布尔值,指示是否应扩展实体引用。这在HTML页面中无效,因为实体引用永远不会扩展。
whatToShow参数是一个位掩码,可通过应用一个或多个过滤器来确定要访问的节点。该参数的可能值作为常量包含在NodeFilter类型上,如下所示:
NodeFilter.SHOW_ALL 显示所有节点。
NodeFilter.SHOW_ELEMENT 显示元素节点。
NodeFilter.SHOW_ATTRIBUTE 显示特性节点。由于DOM结构,实际上无法使用。
NodeFilter.SHOW_TEXT 显示文本节点。
NodeFilter.SHOW_CDATA_SECTION 显示CData片段节点。 不在HTML页面中使用。
NodeFilter.SHOW_ENTITY_REFERENCE 显示实体引用节点。 不在HTML页面中使用。
NodeFilter.SHOW_ENTITY 显示实体节点。 不在HTML页面中使用。
NodeFilter.SHOW_PROCESSING_INSTRUCTION 显示PI节点。 不在HTML页面中使用。
NodeFilter.SHOW_COMMENT 显示注释节点。
NodeFilter.SHOW_DOCUMENT 显示文档节点。
NodeFilter.SHOW_DOCUMENT_TYPE 显示文档类型节点。
NodeFilter.SHOW_DOCUMENT_FRAGMENT 显示文档片段节点,不在HTML页面中使用。
NodeFilter.SHOW_NOTATION 显示符号节点。 不在HTML页面中使用。
除了NodeFilter.SHOW_ALL外,可以使用按位或运算符组合多个选项,如下所示:
let whatToShow = NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT;
createNodeIterator()的filter参数可用于指定自定义NodeFilter对象或充当节点过滤器的函数。NodeFilter对象只有一个方法acceptNode(),如果应该访问给定的节点,则返回NodeFilter.FILTER_ACCEPT;如果不应该访问给定的节点,则返回NodeFilter.FILTER_SKIP。由于NodeFilter是抽象类型,因此无法创建它的实例。相反,只需使用acceptNode()方法创建一个对象,然后将该对象传递给createNodeIterator()。如下所示:
let filter = {
acceptNode(node) {
return node.tagName.toLowerCase() == "p" ?
NodeFilter.FILTER_ACCEPT :
NodeFilter.FILTER_SKIP;
}
};
let iterator = document.createNodeIterator(root, NodeFilter.SHOW_ELEMENT,
filter, false);
第三个参数也可以是采用acceptNode()方法形式的函数,如下所示:
let filter = function(node) {
return node.tagName.toLowerCase() == "p" ?
NodeFilter.FILTER_ACCEPT :
NodeFilter.FILTER_SKIP;
};
let iterator = document.createNodeIterator(root, NodeFilter.SHOW_ELEMENT,
filter, false);
如果不需要过滤器,则第三个参数应设置为null。
要创建访问所有节点类型的简单NodeIterator,可使用以下代码:
let iterator = document.createNodeIterator(document, NodeFilter.SHOW_ALL,
null, false);
NodeIterator的两个主要方法是nextNode()和previousNode()。nextNode()方法在DOM子树的深度优先遍历中向前移动一步,而previousNode()在遍历过程中向后移动一步。首次创建NodeIterator时,内部指针指向root,因此对nextNode()的第一次调用将返回root。当遍历到达DOM子树中的最后一个节点时,nextNode()返回null。 previousNode()方法的工作方式类似。当遍历到达DOM子树中的最后一个节点时,在previousNode()返回遍历的root之后,它将返回null。如下html片段:
<div id="div1">
<p><b>Hello</b> world!</p>
<ul>
<li>List item 1</li>
<li>List item 2</li>
<li>List item 3</li>
</ul>
</div>
如果要遍历div中的所有元素,可使用如下代码:
let div = document.getElementById("div1");
let iterator = document.createNodeIterator(div, NodeFilter.SHOW_ELEMENT,
null, false);
let node = iterator.nextNode();
console.log(node);//div元素
while (node !== null) {
console.log(node.tagName);
// DIV
// P
// B
// UL
// OL * 3
node = iterator.nextNode();
}
如果只想返回li元素,可使用如下代码:
let div = document.getElementById("div1");
let filter = function(node) {
return node.tagName.toLowerCase() == "li" ?
NodeFilter.FILTER_ACCEPT :
NodeFilter.FILTER_SKIP;
};
let iterator = document.createNodeIterator(div, NodeFilter.SHOW_ELEMENT,
filter, false);
let node = iterator.nextNode();
while (node !== null) {
console.log(node.tagName); // output the tag name
node = iterator.nextNode();
}
TreeWalker
TreeWalker是NodeIterator的更高级版本。它具有相同的功能,包括nextNode()和previousNode(),并添加了以下方法以不同方向遍历DOM结构:
parentNode() 遍历到当前节点的父亲
firstChild() 遍历到当前节点的第一个孩子
lastChild() 遍历到当前节点的最后一个孩子
nextSibling() 遍历到当前节点的下一个同胞
previousSibling() 遍历到当前节点的上一个同胞
使用document.createTreeWalker()方法创建一个TreeWalker对象,接受的参数同document.createNodeIterator():开始遍历的根节点,展示哪些节点类型,一个过滤器和一个布尔值指示实体引用是否扩展。由于这些相似之处,因此可以始终使用TreeWalker代替NodeIterator。如下所示:
<div id="div1">
<p><b>Hello</b> world!</p>
<ul>
<li>List item 1</li>
<li>List item 2</li>
<li>List item 3</li>
</ul>
</div>
<script>
let div = document.getElementById("div1");
let filter = function (node) {
return node.tagName.toLowerCase() == "li" ?
NodeFilter.FILTER_ACCEPT :
NodeFilter.FILTER_SKIP;
};
let walker = document.createTreeWalker(div, NodeFilter.SHOW_ELEMENT,
filter, false);
let node = walker.nextNode();
while (node !== null) {
console.log(node.tagName);
// LI
// LI
// LI
node = walker.nextNode();
}
</script>
一个不同的地方是filter的返回值,除了NodeFilter.FILTER_ACCEPT和NodeFilter.FILTER_SKIP外,还有NodeFilter.FILTER_REJECT。当使用NodeIterator对象时,NodeFilter.FILTER_SKIP和NodeFilter.FILTER_REJECT做相同的事情:跳过该节点。当使用TreeWalker对象时,NodeFilter.FILTER_SKIP跳过该节点并继续过滤子树的下一个节点。然而NodeFilter.FILTER_REJECT跳过该节点和整个子树。
TreeWalker的真正强大之处在于它能够绕过DOM结构。无需指定过滤器,就可以直接导航到特定元素:
<div id="div1">
<p><b>Hello</b> world!</p>
<ul>
<li>List item 1</li>
<li>List item 2</li>
<li>List item 3</li>
</ul>
</div>
<script>
let div = document.getElementById("div1");
let walker = document.createTreeWalker(div, NodeFilter.SHOW_ELEMENT, null,
false);
console.log(walker.parentNode()); // null
console.log(walker.firstChild().tagName); // P
//console.log(walker.parentNode()); // div#div1
console.log(walker.lastChild().tagName); //B P的lastChild为B
console.log(walker.nextSibling()); // null B的nextSibling为null
console.log(walker.previousSibling()); // null B的previousSibling为null
console.log(walker.previousNode().tagName); //P B的previousNode为P
let node = walker.firstChild();
while (node !== null) {
console.log(node.tagName);
// B P的firstChild为B
// UL B的nextNode为UL
// LI UL的nextNode为LI
// LI Li的nextNode为Li
// LI Li的nextNode为Li
node = walker.nextNode();
}
</script>
TreeWalker有一个currentNode属性,指示上一个遍历方法返回的节点。可更改此属性以继续从这里恢复遍历:
let node = walker.nextNode();
console.log(node === walker.currentNode); // true
walker.currentNode = document.body; // 改变到开始遍历的地方
范围
为了更好地控制页面,DOM2 Traversal and Range模块定义了一个叫范围的接口。无论节点边界如何,都可以使用范围来选择文档的一部分。 (此选择在幕后进行,用户看不到。)
DOM中的范围
DOM2在Document类型上定义了一个方法叫createRange(),该方法属于document对象。创建方法如下:
let range = document.createRange();
同节点一样,新创建的范围直接绑定到创建它的文档,不能被其他文档使用。范围可在幕后用来选择文档的一部分。创建范围并设置好位置后,就可以对该范围的内容执行许多不同的操作,从而可以对底层DOM树进行细粒度的操作。
每个范围通过Range的实例表示,有许多属性和方法。下面的属性提供了关于范围在文档中位置的信息:
startContainer 范围开始节点的父节点
startOffset startContainer内范围开始位置的偏移量。如果startContainer是文本节点,注释节点,
CData节点,则startOffset是范围开始之前跳过的字符数;否则,偏移量是该范围内第一个子节点的索引。
endContainer 范围结束节点的父节点
endOffset endContainer内范围结束位置的偏移量(规则同startOffset)
commonAncestorContainer 文档中同时拥有后代节点startContainer和endContainer的最深的节点
当范围放置在文档中指定位置后这些属性被填充。
DOM范围的简单选区
使用范围选择文档一部分的最简单方式是使用selectNode()方法或selectNodeContents()方法。每个方法都接受一个参数:一个DOM节点,并使用节点的信息填充范围。selectNode()方法选择整个节点,包括它的孩子们;然而selectNodeContents()仅选择节点的孩子们:
<!DOCTYPE html>
<html>
<body>
<p id="p1"><b>Hello</b> world!</p>
</body>
</html>
可使用如下代码访问这些内容:
let range1 = document.createRange(),
range2 = document.createRange(),
p1 = document.getElementById("p1");
range1.selectNode(p1);
console.log(range1.startContainer); //document.body
console.log(range1.endContainer); //document.body
console.log(range1.commonAncestorContainer); //document.body
//console.log(document.body.firstChild); //#text
console.log(range1.startOffset); //1
console.log(range1.endOffset); //2
range2.selectNodeContents(p1);
console.log(range2.startContainer); //节点p
console.log(range2.endContainer); //节点p
console.log(range2.commonAncestorContainer); //节点p
console.log(range2.startOffset); //0
console.log(range2.endOffset); //2
range1包含<p>元素和它所有的孩子,然而range2包含<b>元素,文本节点“Hello”和“world”:
当调用selectNode()时,startContainer,endContainer和commonAncestorContainer都等于传入的节点的父节点。在上例中,它们都等于document.body。startOffset属性等于父节点的childNodes集合中给定节点的索引(在此示例中为 1 ,请记住,与DOM兼容的浏览器将空格视为文本节点)。而endOffset等于startOffset加上 1 (因为仅选择了一个节点)。
当调用selectNodeContents()时,startContainer,endContainer和commonAncestorContainer等于传入的节点,在上例中为<p>元素。startOffset属性始终等于 0 ,因为范围从给定节点的第一个子节点开始;而endOffset等于子节点的数目(node.childNodes.length),在此示例中为 2 。
使用以下范围方法,可以更精细地控制选区中包括哪些节点:
setStartBefore(refNode) 将范围的起点设置为在refNode之前,因此refNode是选区中的第一个节点。 startContainer属性设置为refNode.parentNode,而startOffset属性设置为refNode在其父节点的childNodes集合内的索引。
setStartAfter(refNode) 将范围的起点设置为在refNode之后,因此refNode不在选区范围内;相反,它的下一个同胞是选区中的第一个节点。 startContainer属性设置为refNode.parentNode,而startOffset属性设置为refNode在其父节点的childNodes集合内的索引加一。
setEndBefore(refNode) 将范围的结束点设置为在refNode之前,因此refNode不在选区范围内;它的前一个同胞是选区中的最后一个节点。 endContainer属性设置为refNode.parentNode,而endOffset属性设置为refNode在其父节点的childNodes集合内的索引。
setEndAfter(refNode) 将范围的结束点设置为在refNode之后,因此refNode是选区中的最后一个节点。 endContainer属性设置为refNode.parentNode,并将endOffset属性设置为其父节点的childNodes集合中refNode的索引加一。
DOM范围的复杂选区
创建复杂选区需要使用setStart()方法和setEnd()方法。两个方法都接受两个参数:参考节点和偏移量。对于setStart()方法,参考节点将成为startContainer,偏移量成为startOffset。而setEnd()方法,参考节点将成为endContainer,偏移量成为endOffset。
使用这两个方法,可以模仿selectNode()和selectNodeContents()方法。如下所示:
<!DOCTYPE html>
<html>
<body>
<p id="p1"><b>Hello</b> world!</p>
<script>
let range1 = document.createRange(),
range2 = document.createRange(),
p1 = document.getElementById("p1"),
p1Index = -1,
i,
len;
for (i = 0, len = p1.parentNode.childNodes.length; i < len; i++) {
if (p1.parentNode.childNodes[i] === p1) {
p1Index = i;
break;
}
}
range1.setStart(p1.parentNode, p1Index);
range1.setEnd(p1.parentNode, p1Index + 1);
range2.setStart(p1, 0);
range2.setEnd(p1, p1.childNodes.length);
</script>
</body>
</html>
若只想从“Hello”中的“llo”到“world”中的“o”中进行选择。在之前的HTML代码中。这是很容易实现的。第一步是获取对所有相关节点的引用,如以下示例所示:
let p1 = document.getElementById("p1"),
helloNode = p1.firstChild.firstChild,
worldNode = p1.lastChild
接下来,必须创建范围并定义其边界,如下所示:
let range = document.createRange();
range.setStart(helloNode, 2);
range.setEnd(worldNode, 3);
因为选区应该在“Hello”中的“e”之后开始,所以将helloNode和偏移量 2 (“e”之后的位置,其中“H”位于位置 0 )传递到setStart()中。
要设置选区的结尾,需将worldNode和偏移量 3 传递到setEnd()中,指示不应选择的第一个字符,位置 3 为“r”(位置 0 实际上存在空格)。如图所示:
因为helloNode和worldNode都是文本节点,所以它们分别成为该范围的startContainer和endContainer,以便startOffset和endOffset可以准确地查看每个节点中包含的文本,而不是查找子节点(传递元素时会发生这种情况)。commonAncestorContainer为p元素。
与DOM范围的内容交互
创建范围后,将在内部创建一个文档片段节点,所选内容中的所有节点都将附加到该文档片段节点上。范围内容必须格式正确,在上一个示例中,在前面的示例中的范围不能代表格式正确的DOM结构,因为选区开始于一个文本节点内部,而结束于另一个文本节点内,而该文本节点无法在DOM中表示。但是,范围可以识别丢失的开始和结束标签,因此可以重建有效的DOM结构以进行操作。
在之前的例子中,范围计算时<b>的开始标签在选区中丢失了,所以范围在幕后动态地加上了。与一个新</b>结束标签一起将“He”包起来,因此改变了DOM,如下所示:
<p><b>He</b><b>llo</b> world!</p>
此外,“World!”文本节点分为两个文本节点,一个包含“wo”,另一个包含“rld!”。生成的DOM树以及该范围的文档片段的内容如下图所示:
创建范围后,可以使用多种方法来操作范围的内容。(请注意,范围内部文档片段中的所有节点只是指向文档中节点的指针。)
第一种方法最容易理解和使用:deleteContents()。此方法只是从文档中删除范围的内容:
let p1 = document.getElementById("p1"),
helloNode = p1.firstChild.firstChild,
worldNode = p1.lastChild,
range = document.createRange();
range.setStart(helloNode, 2);
range.setEnd(worldNode, 3);
range.deleteContents();
执行这些代码将渲染成:
<p><b>He</b>rld!</p>
由于范围选区过程更改了基础DOM结构以保持良好的格式,因此即使删除内容后,生成的DOM结构仍然保持的很好。
extractContents()同deleteContents()一样也从文档中删除范围选区。不同之处在于extractContents()将范围的文档片段作为函数返回值。这可以将范围的内容插入其他位置:
let p1 = document.getElementById("p1"),
helloNode = p1.firstChild.firstChild,
worldNode = p1.lastChild,
range = document.createRange();
range.setStart(helloNode, 2);
range.setEnd(worldNode, 3);
let fragment = range.extractContents();
p1.parentNode.appendChild(fragment);
在此示例中,片段被提取并添加到文档<body>元素的末尾。导致html变为如下:
<p><b>He</b>rld!</p>
<b>llo</b> wo
另一个选择是保留范围,但是创建一个副本,可以使用cloneContents()将其插入文档中的其他位置:
let p1 = document.getElementById("p1"),
helloNode = p1.firstChild.firstChild,
worldNode = p1.lastChild,
range = document.createRange();
range.setStart(helloNode, 2);
range.setEnd(worldNode, 3);
let fragment = range.cloneContents();
p1.parentNode.appendChild(fragment);
此方法与extractContents()非常相似,因为两者均返回文档片段。主要区别在于cloneContents()返回的文档片段是该范围内所包含节点的克隆,而不是实际节点。通过此操作,页面中的HTML如下所示:
<p><b>Hello</b><b>llo</b> wo
插入DOM范围内容
如上一节所述,范围可用于删除或克隆内容,以及在范围内操作内容。 insertNode()方法可以在范围选择的开始处插入节点。例如,假设要在前面的示例中使用的HTML之前插入以下HTML:
<span style="color: red">Inserted text</span>
可使用如下代码完成:
let p1 = document.getElementById("p1"),
helloNode = p1.firstChild.firstChild,
worldNode = p1.lastChild,
range = document.createRange();
range.setStart(helloNode, 2);
range.setEnd(worldNode, 3);
let span = document.createElement("span");
span.style.color = "red";
span.appendChild(document.createTextNode("Inserted text"));
range.insertNode(span);
运行之后会创建如下html代码:
<p id="p1"><b>He<span style="color: red">Inserted text</span>llo</b> world</p>
除了将内容插入范围外,还可以通过使用SurroundContents()方法来插入范围周围的内容。此方法接受一个参数,该参数是包围范围内容的节点。在幕后,执行以下步骤:
提取范围的内容。
给定的节点将插入到原始文档中范围所在的位置。
文档片段的内容将添加到给定节点。
这种功能对于在线突出显示网页中的某些单词很有用,如下所示:
let p1 = document.getElementById("p1"),
helloNode = p1.firstChild.firstChild,
worldNode = p1.lastChild,
range = document.createRange();
range.selectNode(helloNode);
let span = document.createElement("span");
span.style.backgroundColor = "yellow";
range.surroundContents(span);
此代码以黄色背景突出显示范围选区。产生的HTML如下:
<p><b><span style="background-color:yellow">Hello</span></b> world!</p>
折叠DOM范围
如果某个范围没有选择文档的任何部分,则称该文档已折叠。折叠范围类似于文本框的行为。当文本框中有文本时,可以使用鼠标突出显示整个单词。但是,如果再次左键单击鼠标,则所选内容将被删除,并且光标位于两个字母之间。折叠范围时,它的位置设置在文档各部分之间,可以在范围选区的开始或结尾。如下图所示:
可以使用collapse()方法折叠范围,该方法接受一个参数:一个布尔值,指示要折叠到范围的哪一端。如果参数为true,则范围会折叠到起点;如果为false,则范围会折叠到终点。要确定某个范围是否已经折叠,可以按如下所示使用collapsed属性:
range.collapse(true); // 折叠到起点
console.log(range.collapsed); // true
如果不确定范围内的两个节点是否彼此相邻,则测试范围是否折叠是有帮助的。如下所示:
<p id="p1">Paragraph 1</p><p
id="p2">Paragraph 2</p>
如果不知道此代码的确切构成(例如,如果它是自动生成的),则可以尝试创建如下范围:
let p1 = document.getElementById("p1"),
p2 = document.getElementById("p2"),
range = document.createRange();
range.setStartAfter(p1);
range.setStartBefore(p2);
console.log(range.collapsed); // true
在这种情况下,创建的范围被折叠,因为在p1的末尾和p2的开始之间没有任何东西。
比较DOM范围
如果有多个范围,则可以使用compareBoundaryPoints()方法来确定范围是否具有任何共同的边界(开始或结束)。该方法接受两个参数:要比较的范围和如何比较。它是以下常量值之一:
Range.START_TO_START 比较第一个范围的起点和第二个范围的起点。
Range.START_TO_END 比较第一个范围的起点和第二个范围的终点。
Range.END_TO_END 比较第一个范围的终点和第二个范围的终点。
Range.END_TO_START 比较第一个范围的终点和第二个范围的起点。
如果第一个范围的点(起点或终点)位于第二个范围的点之前,则compareBoundaryPoints()方法返回–1 ;如果相等,则返回 0 ;如果第一个范围的点位于第二个范围的点之后,则返回 1 。如下所示:
//<p id="p1"><b>Hello</b> world!</p>
let range1 = document.createRange();
let range2 = document.createRange();
let p1 = document.getElementById("p1");
range1.selectNodeContents(p1);
range2.selectNodeContents(p1);
range2.setEndBefore(p1.lastChild);
console.log(range1.compareBoundaryPoints(Range.START_TO_START, range2)); // 0
console.log(range1.compareBoundaryPoints(Range.END_TO_END, range2)); // 1
在此代码中,两个范围的起点完全相同,因为两者都使用selectNodeContents()中的默认值;因此,该方法返回 0 。但是,对于range2,使用setEndBefore()更改了终点,使range1的终点位于range2的终点之后(见下图),因此该方法返回 1 。
克隆DOM范围
可以通过调用cloneRange()方法来克隆范围。此方法将创建与该范围完全相同的副本:
let newRange = range.cloneRange();
新范围包含与原始范围相同的所有属性,并且可以修改其端点而不会以任何方式影响原始范围。
清除
使用范围完成后,最好调用detach()方法,该方法会将范围从创建范围的文档中分离出来。调用detach()之后,可以安全地取消引用范围,因此可被垃圾回收。如下所示:
range.detach(); // 从文档分离
range = null; // 解除引用
遵循这两个步骤是完成范围使用的最合适方法。分离后,将无法再使用范围。