跳到主要内容

DOM2 和 DOM3

DOM1(DOM Level 1)主要定义了 HTML 和 XML 文档的底层结构。DOM2(DOM Level 2)和DOM3(DOM Level 3)在这些结构之上加入更多交互能力,提供了更高级的 XML 特性。实际上,DOM2和 DOM3 是按照模块化的思路来制定标准的,每个模块之间有一定关联,但分别针对某个 DOM 子集。这些模式如下所示。

  • DOM Core:在 DOM1 核心部分的基础上,为节点增加方法和属性。
  • DOM Views:定义基于样式信息的不同视图。
  • DOM Events:定义通过事件实现 DOM 文档交互。
  • DOM Style:定义以编程方式访问和修改 CSS 样式的接口。
  • DOM Traversal and Range:新增遍历 DOM 文档及选择文档内容的接口。
  • DOM HTML:在 DOM1 HTML 部分的基础上,增加属性、方法和新接口。
  • DOM Mutation Observers:定义基于 DOM 变化触发回调的接口。这个模块是 DOM4 级模块,用于取代 Mutation Events。

DOM 的演进

DOM2 和 DOM3 Core 模块的目标是扩展 DOM API,满足 XML 的所有需求并提供更好的错误处理和特性检测。很大程度上,这意味着支持 XML 命名空间的概念。DOM2 Core 没有新增任何类型,仅仅在 DOM1 Core 基础上增加了一些方法和属性。DOM3 Core 则除了增强原有类型,也新增了一些新类型。

XML 命名空间

XML 命名空间可以实现在一个格式规范的文档中混用不同的 XML 语言,而不必担心元素命名冲突。严格来讲,XML 命名空间在 XHTML 中才支持,HTML 并不支持。因此,本节的示例使用 XHTML

命名空间是使用 xmlns 指定的。XHTML 的命名空间是 http://www.w3.org/1999/xhtml ,应该包含在任何格式规范的 XHTML 页面的 <html> 元素中,如下所示:

<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Example XHTML page</title>
</head>
<body>
Hello world!
</body>
</html>

其他变化

样式

HTML 中的样式有 3 种定义方式:外部样式表(通过 <link> 元素)、文档样式表(使用 <style> 元素)和元素特定样式(使用 style 属性)。DOM2 Style 为这 3 种应用样式的机制都提供了 API。

存取元素样式

任何支持 style 属性的 HTML 元素在 JavaScript 中都会有一个对应的 style 属性。这个 style 属性是 CSSStyleDeclaration 类型的实例,其中包含通过 HTML style 属性为元素设置的所有样式信息,但不包含通过层叠机制从文档样式和外部样式中继承来的样式。HTML style 属性中的 CSS 属性在 JavaScript style 对象中都有对应的属性。因为 CSS 属性名使用连字符表示法(用连字符分隔两个单词,如 background-image),所以在 JavaScript 中这些属性必须转换为驼峰大小写形式(如backgroundImage)。

大多数属性名会这样直接转换过来。但有一个 CSS 属性名不能直接转换,它就是 float。因为 float 是 JavaScript 的保留字,所以不能用作属性名。DOM2 Style 规定它在 style 对象中对应的属性应该是 cssFloat。

任何时候,只要获得了有效 DOM 元素的引用,就可以通过 JavaScript 来设置样式。

示例
let myDiv = document.getElementById('myDiv');

// 设置背景颜色
myDiv.style.backgroundColor = 'red';

// 修改大小
myDiv.style.width = '100px';
myDiv.style.height = '200px';

// 设置边框
myDiv.style.border = '1px solid black';
注意

在标准模式下,所有尺寸都必须包含单位。在混杂模式下,可以把 style.width 设置为"20",相当于"20px"。如果是在标准模式下,把 style.width 设置为"20"会被忽略,因为没有单位。实践中,最好一直加上单位。

通过 style 属性设置的值也可以通过 style 对象获取。比如下面的 HTML:

<div id="myDiv" style="background-color: blue; width: 10px; height: 25px"></div>

如果元素上没有 style 属性,则 style 对象包含所有可能的 CSS 属性的空值。

1.DOM 样式属性和方法

