# DOM 扩展

两个标准:Selectors API 与 HTML5。

# 1、Selectors API

JavaScript 库中最流行的一种能力就是根据 CSS 选择符的模式匹配 DOM 元素。比如,jQuery 就完全以 CSS 选择符查询 DOM 获取元素引用,而不是使用 getElementById() 和 getElementsByTagName()。

Selectors API(参见 W3C 网站上的 Selectors API Level 1)是 W3C 推荐标准,规定了浏览器原生支持的 CSS 查询 API。支持这一特性的所有 JavaScript 库都会实现一个基本的 CSS 解析器,然后使用已有的 DOM 方法搜索文档并匹配目标节点。

Selectors API Level 1 的核心是两个方法:querySelector() 和 querySelectorAll()。

Selectors API Level 2 规范在 Element 类型上新增了更多方法,比如 matches()、find()和 findAll()。不过,目前还没有浏览器实现或宣称实现 find() 和 findAll()。

# 1.1 querySelector()

方法接收 CSS 选择符参数,返回匹配该模式的第一个后代元素,如果没有匹配项则返回 null

// 取得<body>元素
let body = document.querySelector("body"); 
// 取得 ID 为"myDiv"的元素
let myDiv = document.querySelector("#myDiv"); 
// 取得类名为"selected"的第一个元素
let selected = document.querySelector(".selected"); 
// 取得类名为"button"的图片
let img = document.body.querySelector("img.button"); 
1
2
3
4
5
6
7
8

# 1.2 querySelectorAll()

返回所有匹配的节点,而不止一个。这个方法返回的是一个 NodeList 的静态实例

// 取得 ID 为"myDiv"的<div>元素中的所有<em>元素
let ems = document.getElementById("myDiv").querySelectorAll("em"); 
// 取得所有类名中包含"selected"的元素
let selecteds = document.querySelectorAll(".selected"); 
// 取得所有是<p>元素子元素的<strong>元素
let strongs = document.querySelectorAll("p strong"); 
1
2
3
4
5
6

# 1.3 matches()

matches() 方法(在规范草案中称为 matchesSelector())接收一个 CSS 选择符参数,如果元素匹配则该选择符返回 true,否则返回 false。

if (document.body.matches("body.page1")){ 
 // true 
} 
1
2
3

# 2、元素遍历

W3C 通过新的 Element Traversal 规范定义了一组新属性。

Element Traversal API 为 DOM 元素添加了 5 个属性:

  • childElementCount,返回子元素数量(不包含文本节点和注释);
  • firstElementChild,指向第一个 Element 类型的子元素(Element 版 firstChild);
  • lastElementChild,指向最后一个 Element 类型的子元素(Element 版 lastChild);
  • previousElementSibling,指向前一个 Element 类型的同胞元素( Element 版 previousSibling);
  • nextElementSibling,指向后一个 Element 类型的同胞元素(Element 版 nextSibling)
let parentElement = document.getElementById('parent'); 
let currentChildElement = parentElement.firstElementChild;
// 没有子元素,firstElementChild 返回 null,跳过循环
while (currentChildElement) { 
  // 这就是元素节点,做相应处理
  processChild(currentChildElement); 
  if (currentChildElement === parentElement.lastElementChild) { 
    break; 
  } 
  currentChildElement = currentChildElement.nextElementSibling; 
} 
1
2
3
4
5
6
7
8
9
10
11

# 3、HTML5

# 3.1 CSS 类扩展

  • getElementsByClassName() 类名中包含相应类的元素的 NodeList
  • classList 操作类名
// 取得所有类名中包含"username"和"current"元素
// 这两个类名的顺序无关紧要
let allCurrentUsernames = document.getElementsByClassName("username current"); 
// 取得 ID 为"myDiv"的元素子树中所有包含"selected"类的元素
let selected = document.getElementById("myDiv").getElementsByClassName("selected"); 

// classList 用法

<div class="bd user disabled">...</div>

// 要删除"user"类
let targetClass = "user"; 
// 把类名拆成数组
let classNames = div.className.split(/\s+/); 
// 找到要删除类名的索引
let idx = classNames.indexOf(targetClass); 
// 如果有则删除
if (idx > -1) { 
 classNames.splice(i,1); 
} 
// 重新设置类名
div.className = classNames.join(" "); 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

