js高级程序设计(十一)DOM扩展
选择符API
Selectors API是由W3C 发起制定的一个标准,致力于让浏览器原生支持CSS 查询。
Selectors API Level 1 的核心是两个方法:querySelector()和querySelectorAll()。在兼容的浏览器中,可以通过Document 及Element 类型的实例调用它们。目前已完全支持Selectors API Level 1的浏览器有IE 8+、Firefox 3.5+、Safari 3.1+、Chrome 和Opera 10+。
querySelector()方法
querySelector()方法接收一个CSS 选择符,返回与该模式匹配的第一个元素,如果没有找到匹配的元素,返回null。
//取得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 类型调用querySelector()方法时,只会在该元素后代元素的范围内查找匹配的元素。
querySelectorAll()方法
querySelectorAll()方法接收的参数是一个CSS 选择符,返回的是一个NodeList 的实例。
与querySelector()类似,能够调用querySelectorAll()方法的类型包括Document、DocumentFragment 和Element。
//取得某<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()方法,也可以使用方括号语法
var i, len, strong; for (i=0, len=strongs.length; i < len; i++){ strong = strongs[i]; //或者strongs.item(i) strong.className = "important"; }
matchesSelector()方法
matchesSelector()接收一个参数,即CSS 选择符,如果调用元素与该选择符匹配,返回true;否则,返回false。
if (document.body.matchesSelector("body.page1")){ //true }
在取得某个元素引用的情况下,使用这个方法能够方便地检测它是否会被querySelector()或querySelectorAll()方法返回。
截至2011 年年中,还没有浏览器支持matchesSelector()方法;不过,也有一些实验性的实现。IE 9+通过msMatchesSelector()支持该方法,Firefox 3.6+通过mozMatchesSelector()支持该方法,Safari 5+和Chrome 通过webkitMatchesSelector()支持该方法。因此,如果你想使用这个方法,最好是编写一个包装函数。
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 if (element.webkitMatchesSelector){ return element.webkitMatchesSelector(selector); } else { throw new Error("Not supported."); } } if (matchesSelector(document.body, "body.page1")){ //执行操作 }
元素遍历
对于元素间的空格,IE9 及之前版本不会返回文本节点,而其他所有浏览器都会返回文本节点。这样,就导致了在使用childNodes 和firstChild 等属性时的行为不一致。为了弥补这一差异,而同时又保持DOM规范不变,Element Traversal 规范新定义了一组属性。
- childElementCount:返回子元素(不包括文本节点和注释)的个数。
- firstElementChild:指向第一个子元素;firstChild 的元素版。
- lastElementChild:指向最后一个子元素;lastChild 的元素版。
- previousElementSibling:指向前一个同辈元素;previousSibling 的元素版。
- nextElementSibling:指向后一个同辈元素;nextSibling 的元素版。
过去,要跨浏览器遍历某元素的所有子元素,需要像下面这样写代码。
var i, len, child = element.firstChild; while(child != element.lastChild){ if (child.nodeType == 1){ //检查是不是元素 processChild(child); } child = child.nextSibling; }
而使用Element Traversal 新增的元素,代码会更简洁。
var i, len, child = element.firstElementChild; while(child != element.lastElementChild){ processChild(child); //已知其是元素 child = child.nextElementSibling; }
支持Element Traversal 规范的浏览器有IE 9+、Firefox 3.5+、Safari 4+、Chrome 和Opera 10+。
HTML5
与类相关的扩充
HTML5 新增了很多API,致力于简化CSS 类的用法。
1. getElementsByClassName()方法
getElementsByClassName()方法接收一个参数,即一个包含一或多个类名的字符串,返回带有指定类的所有元素的NodeList。传入多个类名时,类名的先后顺序不重要。
//取得所有类中包含"username"和"current"的元素,类名的先后顺序无所谓 var allCurrentUsernames = document.getElementsByClassName("username current"); //取得ID 为"myDiv"的元素中带有类名"selected"的所有元素 var selected = document.getElementById("myDiv").getElementsByClassName("selected");
因为返回的对象是NodeList,所以使用这个方法与使用getElementsByTagName()以及其他返回NodeList 的DOM 方法都具有同样的性能问题。
支持getElementsByClassName()方法的浏览器有IE 9+、Firefox 3+、Safari 3.1+、Chrome 和Opera 9.5+。
2. classList 属性
在操作类名时,需要通过className 属性添加、删除和替换类名。因为className 中是一个字符串,所以即使只修改字符串一部分,也必须每次都设置整个字符串的值。
<div class="bd user disabled">...</div> //删除"user"类 //首先,取得类名字符串并拆分成数组 var classNames = div.className.split(/\s+/); //找到要删的类名 var pos = -1, i, len; for (i=0, len=classNames.length; i < len; i++){ if (classNames[i] == "user"){ pos = i; break; } } //删除类名 classNames.splice(i,1); //把剩下的类名拼成字符串并重新设置 div.className = classNames.join(" ");
HTML5 新增了一种操作类名的方式,可以让操作更简单也更安全,那就是为所有元素添加classList 属性。这个classList 属性是新集合类型DOMTokenList 的实例。与其他DOM 集合类似,DOMTokenList 有一个表示自己包含多少元素的length 属性,而要取得每个元素可以使用item()方法,也可以使用方括号语法。还有如下方法
- add(value):将给定的字符串值添加到列表中。如果值已经存在,就不添加了。
- contains(value):表示列表中是否存在给定的值,如果存在则返回true,否则返回false。
- remove(value):从列表中删除给定的字符串。
- toggle(value):如果列表中已经存在给定的值,删除它;如果列表中没有给定的值,添加它。
//删除"disabled"类 div.classList.remove("disabled"); //添加"current"类 div.classList.add("current"); //切换"user"类 div.classList.toggle("user"); //确定元素中是否包含既定的类名 if (div.classList.contains("bd") && !div.classList.contains("disabled")){ //执行操作 ) //迭代类名 for (var i=0, len=div.classList.length; i < len; i++){ doSomething(div.classList[i]); }
有了classList 属性,除非你需要全部删除所有类名,或者完全重写元素的class 属性,否则也就用不到className 属性了。
支持classList 属性的浏览器有Firefox 3.6+和Chrome。
焦点管理
document.activeElement 属性,这个属性始终会引用DOM 中当前获得了焦点的元素。元素获得焦点的方式有页面加载、用户输入(通常是通过按Tab 键)和在代码中调用focus()方法。
var button = document.getElementById("myButton"); button.focus(); alert(document.activeElement === button); //true
默认情况下,文档刚刚加载完成时,document.activeElement 中保存的是document.body 元素的引用。文档加载期间,document.activeElement 的值为null。
另外就是新增了document.hasFocus()方法,这个方法用于确定文档是否获得了焦点。
var button = document.getElementById("myButton"); button.focus(); alert(document.hasFocus()); //true
通过检测文档是否获得了焦点,可以知道用户是不是正在与页面交互。
查询文档获知哪个元素获得了焦点,以及确定文档是否获得了焦点,这两个功能最重要的用途是提高Web 应用的无障碍性。无障碍Web 应用的一个主要标志就是恰当的焦点管理,而确切地知道哪个元素获得了焦点是一个极大的进步,至少我们不用再像过去那样靠猜测了。
实现了这两个属性的浏览器的包括IE 4+、Firefox 3+、Safari 4+、Chrome 和Opera 8+。
HTMLDocument的变化
1. readyState 属性
Document 的readyState 属性有两个可能的值
- loading,正在加载文档;
- complete,已经加载完文档。
if (document.readyState == "complete"){ //执行操作 }
支持readyState 属性的浏览器有IE4+、Firefox 3.6+、Safari、Chrome 和Opera 9+。
2. 兼容模式
自从IE6 开始区分渲染页面的模式是标准的还是混杂的,检测页面的兼容模式就成为浏览器的必要功能。IE 为此给document 添加了一个名为compatMode 的属性,这个属性就是为了告诉开发人员浏览器采用了哪种渲染模式。就像下面例子中所展示的那样,在标准模式下,document.compatMode 的值等于"CSS1Compat",而在混杂模式下,document.compatMode 的值等于"BackCompat"。
if (document.compatMode == "CSS1Compat"){ alert("Standards mode"); } else { alert("Quirks mode"); }
3. head 属性
HTML5 新增了document.head 属性,引用文档的<head>元素。
var head = document.head || document.getElementsByTagName("head")[0];
实现document.head 属性的浏览器包括Chrome 和Safari 5。
字符集属性
charset 属性表示文档中实际使用的字符集,也可以用来指定新字符集。默认情况下,这个属性的值为"UTF-16",但可以通过<meta>元素、响应头部或直接设置charset 属性修改这个值。
alert(document.charset); //"UTF-16" document.charset = "UTF-8";
另一个属性是defaultCharset,表示根据默认浏览器及操作系统的设置,当前文档默认的字符集应该是什么。如果文档没有使用默认的字符集,那charset 和defaultCharset 属性的值可能会不一样
if (document.charset != document.defaultCharset){ alert("Custom character set being used."); }
支持document.charset 属性的浏览器有IE 、Firefox 、Safari 、Opera 和Chrome 。支持document.defaultCharset 属性的浏览器有IE、Safari 和Chrome。
自定义数据属性
HTML5 规定可以为元素添加非标准的属性,但要添加前缀data-,目的是为元素提供与渲染无关的信息,或者提供语义信息。这些属性可以任意添加、随便命名,只要以data-开头即可。
<div id="myDiv" data-appId="12345" data-myname="Nicholas"></div>
添加了自定义属性之后,可以通过元素的dataset 属性来访问自定义属性的值。dataset 属性的值是DOMStringMap 的一个实例,也就是一个名值对儿的映射。在这个映射中,每个data-name 形式的属性都会有一个对应的属性,只不过属性名没有data-前缀(比如,自定义属性是data-myname,那映射中对应的属性就是myname)。
//本例中使用的方法仅用于演示 var div = document.getElementById("myDiv"); //取得自定义属性的值 var appId = div.dataset.appId; var myName = div.dataset.myname; //设置值 div.dataset.appId = 23456; div.dataset.myname = "Michael"; //有没有"myname"值呢? if (div.dataset.myname){ alert("Hello, " + div.dataset.myname); }
在编写本书时,支持自定义数据属性的浏览器有Firefox 6+和Chrome。
插入标记
需要给文档插入大量新HTML 标记的情况下,通过DOM操作仍然非常麻烦。相对而言,使用插入标记的技术,直接插入HTML 字符串不仅更简单,速度也更快。
1. innerHTML 属性
在读模式下,innerHTML 属性返回与调用元素的所有子节点(包括元素、注释和文本节点)对应的HTML 标记。在写模式下,innerHTML 会根据指定的值创建新的DOM树,然后用这个DOM树完全替换调用元素原先的所有子节点。
设置了innerHTML 之后,可以像访问文档中的其他节点一样访问新创建的节点。
使用innerHTML 属性也有一些限制。在大多数浏览器中,通过innerHTML 插入<script>元素并不会执行其中的脚本。IE8 及更早版本是唯一能在这种情况下执行脚本的浏览器,但必须满足一些条件。
一是必须为<script>元素指定defer 属性,二是<script>元素必须位于(微软所谓的)“有作用域的元素”(scoped element)之后。“无作用域的元素”(NoScope element),也就是在页面中看不到的元素,与<style>元素或注释类似。
因此,在IE8中插入script标签的例子如下:
div.innerHTML = "<script defer>alert('hi');<\/script>"; //无效 div.innerHTML = "_<script defer>alert('hi');<\/script>"; div.innerHTML = "<div> </div><script defer>alert('hi');<\/script>"; div.innerHTML = "<input type=\"hidden\"><script defer>alert('hi');<\/script>";
IE8中的style元素也和script元素类似,是无作用域的元素。
2. outerHTML 属性
在读模式下,outerHTML 返回调用它的元素及所有子节点的HTML 标签。在写模式下,outerHTML会根据指定的HTML 字符串创建新的DOM 子树,然后用这个DOM子树完全替换调用元素。
支持outerHTML 属性的浏览器有IE4+、Safari 4+、Chrome 和Opera 8+。Firefox 7 及之前版本都不支持outerHTML 属性。
3. insertAdjacentHTML()方法
这个方法最早也是在IE 中出现的,它接收两个参数:插入位置和要插入的HTML 文本。第一个参数必须是下列值之一:
- "beforebegin",在当前元素之前插入一个紧邻的同辈元素;
- "afterbegin",在当前元素之下插入一个新的子元素或在第一个子元素之前再插入新的子元素;
- "beforeend",在当前元素之下插入一个新的子元素或在最后一个子元素之后再插入新的子元素;
- "afterend",在当前元素之后插入一个紧邻的同辈元素。
//作为前一个同辈元素插入 element.insertAdjacentHTML("beforebegin", "<p>Hello world!</p>"); //作为第一个子元素插入 element.insertAdjacentHTML("afterbegin", "<p>Hello world!</p>"); //作为最后一个子元素插入 element.insertAdjacentHTML("beforeend", "<p>Hello world!</p>"); //作为后一个同辈元素插入 element.insertAdjacentHTML("afterend", "<p>Hello world!</p>");
支持insertAdjacentHTML()方法的浏览器有IE、Firefox 8+、Safari、Opera 和Chrome。
4. 内存与性能问题
使用本节介绍的方法替换子节点可能会导致浏览器的内存占用问题。假设某个元素有一个事件处理程序(或者引用了一个JavaScript 对象作为属性),在使用前述某个属性将该元素从文档树中删除后,元素与事件处理程序(或JavaScript 对象)之间的绑定关系在内存中并没有一并删除。如果这种情况频繁出现,页面占用的内存数量就会明显增加。因此,在使用innerHTML、outerHTML 属性和insertAdjacentHTML()方法时,最好先手工删除要被替换的元素的所有事件处理程序和JavaScript 对象属性。
for (var i=0, len=values.length; i < len; i++){ ul.innerHTML += "<li>" + values[i] + "</li>"; //要避免这种频繁操作!! } //用下面的方法效率高 var itemsHtml = ""; for (var i=0, len=values.length; i < len; i++){ itemsHtml += "<li>" + values[i] + "</li>"; } ul.innerHTML = itemsHtml;
scrollIntoView()方法
scrollIntoView()可以在所有HTML 元素上调用,通过滚动浏览器窗口或某个容器元素,调用元素就可以出现在视口中。如果给这个方法传入true 作为参数,或者不传入任何参数,那么窗口滚动之后会让调用元素的顶部与视口顶部尽可能平齐。如果传入false 作为参数,调用元素会尽可能全部出现在视口中,(可能的话,调用元素的底部会与视口顶部平齐。)不过顶部不一定平齐。
//让元素可见 document.forms[0].scrollIntoView();
当页面发生变化时,一般会用这个方法来吸引用户的注意力。实际上,为某个元素设置焦点也会导致浏览器滚动并显示出获得焦点的元素。
支持scrollIntoView()方法的浏览器有IE、Firefox、Safari 和Opera。
专有扩展
文档模式
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 中的新功能都可以使用,比如ECMAScript 5、完整的CSS3以及更多HTML5 功能。这个文档模式是最高级的模式。
要理解IE8 及更高版本的工作原理,必须理解文档模式。
要强制浏览器以某种模式渲染页面,可以使用HTTP 头部信息X-UA-Compatible,或通过等价的<meta>标签来设置:
<meta http-equiv="X-UA-Compatible" content="IE=IEVersion">
注意,这里IE 的版本(IEVersion)有以下一些不同的值,而且这些值并不一定与上述4 种文档模式对应。
-
Edge:始终以最新的文档模式来渲染页面。忽略文档类型声明。对于IE8,始终保持以IE8 标准模式渲染页面。对于IE9,则以IE9 标准模式渲染页面。
- EmulateIE9:如果有文档类型声明,则以IE9 标准模式渲染页面,否则将文档模式设置为IE5。
- EmulateIE8:如果有文档类型声明,则以IE8 标准模式渲染页面,否则将文档模式设置为IE5。
- EmulateIE7:如果有文档类型声明,则以IE7 标准模式渲染页面,否则将文档模式设置为IE5。
- 9:强制以IE9 标准模式渲染页面,忽略文档类型声明。
- 8:强制以IE8 标准模式渲染页面,忽略文档类型声明。
- 7:强制以IE7 标准模式渲染页面,忽略文档类型声明。
- 5:强制将文档模式设置为IE5,忽略文档类型声明。
通过document.documentMode 属性可以知道给定页面使用的是什么文档模式。这个属性是IE8中新增的,它会返回使用的文档模式的版本号。
children属性
这个属性是HTMLCollection 的实例,只包含元素中同样还是元素的子节点。除此之外,children 属性与childNodes 没有什么区别,即在元素只包含元素子节点时,这两个属性的值相同。
var childCount = element.children.length; var firstChild = element.children[0];
支持children 属性的浏览器有IE5、Firefox 3.5、Safari 2(但有bug)、Safari 3(完全支持)、Opera8和Chrome(所有版本)。IE8 及更早版本的children 属性中也会包含注释节点,但IE9 之后的版本则只返回元素节点。
contains()方法
调用contains()方法的应该是祖先节点,也就是搜索开始的节点,这个方法接收一个参数,即要检测的后代节点。如果被检测的节点是后代节点,该方法返回true;否则,返回false。
contains()方法已经被DOM4标准采用。
alert(document.documentElement.contains(document.body)); //true
支持contains()方法的浏览器有IE、Firefox 9+、Safari、Opera 和Chrome。
使用DOM Level 3 compareDocumentPosition()也能够确定节点间的关系。支持这个方法的浏览器有IE9+、Firefox、Safari、Opera 9.5+和Chrome。如前所述,这个方法用于确定两个节点间的关系,返回一个表示该关系的位掩码( bitmask)。下表列出了这个位掩码的值。
为模仿contains()方法,应该关注的是掩码16。可以对compareDocumentPosition()的结果执行按位与,以确定参考节点(调用compareDocumentPosition()方法的当前节点)是否包含给定的节点(传入的节点)。
var result = document.documentElement.compareDocumentPosition(document.body); alert(!!(result & 16));
使用一些浏览器及能力检测,就可以写出如下所示的一个通用的contains 函数:
function contains(refNode, otherNode){ if (typeof refNode.contains == "function" && (!client.engine.webkit || client.engine.webkit >= 522)){ return refNode.contains(otherNode); } else if (typeof refNode.compareDocumentPosition == "function"){ return !!(refNode.compareDocumentPosition(otherNode) & 16); } else { var node = otherNode.parentNode; do { if (node === refNode){ return true; } else { node = node.parentNode; } } while (node !== null); return false; } }
插入文本
IE 原来专有的插入标记的属性innerHTML 和outerHTML 已经被HTML5 纳入规范。但另外两个插入文本的专有属性则没有这么好的运气。这两个没有被HTML5 看中的属性是innerText和outerText。
1. innerText 属性
通过innertText 属性可以操作元素中包含的所有文本内容,包括子文档树中的文本。在通过innerText 读取值时,它会按照由浅入深的顺序,将子文档树中的所有文本拼接起来。在通过innerText 写入值时,结果会删除元素的所有子节点,插入包含相应文本值的文本节点。
<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>元素而言,其innerText 属性会返回下列字符串:
This is a paragraph with a list following it. Item 1 Item 2 Item 3
使用innerText 属性设置这个<div>元素的内容,则只需一行代码:
div.innerText = "Hello world!";
执行这行代码后,页面的HTML 代码就会变成如下所示。
<div id="content">Hello world!</div>
设置innerText 属性移除了先前存在的所有子节点,完全改变了DOM子树。此外,设置innerText属性的同时,也对文本中存在的HTML 语法字符(小于号、大于号、引号及和号)进行了编码。
div.innerText = "Hello & welcome, <b>\"reader\"!</b>";
<div id="content">Hello & welcome, <b>"reader"!</b></div>
可以通过innerText 属性过滤掉HTML 标签。方法是将innerText 设置为等于innerText,这样就可以去掉所有HTML 标签。
div.innerText = div.innerText;
执行这行代码后,就用原来的文本内容替换了容器元素中的所有内容(包括子节点,因而也就去掉了HTML 标签)。
支持innerText 属性的浏览器包括IE4+、Safari 3+、Opera 8+和Chrome。Firefox 虽然不支持innerText,但支持作用类似的textContent 属性。textContent 是DOM Level 3 规定的一个属性,其他支持textContent 属性的浏览器还有IE9+、Safari 3+、Opera 10+和Chrome。
为了确保跨浏览器兼容,有必要编写一个类似于下面的函数来检测可以使用哪个属性。
function 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; } }
2. outerText 属性
在读取文本值时,outerText 与innerText 的结果完全一样。但在写模式下,outerText 就完全不同了:outerText 不只是替换调用它的元素的子节点,而是会替换整个元素(包括子节点)。
div.outerText = "Hello world!"; //这行代码实际上相当于如下两行代码 var text = document.createTextNode("Hello world!"); div.parentNode.replaceChild(text, div);
支持outerText 属性的浏览器有IE4+、Safari 3+、Opera 8+和Chrome。由于这个属性会导致调用它的元素不存在,因此并不常用。
滚动
下面列出的几个方法都是对HTMLElement 类型的扩展,因此在所有元素中都可以调用。
-
scrollIntoViewIfNeeded(alignCenter):只在当前元素在视口中不可见的情况下,才滚动浏览器窗口或容器元素,最终让它可见。如果当前元素在视口中可见,这个方法什么也不做。如果将可选的alignCenter 参数设置为true,则表示尽量将元素显示在视口中部(垂直方向)。Safari 和Chrome 实现了这个方法。
-
scrollByLines(lineCount):将元素的内容滚动指定的行高,lineCount 值可以是正值,也可以是负值。Safari 和Chrome 实现了这个方法。
-
scrollByPages(pageCount):将元素的内容滚动指定的页面高度,具体高度由元素的高度决定。Safari 和Chrome 实现了这个方法。
由于scrollIntoView()是唯一一个所有浏览器都支持的方法,因此还是这个方法最常用。