10.2 DOM扩展
根据 W3C 对 DOM 的要求,浏览器可以自行为其添加属性和方法,以增强其功能。新增的部分功能是为了向后兼容,而另一些功能则是根据开发人员的反馈,针对解决常见的问题而添加的。无论出于什么原因,扩展 DOM 的做法已经相当普遍,而且对开发也有极大的好处。
10.2.1 呈现模式
随着 IE6 开始区分标准模式和混杂模式,确定浏览器处于何种模式的需求也就应运而生。IE 为 document 对象添加了一个名为 compatMode 的属性,这个属性的唯一使命就是标识浏览器处于什么模式。如下面的例子所示,如果是标准模式,则 document.compatMode 的值等于 “CSS1Compat” ,如果是混杂模式,则 document.compatMode 的值等于 "BackCompat" 。
if (document.compatMode == "CSS1Compat") {
alert("Standards mode");
} else {
alert("Quirks mode");
}
后来,Firefox、Opera 和 Chrome 都实现了这个属性。Safari 从3.1 版开始也实现了 document.compatMode 。
IE8又为 document 对象引入了一个名为 documentMode 的新属性,其用法如下面的例子所示。这是因为 IE8 有3种不同的呈现模式,而引入这个属性正是为了分辨这些模式。这个属性的值如果是 5 则表示混杂模式 (即 IE5 模式);如果是7,则表示 IE7 仿真模式;如果是8,则表示 IE8 标准模式。
if (document.documentMode > 7) {
alert("IE 8+ Standards Mode");
}
关于在将来的新版浏览器中,这个属性的值会如何变化,微软并没有给出太多说明。因此,如果你想测试的是 IE8 标准模式,那么最好测试这个属性的值是不是大于 7 ,而不是直接测试它是不是等于 8 ,以防将来这个属性的值可能会发生变化。
10.2.2 滚动
DOM 规范没有就如何滚动页面区域这个问题做出规定。为此,各种浏览器都实现了相应的方法,用于以不同方式控制滚动。这些方法都是作为 HTMLElement 类型的扩展存在的,因此可以在所有元素上使用。
- scrollIntoView(alignWithTop): 滚动浏览器窗口或容器元素,以便在视口 (viewport) 中看到当前元素。如果 alignWithTop 的值为 true,或者省略它,那么窗口会尽可能滚动到自身顶部与元素顶部平齐。所有浏览器都实现了这个方法。
- scrollIntoViewIfNeeded(alignCenter): 只有当前元素在视口中不可见的情况下,才滚动浏览器窗口或容器元素,最终让当前元素可见。如果当前元素在视口中可见,这个方法什么也不做。如果将可选的 alignCenter 参数设置为 true,则表示尽量将元素显示在视口中部 (垂直方向)。Safari 和 Chrome 实现了这个方法。
- scrollByLines(lineCount): 将元素的内容滚动指定的行数的高度,lineCount值可是正值也可以是负值。Safari 和 Chrome 实现了这个方法。
- scrollByPages(pageCount): 将元素的内容滚动指定的页面的高度,具体高度由元素的高度决定。Safari 和 Chrome 实现了这个方法。
10.2.3 children 属性
10.2.4 contains() 方法
10.2.5 操作内容
这两个函数都接受一个元素,然后检查这个元素是不是有 textContent 属性。如果有,那么 typeof element.textContent 应该是 “string” ;如果没有,那么这两个函数就会改为使用 innerText。可以像下面这样调用这两个函数:
setInnerText(div, "Hello world");
alert(getInnerText(div)); // "Hello world!"
使用这两个函数可以确保在不同的浏览器中使用正确的属性。
2.innerHTML 属性
innerHTML 与 innerText 在很多方面都很相似。在读取信息时,innerHTML 返回当前元素所有子节点的 HTML 表现,包括元素、注释及文本节点。在写入信息时,innerHTML 会按照指定的值创建新的 DOM 子树,并以该子树替换当期元素的所有子节点。提到 innerHTML 与 innerText 之间最主要的区别,无非就是 innerHTML 处理的是 HTML 字符串,而 innerText 处理的是普通文本字符串。以下面的 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 属性将返回下列字符串:
<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 返回的文本可能会有所不同。IE 和 Opera 常常把所有标签转换为大写,而 Safari、Chrome 和 Firefox 则以文档中指定的形式返回 HTML -- 包括空格和缩进。不要指望所有浏览器都会返回没有丝毫差别的 innerHTML 值。在写入信息时,innerHTML 会将给定的字符串解析为 DOM 子树,并用这个子树替换所有的子节点。由于赋给 innerHTML 的字符串会被当作 HTML ,因此其中包含的所有标签都会按照浏览器处理 HTML 的标准方式,被转换成对应的元素 (同样,这个过程也会因浏览器而异)。如果像下面这样,只设置简单的文本,那么结果就如同设置 innerText 一样:
div.innerHTML = "Hello world!";
如果为 innerHTML 设置的字符串中包含 HTML 代码,结果可能就会大不一样了。区别就在于 innerText 会转义 HTML 语法字符,而 innerHTML 会解析它们。来看下面的例子:
div.innerHTML = "Hello & welcome, <b>\"reader\"!</b>";
执行这行代码之后的结果是:
<div id="content">Hello & welcome, <b>"reader"!</b></div>
在设置完 innerHTML 之后,马上就可以像访问文档中的其他节点一样访问新生成的节点。
innerHTML 也有一些限制。首先,在多数浏览器中,通过 innerHTML 插入的 <script> 元素不会被执行。IE是唯一支持这种操作的浏览器,但条件是必须指定 defer 特性,并且在 <script> 元素前面添加微软所谓的作用域元素 (scoped element)。这是因为 <script> 元素被认为是作用域外元素 (NoScope element),包含着在页面中看不到该元素的意思,就像看不到 <style> 元素或注释一样。在通过 innerHTML 插入的字符串中,如果一开始就是作用域外元素,IE 会把所有作用域外元素都剥离掉,也就是说下面这行代码将无法执行:
div.innerHTML = "<script defer>alert('hi');</scr" + "ipt>"; // 不能执行
在这里,通过 innerHTML 插入的字符串以一个作用域外元素开头,因此整个字符串会变成空字符串。为了确保脚本能够执行,必须前置一个作用域内元素,例如一个文本节点,或者像<input>这样的一个没有结束标签的元素。例如,下面这几行代码都可以正常执行:
div.innerHTML = "_<script defer>alert('hi');</scr" + "ipt>";
div.innerHTML = "<div> </div><script defer>alert('hi');</scr" + "ipt>";
div.innerHTML = "<input type=\"hidden\"><script defer>alert('hi');</scr" + "ipt>";
第一行代码会导致在 <script> 元素前插入一个文本节点。事后,为了不影响页面显示,可能需要移除这个文本节点。第二行代码采用的方法类似,只不过使用的是一个包含非换行空格的 <div> 元素。如果仅仅插入一个空的 <div> 元素,还是不行;必须要包含一点内容,浏览器才会创建文本节点。同样,为了不影响页面布局恐怕还是得要移除这个节点。第三行代码使用的是一个隐藏的 <input> 字段,也能达到相同的效果。不过,由于隐藏的 <input> 字段不影响页面布局,因此这种方式在大多数情况下都是首先。
并不是所有元素都有 innerHTML 属性。不支持 innerHTML 的元素有:<col>、<colgroup>、<frameset>、<head>、<html>、<style>、<table>、<tbody>、<thead>、<tfoot>、<title>和 <tr>。
5.内存和性能问题
使用 innerText、innerHTML、outerText 和 outerHTML 替换子节点可能会导致浏览器的内存问题,尤其是在 IE 中。如果被删除的子树中的元素设置了事件处理程序或者具有值为 JavaScript 对象的属性,就会出现这种问题。假设某个元素有一个事件处理程序 (或一个作为属性的 JavaScript对象),当使用上述某个属性将该元素从文档树中移除后,元素与事件处理程序之间的绑定依旧存在于内存中。如果这种情况频繁出现,页面占用的内存数量就会显著增加。因此,在使用这4个属性时,对于即将移除的元素,最好先手工移除它的所有事件处理程序和 JavaScript 对象属性。
使用这几个属性仍然还是有好处的,特别是使用 innerHTML。一般来说,在插入大量的新HTML时,使用 innerHTML 要比通过多次 DOM 操作先创建节点再指定它们之间的关系有效率得多。这是因为在设置 innerHTML (outHTML) 时,就会创建一个 HTML 解析器。这个解析器是在浏览器级别的代码 (通常是 C++ 编写的) 基础上运行的,因此要比执行 JavaScript 快得多。不可避免地,创建和销毁 HTML 解析器也会带来性能损失,所以最好能够将设置 innerHTML 或 outerHTML 的次数控制在合理的范围内。例如,下列代码使用 innerHTML 创建了很多列表项:
for (var i=0, len=values.length; i<len; i++) {
ul.innerHTML += "<li>" + values[i] + "</li>"; // 不应该这样做!
}
这种每次循环都设置一次 innerHTML 的做法效率很低。而且,每次循环还要从 innerHTML 中读取一次信息,就意味着每次循环要访问两次 innerHTML 。最好的做法是单独构建字符串,然后再一次性地将结果字符串赋值给 innerHTML,像下面这样:
var itemsHtml = "";
for(var i=0, len=values.length; i<len; i++){
itemsHtml += "<li>" + values[i] + "</li>";
}
ul.innerHTML = itemsHtml;
这个例子的效率要高得多,因为它只对 innerHTML 执行了一次赋值操作。