classList 还支持如下方法:

  • add(value),向类名列表中添加指定的字符串值 value。如果这个值已经存在,则什么也不做。
  • contains(value),返回布尔值,表示给定的 value 是否存在。
  • remove(value),从类名列表中删除指定的字符串值 value。
  • toggle(value),如果类名列表中已经存在指定的 value,则删除;如果不存在,则添加。
div.classList.add("user");
div.classList.contains("user");
div.classList.remove("user");
div.classList.toggle("user");
1
2
3
4

# 3.2 焦点管理

HTML5 增加了辅助 DOM 焦点管理的功能。

document.activeElement,始终包含当前拥有焦点的 DOM 元素。页面加载时,可以通过用户输入(按 Tab 键或代码中使用 focus() 方法)让某个元素自动获得焦点。

let button = document.getElementById("myButton"); 
button.focus(); 
console.log(document.activeElement === button); // true
1
2
3

默认情况下,document.activeElement 在页面刚加载完之后会设置为 document.body。而在页面完全加载之前,document.activeElement 的值为 null。

document.hasFocus(),该方法返回布尔值,表示文档是否拥有焦点:

let button = document.getElementById("myButton"); 
button.focus(); 
console.log(document.hasFocus()); // true 
1
2
3

# 3.3 HTMLDocument 扩展

(1)readyState 属性

document.readyState 属性有两个可能的值:

  • loading,表示文档正在加载;
  • complete,表示文档加载完成。
if (document.readyState == "complete") { 
 // 执行操作
} 
1
2
3

(2)compatMode 属性

检测页面的渲染模式

标准模式下 document.compatMode 的值是"CSS1Compat",而在混杂模式下,document.compatMode 的值是"BackCompat"

if (document.compatMode == "CSS1Compat"){ 
  console.log("Standards mode"); 
} else { 
  console.log("Quirks mode"); 
}
1
2
3
4
5

(3)head 属性

HTML5 增加了 document.head 属性,指向文档的<head>元素。

let head = document.head;
1

# 3.4 字符集属性

characterSet 属性表示文档实际使用的字符集,也可以用来指定新字符集。这个属性的默认值是"UTF-16",但可以通过<meta>元素或响应头,以及新增的 characterSeet 属性来修改。

console.log(document.characterSet); // "UTF-16" 
document.characterSet = "UTF-8";
1
2

# 3.5 自定义数据属性

HTML5 允许给元素指定非标准的属性,使用前缀 data- ,不包含与渲染、元素的语义信息等。除了前缀,自定义属性对命名是没有限制,data- 后可以任意字符串。

<div id="myDiv" data-appId="12345" data-myname="Nicholas"></div>
1

定义了自定义数据属性后,可以通过元素的 dataset 属性来访问。dataset 属性是一个 DOMStringMap 的实例,包含一组键/值对映射。

let div = document.getElementById("myDiv"); 
// 取得自定义数据属性的值
let appId = div.dataset.appId; 
let myName = div.dataset.myname; 

// 设置自定义数据属性的值
div.dataset.appId = 23456; 
div.dataset.myname = "Michael";
1
2
3
4
5
6
7
8

# 3.6 插入标记

批量处理节点,提高读取和写入的效率。

(1)innerHTML 属性

在读取 innerHTML 属性时,会返回元素所有后代的 HTML 字符串,包括元素、注释和文本节点。

在写入 innerHTML 时,则会根据提供的字符串值以新的 DOM 子树替代元素中原来包含的所有节点。

let div = document.getElementById("myDiv"); 
console.log(div.innerHTML); // 读取
div.innerHTML = "Hello world!"; // 写入
1
2
3

在所有现代浏览器中,通过 innerHTML 插入的<script>标签是不会执行的。而在 IE8 及之前的版本中,只要这样插入的<script>元素指定了 defer 属性,且<script>之前是“受控元素”(scoped element),那就是可以执行的。<script>元素与<style>或注释一样,都是“非受控元素”(NoScope element),也就是在页面上看不到它们。IE 会把 innerHTML 中从非受控元素开始的内容都删掉,

(2)outerHTML 属性

读取 outerHTML 属性时,会返回元素本身(及所有后代元素)的 HTML 字符串。在写入 outerHTML 属性时,调用它的元素会被传入的 HTML 字符串经解释之后生成的 DOM 子树取代。

<ul id="content"> 
  <li>Item 1</li> 
  <li>Item 2</li> 
  <li>Item 3</li> 
</ul>
<script>
let content = document.getElementById("content");
console.log(content.outerHTML); // 返回包含ul标签以及所有子元素