DOM2 Style 规范也在 style 对象上定义了一些属性和方法。这些属性和方法提供了元素 style 属性的信息并支持修改,列举如下。

  • cssText: 用于获取或设置元素的整个内联样式(即 style 属性中的所有 CSS 规则)。
  • parentRule: 表示 CSS 信息的 CSSRule 对象。
  • item(index): 返回索引为 index 的 CSS 属性名。
  • length: 应用给元素的 CSS 属性数量。
  • getPropertyValue(propertyName): 返回属性 propertyName 的字符串值。
  • getPropertyPriority(propertyName): 如果 CSS 属性 propertyName 使用了!important则返回"important",否则返回空字符串。
  • removeProperty(propertyName): 从样式中删除 CSS 属性 propertyName。
  • setProperty(propertyName, value, priority): 设置 CSS 属性 propertyName 的值为 value,priority 是"important"或空字符串。
cssText 示例
<div id="myDiv" style="background-color: red;width: 100px;height: 100px"></div>
<script>
let myDiv = document.getElementById('myDiv');

console.log(myDiv.style); // CSSStyleDeclaration 对象,包含所有的css样式空值
console.log(myDiv.style.cssText); // background-color: red; width: 100px; height: 100px;

// 对style内联样式进行的是覆盖,而不是写入
myDiv.style.cssText = 'background-color: blue;';
console.log(myDiv.style.cssText); // background-color: blue;
</script>

length 属性是跟 item() 方法一起配套迭代 CSS 属性用的。此时,style 对象实际上变成了一个集合,也可以用中括号代替 item() 取得相应位置的 CSS 属性名,使用 getPropertyValue() 以取得属性的值,如下所示:

length、item() 和 getPropertyValue() 组合示例
// <div id="myDiv" style="background-color: red;width: 100px;height: 100px"></div>
let myDiv = document.getElementById('myDiv');

for (let i = 0, len = myDiv.style.length; i < len; i++) {
let cssName = myDiv.style[i];
let cssValue = myDiv.style.getPropertyValue(cssName);

console.log('属性名:', cssName, '属性值:', cssValue);
// 属性名: background-color 属性值: red
// 属性名: width 属性值: 100px
// 属性名: height 属性值: 100px
}

removeProperty() 方法用于从元素样式中删除指定的 CSS 属性。使用这个方法删除属性意味着会应用该属性的默认(从其他样式表层叠继承的)样式。

removeProperty() 示例
// <div id="myDiv" style="background-color: red;width: 100px;height: 100px"></div>
let myDiv = document.getElementById('myDiv');

myDiv.style.removeProperty('background-color');

2.计算样式

style 对象中包含支持 style 属性的元素为这个属性设置的样式信息,但不包含从其他样式表层叠继承的同样影响该元素的样式信息。DOM2 Style在 document.defaultView 上增加了 getComputedStyle() 方法。

这个方法接收两个参数:要取得计算样式的元素和伪元素字符串(如":after")。如果不需要查询伪元素,则第二个参数可以传 nullgetComputedStyle() 方法返回一个 CSSStyleDeclaration对象(与 style 属性的类型一样),包含元素的计算样式。

getComputedStyle() 语法
/**
* @param element 用于获取计算样式的Element
* @param pseudoElt 指定一个要匹配的伪元素的字符串。必须对普通元素省略(或null)
*/
let style1 = window.getComputedStyle(element, [pseudoElt]);

// 两者是等价的
let style2 = document.defaultView.getComputedStyle(element, null);

返回的 style 是一个实时的 CSSStyleDeclaration 对象,当元素的样式更改时,它会自动更新本身。

getComputedStyle() 示例
<style>
#elem-container {
position: absolute;
left: 100px;
top: 200px;
height: 100px;
}
</style>

<div id="elem-container">Hello World!</div>

<script>
let elem = document.getElementById('elem-container');
let cssStyle1 = window.getComputedStyle(elem, null);

console.log(cssStyle1.getPropertyValue('height')); // 100px

let cssStyle2 = document.defaultView.getComputedStyle(elem, null);
console.log(cssStyle2.getPropertyValue('top')); // 200px
</script>

getComputedStyle 可以从伪元素拉取样式信息 (比如,::after, ::before, ::marker, ::line-marker)

<style>
h3::after {
content: 'rocks!';
}
</style>

<h3>generated content</h3>

<script>
let h3 = document.querySelector('h3'),
result = getComputedStyle(h3, '::after').content;
console.log(`the generated content is: ${result}`);
// the generated content is: "rocks!"
</script>

