15.1 Selectors API

JavaScript库中最流行的一种能力:根据CSS选择符的模式匹配DOM元素。
比如,jQuery就完全以CSS选择符查询DOM获取元素引用,而不是使用getElementById()和getElementsByTagName()。
Selectors API(参见W3C网站上的Selectors API Level 1)是W3C推荐标准,规定了浏览器原生支持的CSS查询API。
支持这一特性的所有JavaScript库都会实现一个基本的CSS解析器,然后使用已有的DOM方法搜索文档并匹配目标节点。
虽然库开发者在不断改进其性能,但JavaScript代码能做到的毕竟有限。
通过浏览器原生支持这个API,解析和遍历DOM树可以通过底层编译语言实现,性能也有了数量级的提升。
Selectors API Level 1的核心是两个方法:querySelector()和querySelectorAll()。
Selectors API Level 2规范在Element类型上新增了更多方法,比如matches()、find()和findAll()。
但是,目前还没有浏览器实现或宣称实现find()和findAll()。

15.1.1 querySelector()

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

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

在Document上使用querySelector()方法时,会从文档元素开始搜索;
在Element上使用querySelector()方法时,则只会从当前元素的后代中查询。
用于查询模式的CSS选择符可繁可简,依需求而定。
如果选择符有语法错误或碰到不支持的选择符,则querySelector()方法会抛出错误。

15.1.2 querySelectorAll()

querySelectorAll()方法跟querySelector()一样,也接收一个用于查询的参数,但它会返回所有匹配的节点,而不止一个。
返回的是一个NodeList的静态实例。
querySelectorAll()返回的NodeList实例一个属性和方法都不缺,但它是一个静态的“快照”,而非“实时”的查询。
这样的底层实现避免了使用NodeList对象可能造成的性能问题。
以有效CSS选择符调用querySelectorAll()都会返回NodeList,无论匹配多少个元素都可以。
如果没有匹配项,则返回空的NodeList实例。
与querySelector()一样,querySelectorAll()也可以在Document、DocumentFragment和Element类型上使用。下面是几个例子:

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

返回的NodeList对象可以通过for-of循环、item()方法或中括号语法取得个别元素。

  1. let strongElements = document.querySelectorAll('p strong');
  2. // 以下3个循环的效果一样
  3. for (let strong of strongElements) {
  4. strong.className = 'important';
  5. }
  6. for (let i = 0; i < strongElements.length; ++i) {
  7. strongElements.item(i).className = 'important';
  8. }
  9. for (let i = 0; i < strongElements.length; ++i) {
  10. strongElements[i].className = 'important';
  11. }

与querySelector()方法一样,若选择符有语法错误或碰到不支持的选择符,则querySelector-All()方法会抛出错误。

15.1.3 matches()

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

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

使用这个方法可方便地检测:某个元素会不会被querySelector()或querySelectorAll()方法返回。
所有主流浏览器都支持matches()。
Edge、Chrome、Firefox、Safari和Opera完全支持,IE9~11及一些移动浏览器支持带前缀的方法。

15.2 元素遍历

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元素提供便利。
这样开发者就不用担心空白文本节点的问题。
举个例子,过去要以跨浏览器方式遍历特定元素的所有子元素,代码大致是这样写的:

  1. let parentElement = document.getElementById('parent');
  2. let currentChildNode = parentElement.firstChild;
  3. // 没有子元素,firstChild返回null, 跳过循环
  4. while (currentChildNode) {
  5. if (currentChildNode.nodeType === 1) {
  6. // 如果有元素节点,则做相应处理
  7. processChild(currentChildNode);
  8. }
  9. if (currentChildNode === parentElement.lastChild) {
  10. break;
  11. }
  12. currentChildNode = currentChildNode.nextSibling;
  13. }

使用Element Traversal属性之后,以上代码可以简化如下:

  1. let parentElement = document.getElementById('parent');
  2. let currentChildElement = parentElement.firstElementChild;
  3. // 没有子元素,firstChild返回null, 跳过循环
  4. while (currentChildNode) {
  5. if (currentChildNode.nodeType === 1) {
  6. // 这就是元素节点,则做相应处理
  7. processChild(currentChildElement);
  8. }
  9. if (currentChildElement === parentElement.lastElementChild) {
  10. break;
  11. }
  12. currentChildElement = currentChildElement.nextElementSibling;
  13. }

IE9及以上版本,以及所有现代浏览器都支持Element Traversal属性。

15.4 专有扩展

15.4.1 children属性

children属性是一个HTMLCollection,只包含元素的Element类型的子节点。
如果元素的子节点类型全部是元素类型,那children和childNodes中包含的节点应该是一样的。

15.4.2 contains()方法

确定一个元素是不是另一个元素的后代,使用contains()方法,在不遍历DOM的情况下获取这个信息。
contains()方法在要搜索的祖先元素上调用,参数是待确定的目标节点。
如果目标节点是被搜索节点的后代,contains()返回true,否则返回false。

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

15.4.3 插入标记

1.innerText属性

innerText属性对应元素中包含的所有文本内容,无论文本在子树中哪个层级。
用于读取值时,innerText会按照深度优先的顺序,将子树中所有文本节点的值拼接起来。
用于写入值时,innerText会移除元素的所有后代,并插入一个包含该值的文本节点

2.outerText属性

outerText与innerText类似,只不过作用范围包含调用它的节点。
读取文本值时,outerText与innerText实际上会返回同样的内容。
写入文本值时,outerText不止会移除所有后代节点,而是会替换整个元素。
outerText是一个非标准的属性,而且也没有被标准化的前景。因此,不推荐依赖这个属性实现重要的操作。

15.4.4 滚动

虽然HTML5把scrollIntoView()标准化了,但不同浏览器中仍然有其他专有方法。
比如,scrollIntoViewIfNeeded()作为HTMLElement类型的扩展可以在所有元素上调用。scrollIntoViewIfNeeded(alingCenter)会在元素不可见的情况下,将其滚动到窗口或包含窗口中,使其可见;
如果已经在视口中可见,则这个方法什么也不做。如果将可选的参数alingCenter设置为true,则浏览器会尝试将其放在视口中央。
Safari、Chrome和Opera实现了这个方法。