content.outerHTML = "<p>This is a paragraph.</p>"; // 此操作将ui标签替换为p标签
</script>
1
2
3
4
5
6
7
8
9
10
11

(3)insertAdjacentHTML() 与 insertAdjacentText()

接收两个参数,第一个参数必须是下列值中的一个:

  • "beforebegin",插入当前元素前面,作为前一个同胞节点;
  • "afterbegin",插入当前元素内部,作为新的子节点或放在第一个子节点前面;
  • "beforeend",插入当前元素内部,作为新的子节点或放在最后一个子节点后面;
  • "afterend",插入当前元素后面,作为下一个同胞节点。

注意这几个值是不区分大小写的。第二个参数会作为 HTML 字符串解析(与 innerHTML 和 outerHTML 相同)或者作为纯文本解析(与 innerText 和 outerText 相同)。

(4)内存与性能问题

使用本节介绍的方法替换子节点可能在浏览器(特别是 IE)中导致内存问题。比如,如果被移除的子树元素中之前有关联的事件处理程序或其他 JavaScript 对象(作为元素的属性),那它们之间的绑定关系会滞留在内存中。如果这种替换操作频繁发生,页面的内存占用就会持续攀升。在使用 innerHTML、outerHTML 和 insertAdjacentHTML() 之前,最好手动删除要被替换的元素上关联的事件处理程序和 JavaScript 对象。

限制使用 innerHTML 和 outerHTML 的次数

for (let value of values){ 
 ul.innerHTML += '<li>${value}</li>'; // 每次迭代都要读取一次 innerHTML,设置一次 innerHTML
} 

// 建议一次性修改
ul.innerHTML = values.map(value => '<li>${value}</li>').join('');
1
2
3
4
5
6

# 3.7 scrollIntoView()

scrollIntoView() 方法存在于所有 HTML 元素上,可以滚动浏览器窗口或容器元素以便包含元素进入视口。这个方法的参数如下:

  • alignToTop 是一个布尔值。不传参数等同于 alignToTop 为 true。
    • true:窗口滚动后元素的顶部与视口顶部对齐。
    • false:窗口滚动后元素的底部与视口底部对齐。
  • scrollIntoViewOptions 是一个选项对象。
    • behavior:定义过渡动画,可取的值为"smooth"和"auto",默认为"auto"。
    • block:定义垂直方向的对齐,可取的值为"start"、"center"、"end"和"nearest",默认为 "start"。
    • inline:定义水平方向的对齐,可取的值为"start"、"center"、"end"和"nearest",默认为 "nearest"。
// 确保元素可见
document.forms[0].scrollIntoView(); 
// 同上
document.forms[0].scrollIntoView(true); 
document.forms[0].scrollIntoView({block: 'start'}); 
// 尝试将元素平滑地滚入视口
document.forms[0].scrollIntoView({behavior: 'smooth', block: 'start'});
1
2
3
4
5
6
7

# 4、专有扩展

# 4.1 children 属性

IE9 之前的版本与其他浏览器在处理空白文本节点上的差异导致了 children 属性的出现。children 属性是一个 HTMLCollection,只包含元素的 Element 类型的子节点。如果元素的子节点类型全部是元素类型,那 children 和 childNodes 中包含的节点应该是一样的。

可以像下面这样使用 children 属性:

let childCount = element.children.length; 
let firstChild = element.children[0];
1
2

# 4.2 contains() 方法

contains() 方法应该在要搜索的祖先元素上调用,参数是待确定的目标节点。contains() 方法应该在要搜索的祖先元素上调用,参数是待确定的目标节点。

console.log(document.documentElement.contains(document.body)); // true 
1

使用 DOM Level 3 的 compareDocumentPosition() 方法也可以确定节点间的关系。

掩 码 节点关系
0x1 断开(传入的节点不在文档中)
0x2 领先(传入的节点在 DOM 树中位于参考节点之前)
0x4 随后(传入的节点在 DOM 树中位于参考节点之后)
0x8 包含(传入的节点是参考节点的祖先)
0x10 被包含(传入的节点是参考节点的后代)
let result = document.documentElement.compareDocumentPosition(document.body); 
console.log(!!(result & 0x10));
1
2

IE9 及之后的版本,以及所有现代浏览器都支持 contains() 和 compareDocumentPosition() 方法。

# 4.3 插入标记

(1)innerText 属性