返回的对象与从元素的 style 属性返回的对象具有相同的类型;然而,两个对象具有不同的目的。从 getComputedStyle 返回的对象是只读的,可以用于检查元素的样式(包括由一个 <style> 元素或一个外部样式表设置的那些样式).

在许多在线的演示代码中,getComputedStyle 是通过 document.defaultView 对象来调用的。大部分情况下,这是不需要的,因为可以直接通过 window 对象调用。但有一种情况,你必需要使用 defaultView, 那是在 firefox3.6 上访问子框架内的样式。

操作样式表

CSSStyleSheet 类型表示 CSS 样式表,包括使用 <link> 元素和通过 <style> 元素定义的样式表。注意,这两个元素本身分别是 HTMLLinkElement 和 HTMLStyleElement。CSSStyleSheet 类型是一个通用样式表类型,可以表示以任何方式在 HTML 中定义的样式表。另外,元素特定的类型允许修改HTML 属性,而 CSSStyleSheet 类型的实例则是一个只读对象(只有一个属性例外 disable)。

CSSStyleSheet 接口代表一个 CSS 样式表,并允许检查和编辑样式表中的规则列表。它从父类型 StyleSheet 继承属性和方法。

  • disabled: 表示样式表是否被禁用 (唯一可读写的属性)。
  • href: 返回样式表的 URL,或者在未使用 <link> 标签包含时返回 null
  • media: 包含样式表支持的媒体类型的 MediaList 对象。
  • ownerNode: 指向拥有当前样式表的节点,可以是 <link><style> 元素。
  • title: ownerNode 的 title 属性。
  • parentStyleSheet: 指向导入当前样式表的样式表。
  • type: 表示样式表的类型,对于 CSS 样式表是 "text/css"。
  • cssRules: 包含当前样式表的所有样式规则的集合。
  • ownerRule: 指向导入当前样式表的规则,如果不是通过 @import 导入的,则为 null
  • insertRule(rule, index): 在指定位置插入样式规则。
  • deleteRule(index): 删除指定位置的样式规则。

document.styleSheets 表示文档中可用的样式表集合。这个集合的 length 属性保存着文档中样式表的数量,而每个样式表都可以使用中括号或 item() 方法获取。

for (const css of document.styleSheets) {
console.log(css); // CSSStyleSheet {}
}

console.log(document.styleSheets[0]); // CSSStyleSheet {}
console.log(document.styleSheets.item(0)); // CSSStyleSheet {}
console.log(document.styleSheets[0] === document.styleSheets.item(0)); // true

1.CSS规则

CSSRule 类型表示样式表中的一条规则。这个类型也是一个通用基类,很多类型都继承它,但其中最常用的是表示样式信息的 CSSStyleRule(其他 CSS 规则还有 @import@font-face@page@charset 等,不过这些规则很少需要使用脚本来操作)。以下是 CSSStyleRule 对象上可用的属性。

  • cssText: 返回整条规则的文本,可能与样式表中实际的文本不同(因为浏览器内部处理样式表的方式可能不同)。
  • parentRule: 如果这条规则被其他规则(如@media)包含,则指向包含规则,否则为 null
  • parentStyleSheet: 包含当前规则的样式表。
  • selectorText: 返回规则的选择符文本,可能与实际文本不同(因为浏览器内部处理样式表的方式可能不同)。
  • style: 返回 CSSStyleDeclaration 对象,可设置和获取当前规则中的样式。
  • type: 表示规则类型的数值常量,对于样式规则始终为 1。

在这些属性中,使用最多的是 cssTextselectorTextstylecssText 属性与 style.cssText类似,不过并不完全一样。前者包含选择符文本和环绕样式声明的大括号,而后者则只包含样式声明(类似于元素上的 style.cssText)。此外,cssText 是只读的,而 style.cssText 可以被重写。

多数情况下,使用 style 属性就可以实现操作样式规则的任务了。这个对象可以像每个元素上的 style 对象一样,用来读取或修改规则的样式。

示例
<style>
div.box {
background-color: blue;
width: 100px;
height: 200px;
}
</style>

<script>
let sheet = document.styleSheets[0];
let rules = sheet.cssRules; // 取得规则集合
let rule = rules[0]; // 取得第一条规则

console.log(rule.selectorText); // div .box

// 两者之间的差异
console.log(rule.style.cssText); // background-color: blue; width: 100px; height: 200px;
console.log(rule.cssText); // div.box { background-color: blue; width: 100px; height: 200px; }

