DOM的扩展

前端开发
2018年12月09日
250

尽管DOM作为API已经非常完善了,但为了实现更多的功能,仍然会有一些标准或专有的扩展。2008年之前,浏览器几乎所有的DOM扩展都是专有的。此后,W3C着手将一已经成为事实标准的专有扩展标准化并写入规范当中。

对DOM的两个主要扩展是Selectors API(选择符API)和HTML5.这两个扩展都源自开发社区,而将某些常见做法及API标准化一直是众望所归。此外,还有一个不那么引人瞩目的Element Traversal(元素遍历)规范,为DOM添加了一些属性。虽然前述两个主要规范(特别是HTML5)已经涵盖了大量的DOM扩展,但专有扩展依然存在。

选择符API

Selectors API(www.w3.org/TR/selectors-api)是由W3C发起制定的一个标准,致力于让浏览器原生支持CSS查询。

Selectors API Level 1 的核心是两个方法:querySelector()querySelectorAll()。在兼容的浏览器中,可以通过Document及Element类型的实例调用它们。目前已经完全支持Selectors API Level 1的浏览器器有IE8+、Firefox3.5+、Chrome和Opera10+。

querySelector()

querySelector()接收一个CSS选择符,返回与该模式匹配的第一个元素,如果没有找到则返回null。

js
// 取得body元素 var body = document.querySelector('body'); // 取得ID为'myDiv'的元素 var myDiv = document.querySelector('#myDiv'); // 取得类为'selected'的第一个元素 var selected = document.querySelector('.selected'); // 取得类为'button'的第一个图像元素 var img = document.body.querySelector('img.button');

通过Document类型调用querySelector()时,会在文档元素的范围内查找匹配的元素;而通过Element类型调用时,会在该元素的后代元素范围查找。

CSS选择符可以简单也可以复杂,视情况而定。如果传入了不被支持的选择符,会抛出错误。

querySelectorAll()

querySelectorAll()同样接收一个CSS选择符作为参数,但返回的是所有匹配的元素而不仅仅是一个元素。这个方法返回的是一个NodeList的实例。

具体来说,返回的值实际上是带有所有属性和方法的NodeList,而其底层实现则类似于一组元素的快照,而非不断对文档进行搜索的动态查询。

只要传给querySelectorAll()方法的CSS选择符有效,该方法都会返回一个NodeList对象,而不管找到多少匹配的元素。如果没找到匹配的元素,NodeList就是空的。

js
// 取得某div中所有em元素(类似于getElementsByTagName('em')) var ems = document.getElementById('myDiv').querySelectorAll('em'); // 取得类为'selected'的所有元素 var selecteds = document.querySelectorAll('.selected'); // 取得所有p元素中的所有strong元素 var strongs = document.querySelectorAll('p strong');

要取得返回的NodeList中的每一个元素,可以使用item()方法,也可以使用方括号方法。

