重温《JavaScript高级程序设计》—12.DOM拓展
Apr 21, 2018前端js本篇内容
- Secectors API
- HTML5 DOM拓展
- 专有的DOM拓展
2008年之前,浏览器几所所有的DOM拓展都是专有的,此后W3C着手将一些已经成为事实标准的专有拓展标准化写入规范当中。
对DOM拓展的两个主要的拓展是Selectors API(选择符API)和HTML5.此外还有一个不那么引人瞩目的Element Traversal(元素遍历)规范。
选择符API
众多JavaScript库中最常用的一项功能,就是根据CSS选择符选择与某个模式匹配的DOM元素。(jQuery的核心就是通过CSS选择符查询DOM文档取得元素的应用)
Selectors API是由W3C发起制定的一个标准,致力于让浏览器原生支持CSS查询。所以实现这一功能的JavaScript库都会写一个基础的CSS解析器,然后再使用已有的DOM方法查询文档并找到匹配的节点。
Selectors API Level 1的核心是两个方法:querySelector()和querySelectorAll()。(兼容IE8+、Firefox3.5+、Chrome和Opera10+)
1.1 querySelector()方法
querySelector()接收一个CSS选择符,返回与该模式匹配的第一个元素,如果没有找到匹配的元素,返回null。
如1
2var myDiv = document.querySelector('#myDiv');
var img = myDiv.querySelector('img.select');
1.2 querySelectorAll()方法
querySelectorAll()同样接收一个CSS选择符,但返回的是一个NodeList的实例。具体来说,返回的值实际上是带有所有属性和方法的NodeList,而其底层实现则类似于一组元素的快照,而非不断对文档进行搜索的动态查询,这样实现科研避免使用NodeList对象通常会引起的大多数性能问题。
如1
2
3var imgs = document.querySelectorAll('img');
var img1 = imgs[0]; // 或imgs.item(0)
1.3 matchesSelector()方法
Selectors API Level 2规范为Element类型新增了一个方法matchesSelector()。这个方法接收一个参数,即CSS选择符,如果调用元素与该选择符匹配,返回true;否则返回false。
如1
2
3if (document.body.matchesSelector('body.page1')) {
// true
}
在取得某个元素引用的情况下,使用这个方法能够方便地检测它是否被querySelector()或querySelectorAll()方法返回。
元素遍历
对于元素间的空格,IE9及之前版本不会返回文本节点,而其他所有浏览器都会返回文本节点。这样,就导致了在使用childNodes和firstChild等属性时的行为不一致。为了弥补这一差异,而同时又保持DOM规范不变,Element Traversal规范新定义了一组属性。
- childElementCount: 返回子元素(不包括文本节点和注释)的个数;
- firstElementChild: 指向第一个子元素;firstChild的元素版;
- lastElementChild: 指向最后一个子元素;lastChild的元素版;
- previousElementSibling: 指向前一个同辈元素;previousSibling的元素版;
- nextElementSibling: 指向后一个同辈元素;nextSibling的元素版。
兼容:IE9+、Firefox3.5+、Safari4+、Chrome和Opera10+。
HTML5(DOM相关)
3.1 与类相关的扩充
HTML5新增了很多API,致力于简化CSS类的用法
3.1.1 getElementsByClassName()方法
该方法接收一个参数,即一个包含一或多个类名的字符串,返回带有指定类的所有元素的NodeList。传入多个类名时,类名的先后顺序不重要。
1 | var allItem = document.getElementsByClassName('test selected'); |
兼容:IE9+、Firefox3+、Safari3.1+、Chrome和Opera9.5+。
3.1.2 classList属性
HTMl5为所有元素添加classList属性,这个classList属性是新集合类型DOMTokenList的实例。与其他DOM集合类似,DOMTokenList有一个表示自己包含多少元素的length属性,而要取得每个元素可以使用item()方法,也可以使用方括号语法。此外,这个新类型还定义如下方法:
- add(value): 将给定的字符串值添加到列表中。如果值已经存在,则不添加;
- contains(value): 表示列表中是否存在给定的值,如果存在则返回true,否则返回false;
- remove(value): 从列表中删除给定的字符串;
- toggle(value): 如果列表中已经存在给定的值,删除它;如果列表中没有给定的值,添加它。
如1
2
3
4
5
6
7
8
9var div = document.getElementById('test');
div.classList.remove('disable');
div.classList.add('current');
div.classList.toggle('user');
if (div.classList.contains('disable')) {
console.log('disable')
}
支持classList属性的浏览器有Firefox3.6+和Chrome。
3.2 焦点管理
HTML5也添加了辅助管理DOM焦点的功能。首先就是document.activeElement属性,这个属性始终会引用DOM中当前获得了焦点的元素。元素获得焦点的方式有页面加载、用户输入(通常是通过按Tab键)和在代码中调用focus()方法。
如1
2
3var button = document.getElementById('myBtn');
button.focus();
console.log(document.activeElement === button); // true
默认情况下,文档刚刚加载完成时,document.activeElement中保存的是document.body元素的引用。文档加载期间,document.activeElement的值为null。
另外就是新增了document.hasFocus()方法,这个方法用于确定文档是否获得了焦点。
1 | var button = document.getElementById('myBtn'); |
通过检测温度是否获得了焦点,可以知道用户是不是正在与页面交互。
实现了这两个属性的浏览器包括IE4+、Firefox3+、Safari4+、Chrome和Opera8+。
3.3 HTMLDocument的变化
3.3.1 readyState属性
Dcoument的readyState属性有两个可能的值:
- loading: 正在加载文档;
- complete: 已经加载完文档。
使用document.readyState的最恰当方式,就是通过它来实现一个指示文档已经加载完成的指示器。
1 | if (document.readyState == 'complete') { |
实现readyState属性的浏览器包括IE4+、Firefox3.6+、Safari、Chrome和Opera9+。
3.3.2 兼容模式
自从IE6开始区分渲染页面的模式是标准的还是混杂的,检测页面的兼容模式就成为浏览器的必要功能。IE为此给document添加了一个名为compatMode的属性,这个属性就是为了告诉开发人员浏览器采用了那种渲染模式。在标准模式下,document.compatMode的值等于’CSS1Compat’,而在混杂模式下,document.compatMode的值等于’BackCompat’。
1 | if (document.compatMode == 'CSS1Compat') { |
后来,陆续实现这个属性的浏览器有Firefox、Safari3.1+、Opera和Chrome。
3.3.3 head属性
作为document.body引用文档的<body>
,HTML5新增了document.head属性,引用文档的<head>
元素。元素的补充,HTML5新增了document.head属性。
1 | var head = document.head || |
实现这个属性的浏览器有Chrome和Safari5+。
3.4 字符集属性
HTML5新增了几个与稳定字符集有关的属性。其中,charset属性表示文档中实际使用的字符集,也可以用来指定新字符集。默认情况下,这个属性的值为”UTF-16”。但也可以通过<meta>
元素、响应头部或直接设置charset属性。
如1
2console.log(document.charset); // 'UTF-16'
document.charset = 'UTF-8';
另一个属性是defaultCharset,表示根据默认浏览器及默认操作系统的设置,当前文档默认的字符集应该是什么。如果文档没有使用默认的字符集,那charset和defaultCharset属性的值可能不一样。
如1
2
3if (document.charset != document.defaultCharset) {
console.log('Custom character set being uesd.');
}
通过这两个属性可以得到文档使用的字符编码的具体信息,也能对字符编码进行准确性地控制。
3.5 自定义数据属性
HTML5规定可以为元素添加非标准的属性,但要添加前缀data-。目的是为元素提供与渲染无关的信息,或者语义信息。
如1
<div id="test" data-appId="123" data-thisname="test"></div>
添加了自定义属性后,可以通过元素的dataset属性来访问自定义属性的值。dataset属性的值是DOMStringMap的一个实例,也就是一个名值对的映射。在这个映射中,每个data-name形式的属性都会有一个对应的属性。
如1
2
3
4
5
6var div = document.getElementById('test');
var appid = div.dataset.appId; // 123
var thisname = div.dataset.thisname; // test
div.dataset.appId = 456;
兼容:Firefox6+和Chrome。
3.6 插入标记
在需要给文档插入大量新HTML标记的情况下,通过DOM操作非常麻烦,不仅需要创建一系列DOM节点,而且要小心地按照正确的顺序把它们连接起来。相对而言,使用插入标记的技术,直接插入HTML字符串更简单也更快。
3.6.1 innerHTML属性
在读模式下,innerHTML属性返回与调用元素的所有子节点(包括元素、注释和文本节点)对应的HTML标记。
在写模式下,innerHTML会根据指定的值创建新的DOM树,然后用这个DOM树完全替换调用元素原先的所有子节点。
如1
2
3
4
5
6<div class="m-logo_ctn">
<em class="u-icon icon-logo">
<img class="u-w100" src="images/i_logo.png">
</em>
<em class="u-icon icon-word a-fadeIn_2s a-end" style="-webkit-animation-delay: 4.5s;"></em>
</div>
对于上面的<div>
元素来说,它的innerHTML属性会返回如下字符串:1
2
3
4<em class="u-icon icon-logo">
<img class="u-w100" src="images/i_logo.png">
</em>
<em class="u-icon icon-word a-fadeIn_2s a-end" style="-webkit-animation-delay: 4.5s;"></em>
但是,不同浏览器返回的文本格式会有所不同。IE和Opera会将所有标签转换为大写形式,而Safari、Chrome和Firefox则会原原本本地按照原先文档中的格式返回HTML,包括空格和缩进。
在写模式下,innerHTML的值会被解析为DOM子树,替换调用元素原先的所有子节点。因为它的值被认为是HTML,所以其中的所有标签都会按照浏览器处理HTML的标准方式转换为元素(转换结果也因浏览器而异)。如果设置的值仅是文本而没有HTML标签,那么结果就是设置纯文本,如1
div.innerHTML = 'hello';
为innerHTML设置的包含HTML的字符串值与解析后innerHTML的值大不相同。如1
div.innerHTML = 'hello & <b>\"reader\"</b>'
以上操作得到的结果:1
<div class="m-logo_ctn">hello & <b>"reader"</b> </div>
为innerHTML设置HTML字符串后,浏览器会将这个字符串解析为相应的DOM树。因此设置了innerHTML之后,再从中读取HTML字符串,会得到与设置时不一样的结果。原因在于返回的字符串是根据原始HTML字符串创建的DOM树经过序列化之后的结果。
使用innerHTML属性也是一些限制。如大多浏览器中,通过innerHTML插入<script>
元素并不会执行其中的脚本。
大多数浏览器都支持以直观的方式通过innerHTML插入<style>
元素,如1
div.innerHTML = '<style type=\"text/css\">'body {background-color: #f00;}</style>';
但在IE8及更早版本中,<style>
也是一个“没有作用域的元素”,因此必须像下面这样给它前置一个“有用作用域的元素”1
2div.innerHTML = '_<style type=\"text/css\">'body {background-color: #f00;}</style>';
div.removeChild(div.firstChild);
并不是所有元素都支持innerHTML属性,不支持innerHTML的元素有:
<col>
、<colgroup>
、<frameset>
、<head>
、<html>
、<style>
、<table>
、<tbody>
、<thead>
、<tfoot>
、<tr>
。另外,在IE8及更早版本中,<title>
元素也没有innerHTML属性。
通过InnerHTML插入代码之前,尽可能先手工检查一下其中的文本内容。
3.6.2 outerHTML属性
在读模式下,outerHTML返回调用它的元素及所有子节点的HTML标签。
在写模式下,outerHTML会根据指定的HTML字符串创建新的DOM子树,然后用这个DOM子树完全替换调用元素。
如1
2
3
4
5
6<div class="m-logo_ctn">
<em class="u-icon icon-logo">
<img class="u-w100" src="images/i_logo.png">
</em>
<em class="u-icon icon-word a-fadeIn_2s a-end" style="-webkit-animation-delay: 4.5s;"></em>
</div>
如果在<div>
元素上调用outerHTML,会返回与上面相同的代码。包括<div>
本身。不过,由于浏览器解析和解释HTML标记的不同,结果也可能会有所不同。
使用outerHTML属性以下面这张方式设置值:1
div.outerHTML = '<p>This is an apple</p>'
结果就是新创建的<p>
元素会取代DOM树中的<div>
元素。
支持outerHTML属性的浏览器有IE4+、Safari4+、Chrome和Opera8+,FIrefox8+。
3.6.3 insertAdjacentHTML()方法
insertAdjacentHTML()方法接收两个参数:插入位置和要插入的HTML文本。其中插入位置:
- “beforebegin”: 在当前元素之前插入一个紧邻的同辈元素;
- “afterbegin”: 在当前元素之下插入一个新的子元素或在第一个元素之前再插入新的子元素;
- “beforeend”: 在当前元素之下插入一个新的子元素或在最后一个元素之后再插入新的子元素;
- “afterend”: 在当前元素之后插入一个紧邻的同辈元素;
如:1
div.inerstAdjacentHTML('beforebegin', '<p>123</p>')
3.6.4 内存与性能问题
使用本章介绍的方法替换子节点可能会导致浏览器的内存占用问题,尤其是在IE中。
在删除带有事件处理程序或引用了其他JavaScript对象子树时,就有可能导致内存占用问题。假设某个元素有一个事件处理程序(或者引用了一个JavaScript对象作为属性),在使用前述某个属性将该元素从文档树中删除后,元素与事件处理程序(或JavaScript对象)之间的绑定关系在内存中并没有一并删除。如果这种情况频繁出现,页面占用的内存数量就会明显增加。因此,在使用innerHTML、outerHTML属性和insertAdjacentHTML()方法时,最好先手工删除要被替换的元素的所有事件处理程序和JavaScript对象属性。
3.7 scrollIntoView()方法
为方便开发人员更好地控制页面滚动,在各种专有方法中,HTML5最终选择了scrollIntoView()作为标准方法。
scrollIntoView()可以在所有HTML元素上调用,通过滚动浏览器窗口或某个容器元素,调用元素就可以出现在视口中。
如果给这个方法传入true作为参数,或者不传入任何参数,那么窗口滚动之后会让调用元素的顶部与视口顶部尽可能平齐。如果传入false作为参数,调用元素会尽可能全部出现在视口中,不过顶部不一定齐平。
如
1 | document.forms[0].scrollIntoView(); |
专有拓展
虽然所有浏览器开发商都知晓坚持标准的重要性,但在发现某项功能缺失时,这些开发商都会一如既往地向DOM中添加专有拓展,以弥补功能上的不足。
4.1 文档模式
IE8引入了一个新的概念叫“文档模式”(document mode)。页面的文档模式决定了可以使用声明功能。换句话说,文档模式决定了你可以使用哪个级别的CSS,可以在JavaScript中使用哪些API。以及如何对待文档类型(doctype)。到了IE9,一共有以下4中文档模式:
- IE5: 以混杂模式渲染页面(IE5默认模式就是混杂模式)。IE8及更高版本中的新功能都无法使用。
- IE7: 以IE7标准模式渲染页面。IE8及更高版本中的新功能都无法使用。
- IE8: 以IE8标准模式渲染页面。IE8中的新功能都可以使用,因此可以使用Selectors API、更多CSS2级选择符和某些CSS3功能,还有一些HTML5的功能。不过IE9中的新功能无法使用。
- IE9: 以IE9标准模式渲染页面。IE9中的新功能都可以使用,比如ECMAScript5、完整的CSS3以及更多HTML5功能。这个文档模式是最高级的模式。
要强制浏览器以某种模式渲染页面,可以使用HTTP头部信息X-UA-Compatible,或通过等价的<meta>
标签来设置,如1
<meta http-equiv="X-UA-Compatible" content="IE=IEVersion">
其中IEVersion:
- Edge: 始终以最新的文档模式来渲染页面。忽略文档类型声明;
- EmulateIE9: 如果有文档类型声明,则以IE9标准模式渲染页面,否则将文档模式设置为IE5;
- EmulateIE8: 如果有文档类型声明,则以IE8标准模式渲染页面,否则将文档模式设置为IE5;
- EmulateIE7: 如果有文档类型声明,则以IE7标准模式渲染页面,否则将文档模式设置为IE5;
- 9: 强制以IE9标准模式渲染页面,忽略文档类型声明;
- 8: 强制以IE8标准模式渲染页面,忽略文档类型声明;
- 7: 强制以IE7标准模式渲染页面,忽略文档类型声明;
- 5: 强制以IE5标准模式渲染页面,忽略文档类型声明;
如,要让文档模式在IE7中一样1
<meta http-equiv="X-UA-Compatible" content="IE=EmulateIE7">
如果不打算考虑文档类型声明,而直接使用IE7标准模式1
<meta http-equiv="X-UA-Compatible" content="IE=7">
通过document.documentMode属性可以知道给定页面使用的是什么文档模式。
4.2 children属性
由于IE9之前的版本与其他浏览器在处理文本节点中的空白符时有差异,因此就出现了children属性。这个属性是HTMLCollection的实例,只包含元素中同样还是元素的子节点。除此之外,children属性与childNodes没有什么区别。
兼容:IE5+、Firefox3.5、Safari2、Opera8和Chrome。
4.3 contains()方法
经常需要知道某个节点是不是另一个节点的后代。IE为此率先引入了contains()方法,以便不通过在DOM文档树中查找即可获得这个信息。调用contains()方法的应该是祖先节点,也就是搜索开始的节点,这个方法接收一个参数,即要检测的后代节点。如果被检测的节点是后代节点,该方法返回true,否则返回false。
如1
console.log(document.documentElement.contains(document.body)); // true
兼容:IE、Firefox9+、Safari、Opera和Chrome。
使用DOM Level3 compareDocumentPosition()也能够确定节点间的关系。支持这个方法的浏览器有IE9+、Firefox、Safari、Opera9.5+和Chrome。该方法返回一个表示该关系的位掩码(bitmask):
- 1: 无关(给定的节点不在当前文档中);
- 2: 居前(给定的节点在DOM树中位于参考节点之前);
- 4: 居后(给定的节点在DOM树中位于参考节点之后);
- 8: 包含(给定的节点是参考节点的祖先);
- 16: 被包含(给定的节点是参考节点的后代);
如:1
2var result = document.documentElement.compareDocumentPosition(document.body);
console.log(!!(result & 16))
4.4 插入文本
4.4.1 innerText属性
通过innerText属性可以操作元素中包含的所有文本内容,包括子文档树中的文本。在通过innerText读取值时,它会按照由浅入深的顺序,将子文档树中的所有文本拼接起来。
在通过innerText写入值时,结果会删除元素的所有子节点,插入包含相应文本值的文本节点。
如1
2
3
4
5
6
7
8
9<div class="m-logo_ctn">
<p>word1</p>
<ul>
<li>word2</li>
<li>word3</li>
<li>word4 <span>word4_1</span></li>
<li>word5</li>
</ul>
</div>
innerText属性会返回下列字符串:1
2
3
4
5
6
7"word1
word2
word3
word4 word4_1
word5
"
由于不同浏览器处理空白符的方式不同,因此输出的文本可能会也可能不会包含原始HTML代码中的缩进。
如1
div.innerText = 'hello & <b>\"reader\"</b>'
结果:1
hello & <b>"reader"!</b>
兼容:IE4+、Safari3+、Opera8+和Chrome。Firefox虽然不支持innerText,但支持作用类似的textContent属性。textContent是DOM Level 3 规定的属性,兼容:IE9+、Safari3+、Opera10+和Chrome。1
2
3
4
5
6
7
8
9
10function getInnerText(element) {
return (typeof element.textContent == 'string' ?
element.textContent : element.innerText)
}
function setInnerText (element, text) {
if (typeof element.textContent == 'string') {
element.textContent = text;
} else element.innerText = text;
}
4.4.2 outerText属性
1 | div.outerText = 'hello'; |
其作用相当于1
2var text = document.createTextNode('hello');
div.parentNode.replaceChild(text, div);
4.5 滚动
- scrollIntoViewIfNeeded(alignCenter): 只在当前元素在视口中不可见的情况下,才滚动浏览器窗口或容器元素,最终让它可见。如果当前元素在视口中可见,这个方法什么也不做。如果将可选的alignCenter参数设置为true,则表示尽量将元素显示在视口中部(垂直方向)。Safari和Chrome实现了这个方法。
- scrollByLines(lineCount): 将元素的内容滚动指定的行高,lineCount值可以是正值,也可以是负值。Safari和Chrome实现了这个方法。
- scrollByPages(pageCount): 将元素的内容滚动指定的页面高度,具体高度由元素的高度决定。Safari和Chrome实现了这个方法。
注意:scrollIntoView()和scrollIntoViewIfNeeded()的作用对象是元素的容器,而scrollByPages()和scrollByLines()影响的则是元素本身。
如1
2
3document.body.scrollByLines(5); // 将页面主题滚动5行
document.images[0].scrollIntoViewIfNeeded(); // 在当前元素不可见的时候,让它进入浏览器的视口
document.body.scrollByPages(-1); // 将页面主体往回滚动1页
温习:
- querySelector()、querySelectorAll()、matchesSelector();
- 元素遍历的几个属性;
- getElementsByClassName()/classList、activeElement、readyState、head、charset/defaultCharset、dataset、innerHTML/outerHTML、insertAdjacentHTML()、innerText/outerText;
- 文档模式;
- children属性;
- contains()方法、compareDocumentPosition()方法;
- 滚动相关方法;
(完)
Author
My name is Micheal Wayne and this is my blog.
I am a front-end software engineer.
Contact: michealwayne@163.com