console.log(rule.style.backgroundColor); // "blue"
</script>

使用这些接口,可以像确定元素 style 对象中包含的样式一样,确定一条样式规则的样式信息。与元素的场景一样,也可以修改规则中的样式,如下所示:

let sheet = document.styleSheets[0];
let rules = sheet.cssRules;
let rule = rules[0]; // 取得第一条规则

rule.style.backgroundColor = 'red';

2.创建规则

CSSStyleSheet.insertRule() 方法用来给当前样式表插入新的样式规则(CSS rule),并且包含一些限制。

insertRule() 语法
/**
* rule 一个包含了将要插入的规则的 DOMString。规则字符串必须包含的内容取决于它的类型:
* - rule-set 类型(普通带有选择器的规则),需要选择器和样式声明;
* - at-rule 类型(以 @ 开头的规则,如 @import, @media 等),需要 at-identifier 和规则内容。
* index 可选, 一个小于或等于 stylesheet.cssRules.length 的正整数,表示新插入的规则在CSSStyleSheet.cssRules 中的位置。默认值是 0。
*/
stylesheet.insertRule(rule [, index])
insertRule() 示例
stylesheet.insertRule('body { background-color: silver }', 0); // 使用 DOM 方法

3.删除规则

CSSStyleSheet.deleteRule() 方法用来从当前样式表对象中删除指定的样式规则。

deleteRule() 语法
/**
* index: 是一个数字,用来指定删除样式规则的位置
*/
stylesheet.deleteRule(index);

与添加规则一样,删除规则并不是 Web 开发中常见的做法。考虑到可能影响 CSS 层叠的效果,删除规则时要慎重。

元素尺寸

本节介绍的属性和方法并不是 DOM2 Style 规范中定义的,但与 HTML 元素的样式有关。DOM 一直缺乏页面中元素实际尺寸的规定。IE 率先增加了一些属性,向开发者暴露元素的尺寸信息。这些属性现在已经得到所有主流浏览器支持。

1.偏移尺寸

第一组属性涉及偏移尺寸(offset dimensions),包含元素在屏幕上占用的所有视觉空间。元素在页面上的视觉空间由其高度和宽度决定,包括所有内边距、滚动条和边框(但不包含外边距)。以下 4 个属性用于取得元素的偏移尺寸。

  • offsetHeight: 元素在垂直方向上占用的像素尺寸,包括它的高度、水平滚动条高度(如果可见)和上、下边框的高度。
  • offsetLeft: 元素左边框外侧距离包含元素左边框内侧的像素数。
  • offsetTop: 元素上边框外侧距离包含元素上边框内侧的像素数。
  • offsetWidth: 元素在水平方向上占用的像素尺寸,包括它的宽度、垂直滚动条宽度(如果可见)和左、右边框的宽度。

其中,offsetLeftoffsetTop 是相对于包含元素的,包含元素保存在 offsetParent 属性中。offsetParent 不一定是 parentNode。比如,<td> 元素的 offsetParent 是作为其祖先的 <table>元素,因为 <table> 是节点层级中第一个提供尺寸的元素。

16-1

要确定一个元素在页面中的偏移量,可以把它的 offsetLeft 和 offsetTop 属性分别与 offsetParent的相同属性相加,一直加到根元素。

示例
function getElementLeft(element) {
let actualLeft = element.offsetLeft;
let current = element.offsetParent;

while (current !== null) {
actualLeft += current.offsetLeft;
current = current.offsetParent;
}
return actualLeft;
}

function getElementTop(element) {
let actualTop = element.offsetTop;
let current = element.offsetParent;

while (current !== null) {
actualTop += current.offsetTop;
current = current.offsetParent;
}
return actualTop;
}

这两个函数使用 offsetParent 在 DOM 树中逐级上溯,将每一级的偏移属性相加,最终得到元素的实际偏移量。对于使用 CSS 布局的简单页面,这两个函数是很精确的。而对于使用表格和内嵌窗格的页面布局,它们返回的值会因浏览器不同而有所差异,因为浏览器实现这些元素的方式不同。一般来说,包含在 <div> 元素中所有元素都以 <body> 为其 offsetParent,因此 getElementLeft()getElementTop() 返回的值与 offsetLeftoffsetTop 返回的值相同。