innerText 属性对应元素中包含的所有文本内容,无论文本在子树中哪个层级。

在用于读取值时,innerText 会按照深度优先的顺序将子树中所有文本节点的值拼接起来。

在用于写入值时,innerText 会移除元素的所有后代并插入一个包含该值的文本节点。

使用方法和 innerHTML 一致。

(2)outerText 属性

outerText 与 innerText 是类似的,只不过作用范围包含调用它的节点。

使用方法和 outerHTML 一致。

# 4.4 滚动

scrollIntoViewIfNeeded()作 为 HTMLElement 类型的扩展可以在所有元素上调用。scrollIntoViewIfNeeded(alingCenter) 会在元素不可见的情况下,将其滚动到窗口或包含窗口中,使其可见;如果已经在视口中可见,则这个方法什么也不做。如果将可选的参数 alingCenter 设置为 true,则浏览器会尝试将其放在视口中央。

// 如果不可见,则将元素可见
document.images[0].scrollIntoViewIfNeeded(); 
1
2

# 5、DOM2和DOM3

DOM1(DOM Level 1)主要定义了 HTML 和 XML 文档的底层结构。

DOM2(DOM Level 2)和 DOM3(DOM Level 3)在这些结构之上加入更多交互能力,提供了更高级的 XML 特性。

  • 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。

# 5.1 内嵌窗格

DOM2 HTML 给 HTMLIFrameElement(即<iframe>,内嵌窗格)类型新增了一个属性,叫 contentDocument。这个属性包含代表子内嵌窗格中内容的 document 对象的指针。

let iframe = document.getElementById("myIframe"); 
let iframeDoc = iframe.contentDocument; 
1
2

contentDocument 属性是 Document 的实例,拥有所有文档属性和方法,因此可以像使用其他 HTML 文档一样使用它。还有一个属性 contentWindow,返回相应窗格的 window 对象,这个对象上有一个 document 属性。所有现代浏览器都支持 contentDocument 和 contentWindow 属性。

跨源访问子内嵌窗格的 document 对象会受到安全限制。如果内嵌窗格中加载了不同域名(或子域名)的页面,或者该页面使用了不同协议,则访问其 document 对象会抛出错误。

# 5.2 样式

HTML 中的样式有 3 种定义方式:外部样式表(通过<link>元素)、文档样式表(使用<style>元素)和元素特定样式(使用 style 属性)。

(1) 存取元素样式

CSS 属性 JavaScript 属性
background-image style.backgroundImage
color style.color
display style.display
font-family style.fontFamily

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

let myDiv = document.getElementById("myDiv");
// 设置背景颜色
myDiv.style.backgroundColor = "red";
// 修改大小
myDiv.style.width = "100px";
myDiv.style.height = "200px";
1
2
3
4
5
6

style 属性的信息和修改方法:

  • cssText,包含 style 属性中的 CSS 代码。
  • length,应用给元素的 CSS 属性数量。
  • parentRule,表示 CSS 信息的 CSSRule 对象(下一节会讨论 CSSRule 类型)。
  • getPropertyCSSValue(propertyName),返回包含 CSS 属性 propertyName 值的 CSSValue 对象(已废弃)。
  • getPropertyPriority(propertyName),如果 CSS 属性 propertyName 使用了!important 则返回"important",否则返回空字符串。
  • getPropertyValue(propertyName),返回属性 propertyName 的字符串值。
  • item(index),返回索引为 index 的 CSS 属性名。
  • removeProperty(propertyName),从样式中删除 CSS 属性 propertyName。
  • setProperty(propertyName, value, priority),设置 CSS 属性 propertyName 的值为 value,priority 是"important"或空字符串。
myDiv.style.cssText = "width: 25px; height: 100px; background-color: green"; 
console.log(myDiv.style.cssText);
1
2

(2)计算样式

getComputedStyle() 方法返回一个 CSSStyleDeclaration 对象(与 style 属性的类型一样),包含元素的计算样式。

let myDiv = document.getElementById("myDiv"); 
let computedStyle = document.defaultView.getComputedStyle(myDiv, null); 
console.log(computedStyle.backgroundColor); // "red" 
console.log(computedStyle.width); // "100px"
1
2
3
4

(3)操作样式表

CSSStyleSheet 类型表示 CSS 样式表,包括使用<link>元素和通过<style>元素定义的样式表。注意,这两个元素本身分别是 HTMLLinkElement 和 HTMLStyleElement。CSSStyleSheet 类型是一个通用样式表类型,可以表示以任何方式在 HTML 中定义的样式表。

