对DOM的两个主要扩展是Selectors API(选择符API)和HTML5,还有一个不太瞩目的Element Traversal元素遍历规范为DOM添加了一些属性,另外还有一些专有扩展。
- 选择符API
- 元素遍历
- HTML5
- 专有扩展
选择符API
让浏览器原生支持css查询,原理就是所有实现这一功能的JavaScript库都会写一个基础的CSS解析器,然后再使用已有的DOM方法查询文档并找到匹配的节点。当把这个功能变成原生API后,解析和树查询操作可以在浏览器内部通过编译后的代码来完成,极大改善性能。
Selectors API Level1核心两个方法:querySelector和querySelectorAll。在兼容的浏览器中可通过Document,Element,DocumentFragment类型实例(方法继承自Document.prototype和Element.prototype和DocumentFragment.prototype)调用它们。兼容性如图,反正大部分支持。
Selectors API Level 2规范为Element类型新增了一个方法matchesSelector()。各浏览器支持不统一。
querySelector()方法:接收一个CSS选择符,返回与该模式匹配的第一个元素,如果没有找到返回null。通过Document类型调用querySelector方法时,会在文档元素范围内查找匹配的元素,通过Element类型调用querySelector方法时只会在该元素后代元素范围内查找匹配的元素。如果传入了不被支持的选择符,querySelector()会抛出错误。
//取得类为“button”的第一个图像元素 var img= document.body.querySelector('img.button');
querySelectorAll()方法:返回的所有而不仅仅是一个元素。这个方法返回的是一个NodeList类型实例而且是静态集合。querySelectorAll优点就是返回的值实际上是带有所有属性和方法的NodeList实例,而其底层实现则类似于一组元素的快照,而非不断对文档进行搜索的动态查询,这样实现可以避免使用NodeList对象通常会引起大多数性能上的问题。如果没有找到匹配的元素,返回空的NodeList实例(类数组)。如果传入了不被支持的选择符,querySelectorAll()会抛错。
//取得所有p元素中的所有strong元素 var strongs = document.querySelectorAll('p strong');
要取得返回的NodeList实例中的每一个元素,可使用item()方法,也可以使用方括号语法。
var i, len, strong; for(i = 0,len = strongs.length; i<len; i++){ strong = strongs[i];// 或strongs.item(i) strong.className = "test"; }
matchesSelector()方法:接受一个css选择符参数,如果调用元素与该选择符相匹配,返回true。
if(document.body.matchesSelector("body.page1")){ //true }
截至现在三大主流浏览器还没有支持matchesSelector方法,但IE9+msMatchesSelector()支持该方法,FF3.6+通过mozMatchesSelector()支持该方法,Safari5+和Chrome通过webkitMatchesSelector()支持该方法。这些方法均在Element.prototype上。编写一个包装函数兼容:
function matchesSelector(ele, selector){ if(ele.matchesSelector){ return ele.matchesSelector(selector); }else if(ele.msMatchesSelector){ return ele.msMatchesSelector(selector); }else if(ele.mozMatchesSelector){ return ele.mozMatchesSelector(selector); }else if(ele.webkitMatchesSelector){ return ele.webkitMatchesSelector(selector); }else{ throw new Error("Not supported"); } } if(matchesSelector(document.body, 'body.page1')){ //true }
元素遍历
对于元素间空格,<=IE8会忽略HTML元素中文本节点,其他浏览器会返回这个文本节点。这样就导致在使用childNodes和firstChild等属性时行为不一致,Element Traversal元素遍历规范定义了一组属性。元素遍历规范为DOM元素添加添加了以下5个属性:均继承自Element.prototype或Document.prototype或DocumentFragment.prototype。
Element.prototype
Document.prototype只继承这三个方法
DocumentFragment.prototype也只继承这三个方法
- childElementCount:返回子元素(不包括文本节点和注释)个数
- firstElementChild:指向第一个子元素;firstChild的元素版
- lastElementChild:指向最后一个子元素;lastChild元素版
- previousElementSibling:指向前一个同辈元素;previousSibling元素版
- nextElementSibling:指向后一个同辈元素;nextSibling元素版
跨浏览器遍历某元素所有子元素:
var child = ele.firstChild; while(child!=ele.lastChild){ if(child.nodeType == 1){ //元素节点 } child = child.nextSibling; }
使用Element Traversal代码会更简洁,支持元素遍历方法的浏览器有IE9+,FF3.5+,Safari4+,Chrome,Opera10+
var child = ele.firstElementChild; while(child != ele.lastElementChild){ //元素节点 child = child.nextElementSibling; }
HTML5
HTML5规范围绕如何使用新增标记定义大量JS API,其中一些API与DOM重叠,定义了浏览器应该支持的DOM扩展。
- 与类相关的扩充:HTML4中class属性用得越来越多,一方面可以通过它为元素添加样式,另一方面可以用它表示元素语义。自然就有很多JS代码操作CSS类,如动态修改类或搜索文档中具有给定类或一组类的元素。HTML5新增了一些API简化了css类的用法。
(1).getElementsByClassName()方法:在Document.prototype和Element.prototype上。它是通过既有的DOM功能实现的,而原生的实现具有极大的性能优势。
该方法接收一个参数,即一个包含一或多个类名的字符串,返回HTMLCollection类型集合。传入多个类名时类名的先后顺序不重要。
//取得所有类中包含“username”和“current”的元素,类名先后顺序无所谓 var eles = document.getElementsByClassName("username current"); //取得ID为“myDiv”的元素中带有类名“selected”的所有元素 var eles = document.getElementById('myDiv').getElementsByClassName('selected');
(2).classList属性:在操作类名时,需要通过className(继承自Element.prototype)属性添加删除替换类名,但缺点是className是一个字符串,所以即使只修改字符串一部分,也必须每次都设置整个字符串的值。比如删除某个类名:
//删除"user"类 var classNames = ele.className.split(/\s+/); var idx = classNames.indexOf('user'); classNames.splice(idx, 1); ele.className = classNames.join(' ');
比如添加类名需要通过字符串拼接,而且后续要检测确定不会多次添加相同类名。
HTML5新增一种操作类名的方式,为所有元素添加了classList属性(继承自Element.prototype)。这个classList属性是DOMTokenList的实例,来学习一下这个接口:原型链继承关系为:ele.classList.__proto__->DOMTokenList.prototype->Object.prototype。
add(value):将给定的字符串值添加到列表中,如果值已存在就不添加了。
contains(value):表示列表中是否存在给定的值,如果存在则返回true,否则返回false。
remove(value):从列表中删除给定的字符串,并即时反映在文档中。
toggle(value):如果列表中已经存在给定的值,删除它;如果列表中没有给定的值,添加它。并且即时反映到文档中。
有了classList属性,除非你需要全部删除所有类名,或者完全重写元素的class属性,否则也用不到classList属性。
兼容性:不太好 - 焦点管理:辅助管理DOM焦点的功能
(1).document.activeElement:始终会引用DOM中当前获得焦点的元素,继承自Document.prototype。元素获得焦点的方式有页面加载,用户输入(通常是按Tab键,能响应tab键的有带有超链接的a元素,表单元素等)和在代码中调用focus()(focus能被调用成功的有带有超链接的a元素,表单元素等,没有document.body元素,p,div元素那些)。
默认情况下文档加载期间document.activeElement为null,文档刚刚加载完成时document.activeElement中保存的是document.body元素引用。
兼容性:
(2).document.hasFocus():确定文档是否获得了焦点(可以是文档内的某些元素),继承自Document.prototype。这里要注意不要在页面加载后在控制台输入document.hasFocus()测试,会返回false的,因为此刻你的鼠标焦点在控制台并不是页面文档。所以在浏览器加载文档代码中测试:
<script type="text/javascript"> document.getElementById('a').focus(); alert(document.activeElement); alert(document.hasFocus()); </script>
- HTMLDocument的变化:HTML5扩展了HTMLDocument,增加了新功能。原型链继承关系为:document.__proto__->HTMLDocument.prototype->Document.prototype->
(1).readyState属性:IE4最早为document对象引入了readyState属性(继承自Document.prototype)。Document的readyState属性有两个可能值:
loading:正在加载文档
complete:已经加载完文档
通过它实现一个指示文档加载完成的指示器,在这个属性得到广泛支持之前要实现这样一个指示器须借助onload事件处理设置一个标签。
if(document.readyState == "complete"){ //执行操作 }
(3).head属性:作为对document.body引用文档的<body>元素的补充,HTML5引入document.head属性(继承自Document.prototype)。由于IE8不支持该属性,所以兼容来写
var head = document.head || document.getElementsByTagName('head')[0];
- 字符集属性:HTML5新增了几个与文档字符集有关的属性。均继承自Document.prototype
charset表示文档中实际使用的字符集,也可用来指定新字符集。可以通过meta标签,响应头部或直接设置charset属性修改这个值。
defaultCharset:表示根据默认浏览器及操作系统的设置,当前文档默认的字符集应该是什么 - 自定义数据属性:HTML5规定可以为元素添加非标准的属性,但要加前缀“data-”,目的是为元素提供与渲染无关的信息,或者提供语义信息。可通过元素的dataset属性访问自定义属性的值(继承自HTMLElement.prototype),该值是一个DOMStringMap的实例即一个名值对的映射。原型链继承关系为:ele.dataset.__proto__->DOMStringMap.prototype->Object.prototype
可以设置ele.dataset.attr='str'并会即时反映到页面中。当需要给元素添加一些不可见数据以便进行其他处理,就要用到自定义数据属性。 - 插入标记:在需要给文档插入大量新的HTML标记情况下,通过DOM操作仍很麻烦因为要创建一系列节点还要按正确顺序链接。相比而言使用插入标记的技术,直接插入HTML字符串不仅更简单速度也更快。以下与插入标记相关的DOM扩展已纳入H5规范:
(1).innerHTML属性:Chrome中继承自Element.prototype,IE中继承自HTMLElement.prototype
读模式:innerHTML返回与调用元素的子节点(包括元素,注释,文本节点)对应的HTML标记
写模式:innerHTML会根据指定的值创建新的DOM树,然后用这个DOM树完全替换调用元素原先的所有子节点。
注意:不同浏览器返回的文本格式会有所不同,早期IE和Opera将所有标签转换为大写形式,Safari,Chrome和FF会按原先文档中格式返回,包括空格和缩进。不要指望所有浏览器返回的innerHTML值完全相同。
在写模式下:innerHTML会被解析为DOM子树,替换调用元素原来的子节点。因为它的值默认被认为是HTML,所以其中所有标签都会按照浏览器处理HTML的标准方式转换为元素(转换结果也因浏览器而异)。如果设置的值仅是文本而没有HTML标签,那么结果就是设置纯文本。
为innerHTML设置的包含HTML字符串值与解析后innerHTML的值大不相同。例如:
这是因为为innerHTML设置HTML字符串后浏览器会将这个字符串解析为相应的DOM树,因此设置了之后再读取HTML字符串会得到与设置时不一样的结果,原因在于返回的字符串是根据原始HTML字符串创建的DOM树经过序列化之后的结果。
使用innerHTML存在一些限制,如下:
1.<script>:多数浏览器中通过innerHTML插入<script>标签并不会执行其中脚本。但<=IE9是唯一能在这种情况下执行脚本的浏览器但必须满足以下两点条件:
必须为<script>元素指定defer属性(继承自HTMLScriptElement.prototype),这个属性的用途表明脚本在执行时不会影响页面的构造,即脚本会被延迟到整个页面都解析完后再运行,相当于告诉浏览器立即下载但延迟执行。但总是先于DOMContentLoaded事件触发前执行。HTML5明确规定defer属性只适用于外部脚本文件,因此支持HTML5的实现会忽略给嵌入脚本设置的defer属性。IE4~IE7还支持对嵌入脚本的defer属性,但IE8之后版本完全支持H5规定的行为。在XHTML文档中要把defer属性设为defer="defer"。
必须位于”有作用域“元素之后,因为<script>元素被认为是无作用域的元素,也就是在页面中看不到的元素,与<style>元素或注释类似。如果通过innerHTML插入的字符串开头就是一个”无作用域的元素“,那么IE会在在解析这个字符串前先删除该元素。
div.innerHTML = "<script defer>console.log('hi')</script>";// 无效
因为此时innerHTML字符串一开始就是一个”无作用域元素“,所以这个字符串会变成空字符串。
解决方案://在<script>前面添加一个有作用域的元素 div.innerHTML = "<div> </div><script defer>alert('hi')<\/script>"; //添加文本节点 div.innerHTML = "_<script defer>alert('hi')<\/script>"; //添加一个没有结束标签的元素 div.innerHTML = "<input type=\”hidden\“><script defer>alert('hi')<\/script>"
IE9在innerHTML = "<script defer>...</script>"也能执行,不用修改作用域
2.<style>:大多浏览器都支持以直观方式通过innerHTML插入<style>元素,如:document.body.innerHTML = "<style type=\"text/css\">body{background:red;}</style>";
但在<=IE8中<style>也是一个一个没有作用域的元素,因此可以通过下面方式给它前置作用域
document.body.innerHTML = "_<script type=\"text/css\">body{background:red;}</style>"
3.并不是所有元素都支持设置innerHTML,可能有些老浏览器不支持,如上面的<=IE9无法设置,但可以取到innerHTML的值。
4.FF对内容类型为application/xhtml+xml的XHTML文档中设置innerHTML有严格限制,在XHTML文档中使用innerHTML时XHTML代码必须完全符合要求,如果代码格式不正确,设置innerHTML将会静默失败。
无论何时,使用innerHTML从外部插入HTML都应首先以可靠方式处理HTML,IE为此提供了window.toStaticHTML()方法,该方法接收一个字符串参数,返回一个经无害处理后的版本(从源HTML中删除所有脚本节点和事件处理程序属性)
(2).outerHTML属性:
读模式下:返回调用它的元素及所有子节点的HTML标签
写模式:根据指定HTML字符串创建新的DOM子树,然后用这个DOM子树完全替换调用元素。不过由于浏览器解析和解释HTL标记不同,结果也可能不同。
div.outerHTML = "<p>this is a paragraph</p>"; //等价于 var p = document.createElement('p'); p.appendChild(document.createTextNode('this is a paragraph')); div.parentNode.replaceChild(p, div);
<=FF7不支持outerHTML属性。
(3).insertAdjacentHTML()方法:Chrome继承自Element.prototype,IE继承自HTMLELement.prototype。两个参数:插入位置和要插入的HTML文本。
第一个参数必须是下列值之一:CHrome下必须为驼峰式大小写,IE无所谓都行
beforeBegin:在当前元素之前插一个紧邻的同辈元素。
afterEnd:在当前元素之后插一个紧邻的同辈元素。
afterBegin:在当前元素之下插入一个新的子元素或在第一个子元素之前插入新的子元素。
beforeEnd:在当前元素之下插入一个新的子元素或在最后一个子元素之后再插入新的子元素。
第二参数是HTML字符串(与innerHTML和outerHTML的值相同)如果浏览器无法解析该字符串会抛错
//作为前一个同辈元素插入 ele.insertAdjacentHTML('beforeBegin', "<p>Hello</p>"); //作为后一个同辈元素插入 ele.insertAdjacentHTML('afterEnd', "<p>Hello</p>"); //作为第一个子元素插入 ele.insertAdjacentHTML('afterBegin', "<p>Hello</p>"); //作为最后一个子元素插入 ele.insertAdjacentHTML('beforeBegin', "<p>Hello</p>");
兼容性:
(4).内存与性能问题:innerHTML或outerHTML替换节点会导致浏览器内存占用问题,在IE中问题更明显。
在删除带有事件处理程序或引用了其他JavaScript对象子树时就有可能导致内存占用问题。假设某个元素有一个事件处理程序(或引用了一个JavaScript对象作为属性),在使用前述某个属性将该元素从文档中删除后,元素与事件处理程序(或JavaScript对象)之间的绑定关系在内存中并没有一并删除。如果这种情况频繁出现页面占用内存熟练会明显增加。因此在使用innerHTML,outerHTML,insertAdjacentHTML方法时最好先手工删除要被替换的元素所有事件处理程序和JavaScript对象属性。
不过,innerHTML还是有优点的,在插入大量新HTML标记使用innerHTML属性与通过多次DOM操作先创建节点再指定它们之间的关系相比效率要高很多。这是因为在设置innerHTML或outerHTML时会创建一个HTML解析器。这个解析器是在浏览器级别的代码(通常是C++编写)基础上运行的,因此比执行JavaScript快的多。但不可避免,创建和销毁HTML解析器也会带来性能损失,所以最好能将设置innerHTML或outerHTML的次数控制在合理范围。例如:
for(var i=0;i<len;i++){ ul.innerHTML +='<li>'+i+'</li>'; // 要避免这种频繁操作 }
这种每次循环都设置一次innerHTML做法很低效,而且每次循环还要从innerHTML中读取一次信息,意味着每次循环要访问两次innerHTML。最好做法是单独构建字符串后再一次性将结果字符串赋给innerHTML
var itemHtml =''; for(var i=0;i<len;i++){ itemHtml += '<li>'+i+'</li>'; } ul.innerHTML = itemHtml;
- scrollIntoView()方法:HTML5采用scrollIntoView()作为标准方法,Chrome继承自Element.prototype,IE继承自HTMLElement.prototype。该方法可以在所有HTML元素上调用,通过滚动浏览器窗口或某个容器元素,调用元素就可以出现在视口中。如果给这个方法传入true作为参数或不传参数,那么该方法调用后会让调用元素顶部与视口顶部尽可能平齐。如果传入false,调用元素会尽可能全部出现在视口(可能的话调用元素底部会与视口顶部平齐)。实际上为某个元素设置焦点也会导致浏览器滚动并显示出获得焦点的元素。
专有扩展
- 文档模式:IE8引入“文档模式”的概念,页面的文档模式决定了可以使用什么功能,即文档模式决定你可以使用哪个级别css,可以在JS中使用哪些API以及如何对待文档类型(doctype)。到了IE9,公有以下四种文档模式:
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>标签来设置:
<meta http-equiv="X-UA-Compatible" content="IE=IEVersion">
这里IE的版本有以下不同的值而且这些值不一定与上述4种文档模式对应:
忽略文档类型声明的版本:
Edge:始终以最新文档模式渲染页面,对于IE8始终保持以IE8标准模式渲染页面,对于IE9始终保持以IE9标准模式渲染页面。
9:强制以IE9标准模式渲染页面。
8:强制以IE8标准模式渲染页面。
7:强制以IE7标准模式渲染页面。
5:强制将文档模式设置为IE5。
不忽略文档类型声明的版本:
EmulateIE9:如果有文档类型声明,以IE9标准模式渲染页面,否则将文档模式设置为IE5。
EmulateIE8:如果有文档类型声明,以IE8标准模式渲染页面,否则将文档模式设置为IE5。
EmulateIE7:如果有文档类型声明,以IE7标准模式渲染页面,否则将文档模式设置为IE5。<!--想让文档模式像在IE7中一样 --> <meat http-equiv="X-UA-Compatible" content="IE=EmulateIE7"> <!--不打算考虑文档类型声明,直接使用IE7标准模式--> <meat http-equiv="X-UA-Compatible" content="IE=7">
没有规定说必须在页面中设置X-UA-Compatible,默认情况下浏览器会通过文档类型声明确定是使用最佳的可用文档模式还是混杂模式。通过document.documentMode属性可知给定页面使用的是什么文档模式,这个属性只在IE中有(继承自Document.prototype)它返回文档模式的版本号。
- children属性:由于<=IE8与其他浏览器再处理文本节点中空白符有差异,因此出现children属性(Chrome继承自Document.prototype或Element.prototype,IE继承自HTMLElement.prototype),这个属性是HTMLCollection实例,只包含是元素的子节点。IE不支持document.children,但可以ele.children。在<=IE8children属性中会包含注释节点,但IE9之后版本则只返回元素节点。
- contains()方法:某个节点是不是另一个节点的后代,IE为了率先引入contains方法,一遍不通过在DOM文档树中查找即可获得这个信息。使用:祖先node.contains(后代node),如果被检测节点是后代节点返回true。该方法在Chrome中继承自Node.prototype(document和ele均可用),在IE中继承自HTMLELement.prototype(只能ele用)。
DOM Level3 compareDocumentPosition()也能确定节点间关系,继承自Node.prototype。支持这个方法的有IE9+和其他浏览器。这个方法返回表示关系的掩码
掩码 节点关系 1 无关(给定节点不在当前文档中) 2 居前(给定节点在DOM树中位于参考节点之前) 4 居后(给定节点在DOM树中位于参考节点之后) 8 包含(给定节点是参考节点祖先) 16 被包含(给定节点是参考节点后代)
为模仿contains()方法,关注掩码16,我们可以对compareDocumentPosition的结果进行按位与,以确定参考节点是否包含给定节点
var result = document.documentElement.compareDocumentPosition(document.body); console.log(result);// 20 console.log(!!(result&16));// true
result结果并不是16而是20是因为居后4和被包含16之和。20化为二进制位10100,16化为二进制10000,按位与结果为10000(16),!!两个逻辑非操作会将该数值转化为布尔值。这里说一下为什么要和16按位与,我觉得是因为按位与是都是1&1才是1,其余是0,所以当节点关系不是包含关系而是其他关系(掩码总是<16)所以小于16的数与10000怎样与都是返回0的。
写一个通用的contains函数:/* 判断某节点是否是另外一个节点的后代的兼容性代码 refNode:参照节点 targetNode:要检查的节点 */ function contains(refNode, targetNode){ if(typeof refNode.contains == 'function'){ return refNode.contains(targetNode); }else if(typeof refNode.compareDocumentPosition == 'function'){ return !!(refNode.compareDocumentPosition(targetNode)&16); }else{ var node = targetNode.parentNode; do{ if(node == refNode){ return true; }else{ node = node.parentNode; } }while(node!== null); return false; } }
- 插入文本:IE原来专有的插入标记的属性innerHTML和outerHTML已经被H5纳入规范,但innerText和outerText却没有。
(1).innerText属性:操作元素中包含的所有文本内容,包括子文档树中的文本。继承自HTMLElement.prototype。在通过innerText读取值时它会按由浅入深顺序将子文档树中所有文本拼接起来。写入值时结果会删除元素的所有子节点插入包含相应文本值的文本节点,由于不同浏览器处理空白符的方式不同因此输出的文本可能会也可能不会包含原始HTML代码中的缩进。
可以通过innerText属性过滤掉它原本的HTML标签
ele.innerText = ele.innerText;
FF低版本不支持不支持innerText,但支持作用类似的textContent属性(继承自Node.prototype),textContent是DOM Level3规定的一个属性,其他支持textContent属性的浏览器还有IE9+和其他主流浏览器。
兼容性代码:function getInnerText(ele){ return (typeof ele.textContent == 'string')?ele.textContent:ele.innerText; } function setInnerText(ele, str){ if(typeof ele.textContent == 'string'){ ele.textContent = str; }else{ ele.innerText = str; } }
<=IE8的innerText会忽略行内样式和脚本。因此避免从包含行内样式或行内脚本的DOM子树副本或片段中读取文本。
(2).outerText属性:继承自HTMLElement.prototype,作用范围扩大到包含调用它的节点,写模式下它会替换整个元素(包括子节点)
div.outerText ="xxx"; //等价于 var text = document.createTextNode('xxx'); div.parentNode.replaceChild(text, div);
- 滚动:scrollIntoViewIfNeeded(aligncenter):Chrome继承Element.prototype。只在当前元素在视口中不可见的情况下才滚动浏览器窗口或容器元素最终让它可见。如果元素在当前视口中本身可见这个方法什么也不做。如果将可选的alignCenter参数设置为true则表示尽量将元素显示在视口中部(垂直方向),只有Chrome和Safari实现了该方法。
需要注意的是scrollIntoView()和scrollIntoViewIfNeeded()的作用对象是元素的容器。
参考
《JavaScript高级程序设计》