重构函数(升级版)
/**
* @param element: 元素节点
* @param direction: offsetTop | offsetLeft
*/
function getElementOffset(element, direction) {
let actualLeft = element[direction];
let current = element.offsetParent;

while (current !== null) {
actualLeft += current[direction];
current = current.offsetParent;
}
return actualLeft;
}

2.客户端尺寸

元素的客户端尺寸(client dimensions)包含元素内容及其内边距所占用的空间。客户端尺寸只有两个相关属性:clientWidthclientHeight。其中,clientWidth 是内容区宽度加左、右内边距宽度,clientHeight 是内容区高度加上、下内边距高度。

16-2

客户端尺寸实际上就是元素内部的空间,因此不包含滚动条占用的空间。这两个属性最常用于确定浏览器视口尺寸,即检测 document.documentElementclientWidthclientHeight。这两个属性表示视口( <html><body> 元素)的尺寸。

3. 滚动尺寸

滚动尺寸(scroll dimensions),提供了元素内容滚动距离的信息。有些元素,比如 <html> 无须任何代码就可以自动滚动,而其他元素则需要使用 CSS 的 overflow 属性令其滚动。滚动尺寸相关的属性有如下 4 个。

  • scrollHeight: 没有滚动条出现时,元素内容的总高度。
  • scrollLeft: 内容区左侧隐藏的像素数,设置这个属性可以改变元素的滚动位置。
  • scrollTop: 内容区顶部隐藏的像素数,设置这个属性可以改变元素的滚动位置。
  • scrollWidth: 没有滚动条出现时,元素内容的总宽度。

16-3

scrollWidthscrollHeight 可以用来确定给定元素内容的实际尺寸。例如,<html>元素是浏览器中滚动视口的元素。因此,document.documentElement.scrollHeight 就是整个页面垂直方向的总高度。

scrollWidthscrollHeightclientWidthclientHeight 之间的关系在不需要滚动的文档上是分不清的。如果文档尺寸超过视口尺寸,则在所有主流浏览器中这两对属性都不相等,scrollWidthscrollHeight 等于文档内容的宽度,而 clientWidthclientHeight 等于视口的大小。

scrollLeftscrollTop 属性可以用于确定当前元素滚动的位置,或者用于设置它们的滚动位置。元素在未滚动时,这两个属性都等于 0。如果元素在垂直方向上滚动,则 scrollTop 会大于 0,表示元素顶部不可见区域的高度。如果元素在水平方向上滚动,则 scrollLeft 会大于 0,表示元素左侧不可见区域的宽度。因为这两个属性也是可写的,所以把它们都设置为 0 就可以重置元素的滚动位置。

下面这个函数检测元素是不是位于顶部,如果不是则把它滚动回顶部:

function scrollToTop(element) {
// 这个函数使用 scrollTop 获取并设置值。
if (element.scrollTop != 0) {
element.scrollTop = 0;
}
}

4. 确定元素尺寸

浏览器在每个元素上都暴露了 getBoundingClientRect() 方法,返回一个 DOMRect 对象,包含 6 个属性:lefttoprightbottomheightwidth。这些属性给出了元素在页面中相对于视口的位置。 16-4

遍历

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 树如图:

16-5

其中的任何节点都可以成为遍历的根节点。比如,假设以 <body> 元素作为遍历的根节点,那么接下来是 <p> 元素、<b> 元素和两个文本节点(都是 <body> 元素的后代)。但这个遍历不会到达 <html> 元素、<head> 元素,或者其他不属于 <body> 元素子树的元素。而以 document 为根节点的遍历,则可以访问到文档中的所有节点。

如图展示了以 document 为根节点的深度优先遍历。

16-6

document 开始,然后循序移动,第一个节点是 document,最后一个节点是包含" world!"的文本节点。到达文档末尾最后那个文本节点后,遍历会在 DOM 树中反向回溯。此时,第一个访问的节点就是包含" world!"的文本节点,而最后一个是 document 节点本身。NodeIterator 和 TreeWalker 都以这种方式进行遍历。

NodeIterator

NodeIterator 类型是两个类型中比较简单的,document.createNodeIterator() 是一个用于创建节点迭代器(NodeIterator)的方法。它接受三个参数,并返回一个新的节点迭代器对象,该对象可以用于遍历文档中的节点。

注意