let sheet = null; 
for (let i = 0, len = document.styleSheets.length; i < len; i++) { 
  sheet = document.styleSheets[i]; 
  console.log(sheet.href); 
} 
1
2
3
4
5

# 5.3 元素尺寸

(1)偏移尺寸

包含元素在屏幕上占用的所有视觉空间。元素在页面上的视觉空间由其高度和宽度决定,包括所有内边距、滚动条和边框(但不包含外边距)。

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

所有这些偏移尺寸属性都是只读的,每次访问都会重新计算。因此,应该尽量减少查询它们的次数。比如把查询的值保存在局量中,就可以避免影响性能。

(2)客户端尺寸

元素的客户端尺寸(client dimensions)包含元素内容及其内边距所占用的空间。客户端尺寸只有两个相关属性:clientWidth 和 clientHeight。

  • clientWidth 是内容区宽度加左、右内边距宽度
  • clientHeight 是内容区高度加上、下内边距高度。
客户端尺寸

客户端尺寸实际上就是元素内部的空间,因此不包含滚动条占用的空间。这两个属性最常用于确定浏览器视口尺寸,即检测 document.documentElement 的 clientWidth 和 clientHeight。

这两个属性表示视口(<html><body>元素)的尺寸

与偏移尺寸一样,客户端尺寸也是只读的,而且每次访问都会重新计算。

(3)滚动尺寸

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

  • scrollHeight,没有滚动条出现时,元素内容的总高度。
  • scrollLeft,内容区左侧隐藏的像素数,设置这个属性可以改变元素的滚动位置。
  • scrollTop,内容区顶部隐藏的像素数,设置这个属性可以改变元素的滚动位置。
  • scrollWidth,没有滚动条出现时,元素内容的总宽度。
滚动尺寸
// 数检测元素是不是位于顶部,如果不是则把它滚动回顶部
function scrollToTop(element) { 
  if (element.scrollTop != 0) { 
    element.scrollTop = 0; 
  } 
} 
1
2
3
4
5
6

(4)确定元素尺寸

浏览器在每个元素上都暴露了 getBoundingClientRect() 方法,返回一个 DOMRect 对象,包含 6 个属性:left、top、right、bottom、height 和 width。这些属性给出了元素在页面中相对于视口的位置。

元素尺寸

# 5.4 遍历

DOM2 Traversal and Range 模块定义了两个类型用于辅助顺序遍历 DOM 结构。这两个类型 —— NodeIterator 和 TreeWalker —— 从某个起点开始执行对 DOM 结构的深度优先遍历。

DOM 遍历是对 DOM 结构的深度优先遍历,至少允许朝两个方向移动(取决于类型)。遍历以给定节点为根,不能在 DOM 中向上超越这个根节点。

# 5.5 范围

(1)DOM 范围

createRange() 使用这个方法可以创建一个 DOM 范围对象

let range = document.createRange();
1

每个范围都是 Range 类型的实例,拥有相应的属性和方法。

  • startContainer,范围起点所在的节点(选区中第一个子节点的父节点)。
  • startOffset,范围起点在 startContainer 中的偏移量。如果 startContainer 是文本节点、注释节点或 CData 区块节点,则 startOffset 指范围起点之前跳过的字符数;否则,表示范围中第一个节点的索引。
  • endContainer,范围终点所在的节点(选区中最后一个子节点的父节点)。
  • endOffset,范围起点在 startContainer 中的偏移量(与 startOffset 中偏移量的含义相同)。
  • commonAncestorContainer,文档中以startContainer和endContainer为后代的最深的节点。

(2)简单选择

selectNode() 和 selectNodeContents() 方法都接收一个节点作为参数,并将该节点的信息添加到调用它的范围。selectNode() 方法选择整个节点,包括其后代节点,而 selectNodeContents() 只选择节点的后代。

<!DOCTYPE html> 
<html> 
 <body> 
 <p id="p1"><b>Hello</b> world!</p> 
 </body> 
</html> 
1
2
3
4
5
6
let range1 = document.createRange(), 
 range2 = document.createRange(), 
 p1 = document.getElementById("p1"); 
range1.selectNode(p1); 
range2.selectNodeContents(p1);
1
2
3
4
5
上次更新: 3/9/2022, 6:56:39 PM