DOM 扩展
尽管 DOM API 已经相当不错,但仍然不断有标准或专有的扩展出现,以支持更多功能。2008 年以前,大部分浏览器对 DOM 的扩展是专有的。此后,W3C 开始着手将这些已成为事实标准的专有扩展编制成正式规范。
基于以上背景,诞生了描述 DOM 扩展的两个标准:Selectors API 与 HTML5。这两个标准体现了社区需求和标准化某些手段及 API 的愿景。另外还有较小的 Element Traversal 规范,增加了一些 DOM 属性。专有扩展虽然还有,但这两个规范(特别是 HTML5)已经涵盖其中大部分。
Selectors API
Selectors API(参见 W3C 网站上的 Selectors API Level 1)是 W3C 推荐标准,规定了浏览器原生支持的 CSS 查询 API。支持这一特性的所有 JavaScript 库都会实现一个基本的 CSS 解析器,然后使用已有的 DOM 方法搜索文档并匹配目标节点。虽然库开发者在不断改进其性能,但 JavaScript 代码能做到的毕竟有限。通过浏览器原生支持这个 API,解析和遍历 DOM 树可以通过底层编译语言实现,性能也有了数量级的提升。
Selectors API Level 1 的核心是两个方法:querySelector()
和 querySelectorAll()
。在兼容浏览器中,Document 类型和 Element 类型的实例上都会暴露这两个方法。
Selectors API Level 2 规范在 Element 类型上新增了更多方法,比如 matches()
、find()
和 findAll()
。不过,目前还没有浏览器实现或宣称实现 find()
和 findAll()
。
querySelector()
querySelector()
方法返回文档中与指定选择器或选择器组匹配的第一个 Element 对象。如果找不到匹配项,则返回 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');
在 Document 上使用 querySelector()
方法时,会从文档元素开始搜索;在 Element 上使用 querySelector()
方法时,则只会从当前元素的后代中查询。
用于查询模式的 CSS 选择符可繁可简,依需求而定。如果选择符有语法错误或碰到不支持的选择符,则 querySelector()
方法会抛出错误。
在性能方面,如果你只是需要通过唯一的 ID 来查找元素,那么 getElementById
通常是首选,因为它更快速。但是,如果你需要使用更复杂的选择器来查找元素,那么 querySelector
是更适合的选择,尽管它可能稍微慢一些。
querySelectorAll()
querySelectorAll()
方法跟 querySelector()
一样,也接收一个用于查询的参数,但它会返回所有匹配的节点,而不止一个。这个方法返回的是一个 NodeList 的静态实例。
再强调一次,querySelectorAll()
返回的 NodeList 实例一个属性和方法都不缺,但它是一个静态的“快照”,而非“实时”的查询。这样的底层实现避免了使用 NodeList 对象可能造成的性能问题。
与 querySelector()
一样,querySelectorAll()
也可以在 Document、DocumentFragment 和 Element 类型上使用。
// 取得 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');
返回的 NodeList 对象可以通过 for-of
循环、item()
方法或 []
中括号语法取得个别元素。
let strongElements = document.querySelectorAll('p strong');
for (let strong of strongElements) {
strong.className = 'important';
}
for (let i = 0; i < strongElements.length; ++i) {
strongElements.item(i).className = 'important';
}
for (let i = 0; i < strongElements.length; ++i) {
strongElements[i].className = 'important';
}
与 querySelector()
方法一样,如果选择符有语法错误或碰到不支持的选择符,则 querySelectorAll()
方法会抛出错误。
matches()
matches()
方法(在规范草案中称为 matchesSelector()
)接收一个 CSS 选择符参数,如果元素匹配则该选择符返回 true
,否则返回 false
。
<ul id="birds">
<li>橙翅鹦鹉</li>
<li class="endangered">菲律宾鹰</li>
<li>大白鹈鹕</li>
</ul>
<script type="text/javascript">
var birds = document.getElementsByTagName('li');
for (var i = 0; i < birds.length; i++) {
if (birds[i].matches('.endangered')) {
console.log(birds[i].textContent + '快濒临灭绝!');
}
}
</script>
有一些浏览器使用前缀,在非标准名称 matchesSelector()
下实现了这个方法!但为了代码的可维护性和跨浏览器兼容性,建议尽可能使用标准名称 matches()
。
元素遍历
IE9 之前的版本不会把元素间的空格当成空白节点,而其他浏览器则会。这样就导致了 childNodes
和 firstChild
等属性上的差异。为了弥补这个差异,同时不影响 DOM 规范,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
)。
在支持的浏览器中,所有 DOM 元素都会有这些属性,为遍历 DOM 元素提供便利。这样开发者就不用担心空白文本节点的问题了。
举个例子,过去要以跨浏览器方式遍历特定元素的所有子元素,代码大致是这样写的:
<ul id="parent">
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
<script type="text/javascript">
let parentElement = document.getElementById('parent');
let currentChildNode = parentElement.firstChild;
let children = [];
// 没有子元素,firstChild 返回 null,跳过循环
while (currentChildNode) {
// 需要判断是否节点元素,而非文本、注释节点
if (currentChildNode.nodeType === 1) {
children.push(currentChildNode);
}
currentChildNode = currentChildNode.nextSibling;
}
console.log(children); // [li, li, li]
</script>
使用 Element Traversal 属性之后,以上代码可以简化如下:
<ul id="parent">
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
<script type="text/javascript">
let parentElement = document.getElementById('parent');
// highlight-success
let currentChildNode = parentElement.firstElementChild;
let children = [];
// highlight-success-start
while (currentChildNode) {
children.push(currentChildNode);
currentChildNode = currentChildNode.nextElementSibling;
}
// highlight-success-end
console.log(children); // [li, li, li]
</script>
HTML5
HTML5 代表着与以前的 HTML 截然不同的方向。在所有以前的 HTML 规范中,从未出现过描述 JavaScript 接口的情形,HTML 就是一个纯标记语言。JavaScript 绑定的事,一概交给 DOM 规范去定义。
然而,HTML5 规范却包含了与标记相关的大量 JavaScript API 定义。其 中有的 API 与 DOM 重合,定义了浏览器应该提供的 DOM 扩展。
CSS类扩展
自 HTML4 被广泛采用以来,Web 开发中一个主要的变化是 class 属性用得越来越多,其用处是为元素添加样式以及语义信息。自然地,JavaScript 与 CSS 类的交互就增多了,包括动态修改类名,以及根据给定的一个或一组类名查询元素,等等。为了适应开发者和他们对 class 属性的认可,HTML5 增加了一些特性以方便使用 CSS 类。
1.getElementsByClassName()
getElementsByClassName()
是 HTML5 新增的最受欢迎的一个方法,暴露在 document 对象和所有 HTML 元素上。这个方法脱胎于基于原有 DOM 特性实现该功能的 JavaScript 库,提供了性能高好的原生实现。
返回一个包含了所有指定类名的子元素的类数组对象。当在 document 对象上调用时,会搜索整个 DOM 文档,包含根节点。你也可以在任意元素上调用 getElementsByClassName()
方法,它将返回的是以当前元素为根节点,所有指定类名的子元素。
/**
* elements 是一个实时集合,包含了找到的所有元素。
* names 是一个字符串,表示要匹配的类名列表;类名通过空格分隔
*/
let elements = document.getElementsByClassName(names);
// 获取所有 class 为 'test' 的元素:
document.getElementsByClassName('test');
// 获取所有 class 同时包括 'red' 和 'test' 的元素。
document.getElementsByClassName('red test');
// 在 id 为'main'的元素的子节点中,获取所有 class 为'test'的元素
document.getElementById('main').getElementsByClassName('test');
2.classList 属性
Element.classList
是一个只读属性,返回一个元素 class 属性的动态 DOMTokenList 集合。这可以用于操作 class 集合。
与其他 DOM 集合类型一样,DOMTokenList 也有 length 属性,也可以通过 item()
或 []
中括号取得个别的元素。此外,DOMTokenList 还增加了以下方法。
-
add(value)
: 向类名列表中添加指定的字符串值 value。如果这个值已经存在,则什么也不做。 -
contains(value)
: 返回布尔值,表示给定的 value 是否存在。 -
remove(value)
: 从类名列表中删除指定的字符串值 value。 -
toggle(value,force)
:
如果类名列表中已经存在指定的 value,则删除;如果不存在,则添加。
force
可选,如果包含该值,设置后会将方法变成单向操作。如果设置为 false
,仅会删除标记列表中匹配的给定标记,且不会再度添加。如果设置为 true
,仅在标记列表中添加给定标记,且不会再度删除。
-
replace(oldToken, newToken)
可以将列表中一个已存在的 oldToken
替换为一个新 newToken
。如果第一个参数在列表中不存在, replace()
立刻返回 false
,而不会将新 token
字符串添加到列表中。
const div = document.createElement('div');
div.className = 'foo';
// 初始状态:<div class="foo"></div>
console.log(div.outerHTML);
// 使用 classList API 移除、添加类值
div.classList.remove('foo');
div.classList.add('anotherclass');
// <div class="anotherclass"></div>
console.log(div.outerHTML);
// 如果 visible 类值已存在,则移除它,否则添加它
div.classList.toggle('visible');
// 添加 force 参数,来转变函数行为 add 或 remove
div.classList.toggle('visible', i < 10);
console.log(div.classList.contains('foo'));
// 添加或移除多个类值
div.classList.add('foo', 'bar', 'baz');
div.classList.remove('foo', 'bar', 'baz');
// 使用展开语法添加或移除多个类值
const cls = ['foo', 'bar'];
div.classList.add(...cls);
div.classList.remove(...cls);
// 将类值 "foo" 替换成 "bar"
div.classList.replace('foo', 'bar');
// 迭代类名
for (let item of div.classList) {
}