document.createNodeIterator() 方法在某些浏览器中可以接受四个参数.第四个参数 entityReferenceExpansion 是一个可选参数,用于控制是否扩展实体引用。并非所有浏览器都支持 entityReferenceExpansion 参数。例如,Chrome 和 Firefox 支持该参数,而 Safari 和 Edge 则不支持。

  • root: 作为遍历根节点的节点。
  • filter

一个回调函数或一个具有 acceptNode() 方法的对象。从根节点开始到子树中的每个节点都会调用一次该函数或方法,哪些节点需要进入迭代节点列表等待调用则取决于 whatToShow 标志。该方法需要返回下列常量之一:NodeFilter.FILTER_ACCEPTNodeFilter.FILTER_REJECTNodeFilter.FILTER_SKIP

  • whatToShow

参数是一个位掩码,通过应用一个或多个过滤器来指定访问哪些节点。这个参数对应的常量是在 NodeFilter 类型中定义的。

常数数值描述
NodeFilter.SHOW_ALL4294967295 (即unsigned long 的最大值)显示所有节点。
NodeFilter.SHOW_COMMENT128显示 Comment 节点。
NodeFilter.SHOW_DOCUMENT256显示 Document 节点。
NodeFilter.SHOW_DOCUMENT_FRAGMENT1024显示 DocumentFragment 节点。
NodeFilter.SHOW_DOCUMENT_TYPE512显示 DocumentType 节点。。
NodeFilter.SHOW_ELEMENT1显示 Element 节点。
NodeFilter.SHOW_PROCESSING_INSTRUCTION64显示 ProcessingInstruction 节点。
NodeFilter.SHOW_TEXT4显示 Text 节点。

这些值除了 NodeFilter.SHOW_ALL 之外,都可以组合使用。比如,可以像下面这样使用按位或操作组合多个选项:

示例
const nodeIterator = document.createNodeIterator(
document.body,
NodeFilter.SHOW_ELEMENT,
node =>
node.nodeName.toLowerCase() === 'p'
? NodeFilter.FILTER_ACCEPT
: NodeFilter.FILTER_REJECT,
);
const pars = [];
let currentNode;

while ((currentNode = nodeIterator.nextNode())) {
pars.push(currentNode);
}
相同,但使用具有 acceptNode() 方法的对象
const nodeIterator = document.createNodeIterator(
document.body,
NodeFilter.SHOW_ELEMENT,
{
acceptNode(node) {
return node.nodeName.toLowerCase() === 'p'
? NodeFilter.FILTER_ACCEPT
: NodeFilter.FILTER_REJECT;
},
},
);
const pars = [];
let currentNode;

while ((currentNode = nodeIterator.nextNode())) {
pars.push(currentNode);
}

NodeIterator 的两个主要方法是 nextNode()previousNode()nextNode() 方法在 DOM子树中以深度优先方式进前一步,而 previousNode() 则是在遍历中后退一步。创建 NodeIterator对象的时候,会有一个内部指针指向根节点,因此第一次调用 nextNode() 返回的是根节点。当遍历到达 DOM 树最后一个节点时,nextNode() 返回 nullpreviousNode() 方法也是类似的。当遍历到达DOM 树最后一个节点时,调用 previousNode() 返回遍历的根节点后,再次调用也会返回 null

nextNode() 示例
<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 iterator = document.createNodeIterator(div, NodeFilter.SHOW_ELEMENT);
let node = iterator.nextNode();
while (node !== null) {
console.log(node.tagName); // 输出标签名
node = iterator.nextNode();
}
</script>
<!--
DIV
P
B
UL
LI
LI
LI
-->

如果只想遍历 <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,
);

let node = iterator.nextNode();
while (node !== null) {
console.log(node.tagName); // 输出标签名
node = iterator.nextNode();
}

nextNode()previousNode() 方法共同维护 NodeIterator 对 DOM 结构的内部指针,因此修改 DOM 结构也会体现在遍历中。

TreeWalker

TreeWalker 是 NodeIterator 的高级版。除了包含同样的 nextNode()previousNode() 方法,TreeWalker 还添加了如下在 DOM 结构中向不同方向遍历的方法。

  • parentNode(): 遍历到当前节点的父节点。
  • firstChild(): 遍历到当前节点的第一个子节点。
  • lastChild(): 遍历到当前节点的最后一个子节点。
  • nextSibling(): 遍历到当前节点的下一个同胞节点。
  • previousSibling(): 遍历到当前节点的上一个同胞节点。