js
var i, len, strong; for (i = 0, len = strongs.length; i < len; i++) { strong = strongs[i]; // 或者strongs.item(i) strong.className = 'important'; }

matchesSelector()

Selectors API Level 2 规范为Element类型新增了一个方法matchesSelector()。这个方法接收一个参数,即CSS2选择符,如果调用元素与该选符匹配,返回true,否则返回false.

js
if (document.body.matchesSelector('body.page1')) { // true }

在取得某个元素引用的情况下,使用这个方法能够方便地检测它是否被querySelector()querySelectorAll()方法返回。

使用这个方法最好是编写一个包装函数。

js
function matchesSelector (element, selector) { if (element.matchesSelector) { return element.matchesSelector(selector); } else if (element.msMatchesSelector) { return element.msMatchesSelector(selector); } else if (element.mozMatchesSelector) { return element.mozMatchesSelector(selector); } else { throw new Error('Not supported.'); } } if (matchesSelector(document.body, 'body.page1')) { // 执行操作 }

元素遍历

对于元素间的空格,IE9及之前版本不会返回文本节点,而其他所有浏览器都会返回文本节点。这样,就导致了在使用childNodesfirstChild等属性时的行为不一致。为了弥补这一差异,而同时又保持DOM规范不变,Element Traversal规范(www.w3.org/TR/ElementTraversal/)新定义了一组属性。

Elemen Traversal API为DOM元素添加了以下5个属性。

  • childElementCount:返回子元素(不包括文本节点和注释)的个数。
  • firstElementChild:指向第一个子元素,firstChild的元素版。
  • lastElementChild。
  • previousElementSibling:指向前一个同辈元素,previousSibling的元素版。
  • nextElementSibling。

使用这些新增的属性,跨浏览器遍历某元素的所有子元素,代码会更简洁。

js
var child = element.firstElementChild, i, len; while (child !== element.lastElementChild) { processChild(child); // 已知其是元素 child = child.nextElementSibling; }

支持Element Traversal规范的浏览器有IE9+、Firefox3.5+、Safari4+、Chorme和Opera10+。

HTML5

对于传统HTML而言,HTML5是一个叛逆。所有之前的版本对JavaScript接口的描述都不过三言两语,主要篇幅都用于定义标记,与JavaScript相关的内容一概交给DOM规范去定义。

而HTML5规范则围绕如何使用新增标记定义了大量JavaScript API。其中一些API与DOM重叠,定义了浏览器应该支持的DOM扩展。

与类相关的扩充

getElementsByClassName()

HTML5添加的getElementsByClassName()方法是最受人欢迎的一个方法,可以通过document对象及所有HTML元素调用这个方法。

getElementsByClassName()接收一个参数,即一个包含一个或多个类名的字符串,返回带有指定类的所有元素的NodeList。传入多个类名的时候,类名先后顺序不重要。

js
// 取得所有类中包含'username'和'current'的元素 var allCurrentUsernames = document.getElementsByClassName('username current'); // 取得ID为'myDiv'的元素中带有类名'selected'的所有元素 var selected = document.getElementById('myDiv').getElementsByClassName('selected');

支持getElementsByClassName()方法的浏览器有IE9+、Firefox3+、Safari3.1+、Chorme和Opera9.5+。

classList属性

HTML5新增了一种操作类名的方式,可以让操作更简单也更安全,那就是为所有元素添加classList属性。这个classList属性是新集合类型DOMTokenList的实例。与其他DOM集合类型类似,DOMTokenList有一个表示自己包含多少个元素的length属性,而要取得每个元素可以使用item()方法,也可以使用方括号语法。此外,这个新类型还定义了如下方法:

  • add(value):将给定的字符串值添加到列表中,如果值存在,就不添加了。
  • contains(value):表示列表上中否存在给定的值,存在返回true,否则返回false。
  • remove(value):从列表中删除给定的字符串。
  • toggle(value):如果列表中已经存在给定的值,则删除,否则添加

支持classList属性的浏览器有Firefox3.6+和Chrome。

js
// 删除'disabled'类 div.classList.remove('disabled'); // 添加'current'类 div.classList.add('current'); // 确定元素中是否包含既定的类名 if (div.classList.contains('bd') && !div.classList.contains('disabled')) { // ... } // 迭代类名 for (var i = 0, len = div.classList.length; i < len; i++) { doSomething(div.classList[i]); }

焦点管理

activeElement属性

HTML5也添加了辅助管理DOM焦点的功能。首先就是document.activeElement属性,这个属性始终会引用DOM中当前获得了焦点的元素。元素获得焦点的方式有页面加载、用户输入和在代码中调用focus()方法。

js
var button = document.getElementById('myButton'); button.focus(); alert(document.activeElement === button); // true

默认情况下,文档刚刚加载完,document.activeElement中保存的是document.body元素的引用。文档加载期间,document.activeElement的值为null。

hasFocus()

hasFocus()方法用于确定文档是否获得了焦点。

js
var button = document.getElementById('myButton'); button.focus(); alert(document.hasFocus()); // true

支持这两个属性的浏览器有IE4+、Firefox3+、Safari4+、Chorme和Opera8+。

HTMLDocument的变化

HTML5扩展了HTMLDocument,增加了新的功能。与HTML5中新增的其他DOM扩展类似,这些变化同样基于那些已经得到很多浏览器完美支持的专有扩展。

readyState属性

Document的readyState属性有两个可能的值:

  • loading,正在加载文档;
  • complete,已经加载完文档
js
if (document.readyState === 'complete') { // 文档已经加载完毕 }

支持readyState属性的浏览器有IE4+、Firefox3.6+、Safari+、Chorme和Opera9+。

兼容模式

document.compatMode属性是为了告诉开发人员浏览器采用了哪种渲染模式。在标准模式下,这个属性的值等于CSS1Compat,而在混杂模式下,值等于BackCompat

js
if (document.compatMode === 'CSS1Compat') { alert('正在使用标准模式'); } else { alert('正在使用混杂模式'); }

head属性

js
var head = document.head || document.getElementsByTagName('head')[0]

实现document.head属性的浏览器包括Chrome和Safari5。

字符集属性

HTML5新增了几个与文档字符集有关的属性。其中,charset属性表示文档中实际使用的字符集,也可以用来指定新字符集。默认情况下,这个属性的值为UTF-16,但可以通过<meta>元素、响应头或直接设置charset属性来修改这个值。

js
alert(document.charset); // 'UTF-16' document.charset = 'UTF-8'

另一个属性就是defaultCharset,表示根据默认浏览器及操作系统的设置,当前文档默认的字符集应该是什么。如果文档没有使用默认的字符集,那么charsetdefaultCharset属性的值可能会不一样。

js
if (document.charset !== document.defalutCharset) { // ... }

支持document.charset属性的浏览器有IE、Safari和Chrome。Firefox支持document.characterset

支持document.defaultCharset属性的浏览器有IE、Safari和Chrome。

自定义数据属性

HTML5规定可以为元素添加非标准的属性,但要添加前缀data-,目的是为元素提供与渲染无关的信息,或者提供语义信息。这些属性可以任意添加、随便命名,只要以data-开关即可。

html
<div id="myDiv" data-appId="123456" data-myname="Nicholas"></div>

添加了自定义属性之后,可以通过元素的dataset属性来访问自定义属性的值。dataset属性的值是DOMStringMap的一个实例,也就是一个名值对的映射。

js
var div = document.getElementByI('myDiv'); // 获取值 var appId = div.dataset.appId; var myname = div.dataset.myname; // 设置值 div.dataset.appId = '654321'; div.dataset.myname = 'Michael'; // 检测是否有某个值 if (div.dataset.othername) { // ... }

插入标记

innerHTML 属性

有读模式下,innerHTML属性返回与调用元素的所有子节点对应的HTML标记。在写模式下,innerHTML元素会根据指定的值创建新的DOM树,然后用这个DOM树完全替换调用元素原先的所有子节点。

html
<div id="content"> <p>This is a <strong>paragraph</strong> with a list following it.</p> <ul> <li>item 1</li> <li>item 2</li> <li>item 3</li> </ul> </div>

对于上面的<div>元素来说,它的innerHTML属性会返回下面的字符串:

html
<p>This is a <strong>paragraph</strong> with a list following it.</p> <ul> <li>item 1</li> <li>item 2</li> <li>item 3</li> </ul>

但是,不同浏览器返回的文本格式会有所不同。

在写模式下,innerHTML的值会被解析为DOM子树,替换调用元素原来的所有子节点。因为它的值被认为是HTML,所以其中的所有标签都会按照浏览器处理HTML的标准方式转换为元素(同样,这里的转换结果也因浏览器而异)。

js
div.innerHTML = 'Hello world!'; // <div id="content">Hello world!</div> div.innerHTML = 'Hello world & welcome, <b>"reader"!</b>'; // <div id="content">Hello world &amp; welcome, <b>&quot;reader&quot;!</b></div>

使用innerHTML属性也有一些限制。

在大多数浏览器中,通过innerHTML插入<script>元素并不会执行其中的脚本。

并不是所有元素都支持innerHTML属性<col>、<colgroup>、<frameset>、<head><html>、<style>、<table>、<tbody>、<thead>、<tfoot>和<tr>。此外,在IE8及更早版本中,<title>元素也没有innerHTML属性

无论什么时候,只要使用innerHTML从外部插入HTML,都应先以可靠的方式处理HTML。IE8为此提供了window.toStaticHTML()方法,这个方法接收一个参数,即一个HTML字符串,返回一个经过无害处理后的版本。

outerHTML属性

在读模式下,outerHTML返回调用它的元素及所有子节点的HTML标签。在写模式下,outerHTML会根据指定的HTML字符串创建新的DOM子树,然后用这个DOM子树完全替换调用元素。

js
<div id="content"> <p>This is a <strong>paragraph</strong> with a list following it.</p> <ul> <li>item 1</li> <li>item 2</li> <li>item 3</li> </ul> </div>

如果在<div>元素上调用outerHTML,会返回与上面相同的代码,包括<div>本身。

使用outerHTML属性以下面这种方式设置值:

js
div.outerHTML = '<p>This is a paragraph.</p>'; // 等同于 var p = document.createElement('p'); p.appendChild(document.createTextNode('This is a paragraph.)); div.parentNodes.replaceChild(p, div);

支持outerHTML属性的浏览器有IE4+、Safari4+、Chorme和Opera8+。Firefox7及之前所有版本都不支持。

insertAdjcentHTML()

插入标准的最后一个新增方式是insertAdjcentHTML()。这个方法接收两个参数:插入位置和要插入HTML文本。第一个参数必须是下列值之一:

  • ‘beforebegin’:在当前元素之前插入
  • ‘afterbegin’:在当前元素之后插入或在第一个子元素之前插入
  • ‘beforeend’:在当前元素之后插入或在最后一个子元素之后插入
  • ‘afterend’: 在当前元素之后插入
js
element.insertAdjcentHTML('beforebegin', '<p>Hello world.</p>')

支持insertAdjcentHTML()的浏览器有IE、Firefox8+、Safari、Opera和Chrome。

scrollIntoView()

scrollIntoView()可以在所有HTML元素上调用,通过滚动浏览器窗口或某个容器元素,调用元素就可以出现在视口中。如果给这个方法传true作为参数,或者不传入任何参数,那么窗口滚动之后会让调用元素的顶部与视口顶部尽可能。如果传入false作为参数,调用元素会尽可能全部出现在视口中,(可能的话,调用元素的底部会与视口底部平齐。)不过顶部不一定平齐。

js
// 让元素可见 document.forms[0].scrollIntoView();

支持scrollIntoView()方法的浏览器有IE、Firefox、Safari和Opera。

专有扩展

文档模式

IE8引入了一个新的概念叫“文档模式”(document mode)。页面的文档模式决定了可以使用什么功能。到了IE9,总共有以下4种文档模式:

  • IE5
  • IE7
  • IE8
  • IE9

要强制浏览器以某种模式渲染页面,可以使用HTTP头部信息X-UA-Compatible,或通过等价的<meta>标签来设置。

html
<meta http-equiv="X-UA-Compatible" content="IE=IEVersion">

注意,这里IE的版本(IEVersion)有以下不同的值,而且这些值并不一定与上述4种文档模式对应。

  • Edge:始终以最新的文档模式来渲染,忽略文档类型声明。
  • EmulateIE9:如果有文档类型声明,则以IE9标准模式渲染页面,否则将文档模式设置为IE5。
  • EmulateIE8:如果有文档类型声明,则以IE8标准模式渲染页面,否则将文档模式设置为IE5。
  • EmulateIE7:如果有文档类型声明,则以IE7标准模式渲染页面,否则将文档模式设置为IE5。
  • 9 强制以IE9标准模式渲染页面,忽略文档类型声明。
  • 8 强制以IE8标准模式渲染页面,忽略文档类型声明。
  • 7 强制以IE7标准模式渲染页面,忽略文档类型声明。
  • 5 将文档模式设置为IE5,忽略文档类型声明。

children属性

children属性是HTMLCollection的实例,只包含元素中的元素子节点。除此之外,children属性childNodes没有什么区别。

js
var childCount = element.children.length; var firstChild = element.children[0];

支持children属性的浏览器有IE5、Firefox3.5、Safari2(但有bug)、Safari3(完全支持)、Opera8和Chorme(所有版本)。

contains()

在实际开发中,经常需要知道某个节点是不是另一个节点的后代。contains()方法就可以做到这点,这个方法接收一个参数,即要检测的后代节点。如果是后代节点返回true,反之false。

js
// 检测body是不是html的后代 alert(document.documentElement.contains(document.body)); // true

支持这个方法的浏览器有IE、Firefox9+、Safari、Opera和Chrome。

使用DOM Level 3 compareDocumentPosition()也能够确定节点间的关系。支持这个方法的浏览器有IE9+、Firefox、Safari、Opera9.5+和Chrome。这个方法返回一个表示关系的位掩码(bitmask)。

掩码 节点关系
1 无关
2 居前
4 居后
8 包含
16 被包含
js
var result = document.documentElement.compareDocumentPosition(document.body); alert(!!(result & 16)); // 使用两个逻辑非操作符会将数值转换成布尔值。

插入文本

innerText属性

读取值时,innerText会按照由浅入深的顺序,将子文档树中所有文本拼接起来。写入值时,会删除元素的所有子节点,插入包含相应文本值的文本节点。

outerText属性

读取值时,与innertText的结果完全一样。写入值时,会替换整个元素(包括子节点)。

滚动

在HTML5将scrollIntoView()纳入规范之后,仍然还有其他几个专有方法可以在不同的浏览器器使用。下面列出的几个方法都是对HTMLElement类型的扩展,因此可以在所有元素调用。

  • scrollIntoViewIfNeeded(alignCenter):只在当前元素在视口中不可见的情况下,才滚动,让它可见。如果将参数设置为true,则表示尽量将元素显示在视口中部(垂直方向)。Safari和Chrome实现了这个方法。
  • scrollByLines(lineCount):将元素的内容滚动指定的行高,lineCount可以是正值,也可以是负值。Safari和Chrome实现了这个方法。
  • scrollByPages(pageCount):将元素的内容滚动指定的页面高度,具体高度由元素的高度决定。Safari和Chrome实现了这个方法。

注意:scrollIntoView()scrollIntoViewIfNeeded()的操作对象是元素的容器,而scrollByLines()scrollByPages()影响的则是元素自身。

js
// 将页面主体滚动5行 document.body.scrollByLines(5); // 在当前元素不可见时,让它进入浏览器的视口 document.images[0].scrollIntoViewIfNeeded(); // 将页面主体往回滚动1页 document.body.scrollByPages(-1);

由于scrollIntoView()是唯一一个所有浏览器都支持的方法,因此还是这个方法最常用。