TreeWalker 对象要调用 document.createTreeWalker() 方法来创建,这个方法接收与 document.createNodeIterator() 同样的参数. 因为两者很类似,所以 TreeWalker 通常可以取代 NodeIterator.

document.createTreeWalker() 参数 whatToShow , 比 document.createNodeIterator() 方法多两个参数,其它一样,如下:

常数数值描述
NodeFilter.SHOW_ATTRIBUTE0x2显示 Attr 节点。
NodeFilter.SHOW_CDATA_SECTION0x8显示 CDATASection 节点
示例
<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);

let node = walker.nextNode();

while (node !== null) {
console.log(node.tagName); // 输出标签名
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 则表示跳过该节点以及该节点的整个子树。

如果把前面示例中的过滤器函数改为返回 NodeFilter.FILTER_REJECT(而不是 NodeFilter.FILTER_SKIP),则会导致遍历立即返回,不会访问任何节点。这是因为第一个返回的元素是 <div>,其中标签名不是"li",因此过滤函数返回 NodeFilter.FILTER_REJECT,表示要跳过整个子树。因为 <div> 本身就是遍历的根节点,所以遍历会就此结束。

当然,TreeWalker 真正的威力是可以在 DOM 结构中四处游走。如果不使用过滤器,单纯使用 TreeWalker 的漫游能力同样可以在 DOM 树中访问 <li> 元素,比如:

let div = document.getElementById('div1');
let walker = document.createTreeWalker(div, NodeFilter.SHOW_ELEMENT);

walker.firstChild(); // 前往<p>
walker.nextSibling(); // 前往<ul>

let node = walker.firstChild(); // 前往第一个<li>
while (node !== null) {
console.log(node.tagName);
node = walker.nextSibling();
}

TreeWalker 类型也有一个名为 currentNode 的属性,表示遍历过程中上一次返回的节点(无论使用的是哪个遍历方法)。可以通过修改这个属性来影响接下来遍历的起点,如下面的例子所示:

let node = walker.nextNode();
console.log(node === walker.currentNode); // true
walker.currentNode = document.body; // 修改起点

相比于 NodeIterator,TreeWalker 类型为遍历 DOM 提供了更大的灵活性。

范围

为了支持对页面更细致的控制,DOM2 Traversal and Range 模块定义了范围接口。范围可用于在文档中选择内容,而不用考虑节点之间的界限。(选择在后台发生,用户是看不到的。)范围在常规 DOM操作的粒度不够时可以发挥作用。

DOM 范围

DOM2 在 Document 类型上定义了一个 createRange() 方法,暴露在 document 对象上。使用这个方法可以创建一个 DOM 范围对象,如下所示:

let range = document.createRange();

与节点类似,这个新创建的范围对象是与创建它的文档关联的,不能在其他文档中使用。然后可以使用这个范围在后台选择文档特定的部分。创建范围并指定它的位置之后,可以对范围的内容执行一些操作,从而实现对底层 DOM 树更精细的控制。

每个范围都是 Range 类型的实例,拥有相应的属性和方法。下面的属性提供了与范围在文档中位置相关的信息。

  • startContainer: 范围起点所在的节点(选区中第一个子节点的父节点)。
  • startOffset

范围起点在 startContainer 中的偏移量。如果 startContainer 是文本节点、注释节点或 CData 区块节点,则 startOffset 指范围起点之前跳过的字符数;否则,表示范围中第一个节点的索引。

  • endContainer: 范围终点所在的节点(选区中最后一个子节点的父节点)。
  • endOffset: 范围起点在 startContainer 中的偏移量(与 startOffset 中偏移量的含义相同)。
  • commonAncestorContainer: 文档中以 startContainerendContainer 为后代的最深的节点。

这些属性会在范围被放到文档中特定位置时获得相应的值。

示例
const range = document.createRange();

range.setStart(startNode, startOffset);
range.setEnd(endNode, endOffset);

一旦建立了 Range 对象,在使用它的大多数方法之前需要设置它的边界点。

简单选择

通过范围选择文档中某个部分最简单的方式,就是使用 selectNode()selectNodeContents()方法。这两个方法都接收一个节点作为参数,并将该节点的信息添加到调用它的范围。selectNode() 方法选择整个节点,包括其后代节点,而 selectNodeContents() 只选择节点的后代。

selectNode() 和 selectNodeContents() 示例
<p id="p1"><b>Hello</b> world!</p>

<script>
// 获取要选择的节点
let p1 = document.getElementById('p1');
// 创建一个 Range 对象
let range = document.createRange();
// 使用 selectNode 方法选择节点
let range1 = range.selectNode(p1);
let range2 = range.selectNodeContents(p1);
</script>

例子中的这两个范围包含文档的不同部分。range1 包含 <p> 元素及其所有后代,而 range2 包含 <b> 元素、文本节点"Hello"和文本节点" world!",

调用 selectNode() 时,startContainerendContainercommonAncestorContainer 都等于传入节点的父节点。在这个例子中,这几个属性都等于 document.bodystartOffset 属性等于传入节点在其父节点 childNodes 集合中的索引(在这个例子中,startOffset 等于 1,因为 DOM的合规实现把空格当成文本节点),而 endOffset 等于 startOffset 加 1(因为只选择了一个节点)。

let p1 = document.getElementById('p1');
let range = document.createRange();

range.selectNode(p1);

console.log(range.startContainer); // body
console.log(range.endContainer); // body
console.log(range.commonAncestorContainer); // body

console.log(range.startOffset); // 1
console.log(range.endOffset); // 2

在调用 selectNodeContents() 时,startContainerendContainercommonAncestorContainer 属性就是传入的节点,在这个例子中是 <p> 元素。startOffset 属性始终为 0,因为范围从传入节点的第一个子节点开始,而 endOffset 等于传入节点的子节点数量(node.childNodes.length),在这个例子中等于 2。

let p1 = document.getElementById('p1');
let range = document.createRange();

range.selectNodeContents(p1);

console.log(range.startContainer); // p
console.log(range.endContainer); // p
console.log(range.commonAncestorContainer); // p

console.log(range.startOffset); // 0
console.log(range.endOffset); // 2

在像上面这样选定节点或节点后代之后,还可以在范围上调用相应的方法,实现对范围中选区的更精细控制。

  • setStartBefore(refNode)

把范围的起点设置到 refNode 之前,从而让 refNode 成为选区的第一个子节点。startContainer 属性被设置为 refNode.parentNode,而 startOffset属性被设置为 refNode 在其父节点 childNodes 集合中的索引。

  • setStartAfter(refNode)

把范围的起点设置到 refNode 之后,从而将 refNode 排除在选区之外,让其下一个同胞节点成为选区的第一个子节点。startContainer 属性被设置为refNode.parentNodestartOffset 属性被设置为 refNode 在其父节点 childNodes 集合中的索引加 1。

  • setEndBefore(refNode)

把范围的终点设置到 refNode 之前,从而将 refNode 排除在选区之外、让其上一个同胞节点成为选区的最后一个子节点。endContainer 属性被设置为 refNode.parentNodeendOffset 属性被设置为 refNode 在其父节点 childNodes 集合中的索引。

  • setEndAfter(refNode)

把范围的终点设置到 refNode 之后,从而让 refNode 成为选区的最后一个子节点。endContainer 属性被设置为 refNode.parentNodeendOffset 属性被设置为 refNode 在其父节点 childNodes 集合中的索引加 1。

调用这些方法时,所有属性都会自动重新赋值。不过,为了实现复杂的选区,也可以直接修改这些属性的值。

复杂选择

要创建复杂的范围,需要使用 setStart()setEnd() 方法。这两个方法都接收两个参数:参照节点 和偏移量。对 setStart() 来说,参照节点会成为 startContainer,而偏移量会赋值给 startOffset。 对 setEnd() 而言,参照节点会成为 endContainer,而偏移量会赋值给 endOffset

操作范围

范围插入

范围折叠

范围比较

复制范围

调用范围的 cloneRange() 方法可以复制范围。这个方法会创建调用它的范围的副本:

let newRange = range.cloneRange();

新范围包含与原始范围一样的属性,修改其边界点不会影响原始范围。

清理

在使用完范围之后,最好调用 detach() 方法把范围从创建它的文档中剥离。调用 detach() 之后,就可以放心解除对范围的引用,以便垃圾回收程序释放它所占用的内存。

range.detach(); // 从文档中剥离范围
range = null; // 解除引用

这两步是最合理的结束使用范围的方式。剥离之后的范围就不能再使用了。