第10章 DOM (六)

来源:互联网 发布:小米笔记本12.5编程 编辑:程序博客网 时间:2024/06/11 22:21
 

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 实现了这个方法。
要注意的是,scrollIntoView() 和 scrollIntoViewIfNeeded() 作用的是元素的窗口,而 scrollByLines() 和 scrollByPages() 影响的则是元素自身。下面是几个示例:
// 将页面主体滚动5行
document.body.scrollByLines(5);
// 确保当前元素可见
docuemtn.forms[0].scrollIntoView();
// 确保只在当前元素不可见的情况下使其可见
document.images[0].scrollIntoViewIfNeeded();
// 将主页面主体往回滚动1页
document.body.scrollByPages(-1);
由于所有浏览器都支持方法只有 scrollIntoView() ,因此这个方法是最常用的。

10.2.3 children 属性

IE 与其他浏览器对文本节点中空白符的解释不一致,导致了 children 属性的出现。作为 HTMLCollection,children 属性中只包含元素的子节点中那些也是元素的节点。换句话说,在元素的子节点都是元素节点的情况下,children 属性与 childNodes 是相同的。以下是使用 children 属性的示例:
var childCount = element.children.length;
var firstChild = element.children[0];
除了 Firefox 之外,其他浏览器都支持 children 集合。IE中的 children集合会包含注释节点。

10.2.4 contains() 方法

开发人员经常需要确定某个给定的节点是不是另一个节点的后代。为此,IE 率先引入了 contains() 方法,让开发人员无须遍历 DOM 文档树即可获知此信息。应该在作为搜索起点的祖先节点上调用 contains() 方法,并为该方法传递一个参数,即要检测的后代节点。如果传入的节点是当前节点的后代,那么方法返回 true;否则返回 false。来看下面的例子:
alert(document.documentElement.contains(document.body)); // true
这个例子测试 <body> 元素是不是 <html> 元素的后代,而在格式正确的 HTML 页面中,这个例子会返回 true。IE、Safari 3 及更高版本、Opera 8及更高版本、Chrome 都支持 contains() 方法。Safari 2.x 中虽然也有这个方法,但无法正常使用。因此,需要通过浏览器检测来确定Safari 的版本,以保证可以正常使用这个方法。
Firefox不支持 contains() 方法,但 Firefox 在 DOM3 级实现中提供了一个替代的 compareDocumentPosition() 方法 (Opera9.5及更高版本也支持此方法)。这个方法用于确定两个节点之间的关系,返回一个表示关系的位掩码 (bitmask)。下表列出了这个位掩码的值。
为模仿 contains() 方法,应该关注的是掩码 16 。可以对 compareDocumentPosition() 的结果执行按位与,以确定参考节点 (调用compareDocumentPosition()方法的当前节点) 是否包含给定的节点 (传入的节点)。来看下面的例子:
var result = document.documentElement.compareDocumentPosition(document.body);
alert(!!(result & 16));
执行上面的代码后,结果会变成 20 (表示 "居后" 的4加上表示 "被包含" 的16)。对掩码 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;
}
}
这个函数使用了三种方法来确定一个节点是不是另一个节点的后代。函数的第一个参数是参考节点,第二个参数是要检查的节点。在函数体内,首先检测 refNode 中是否存在 contains() 方法 (能力检测)。这一部分代码还检查了当前浏览器所用的 WebKit 版本号。如果方法存在而且不是 WebKit (!client.engine.webkit) ,则会继续执行代码。否则,如果浏览器是 WebKit 且至少是 Safari 3(WebKit 版本号为 522或更高),那么也可以继续执行代码。在 WebKit 版本号小于 522 的Safari 浏览器中, contains() 方法不能正常使用。
接下来检查是否存在 compareDocumentPosition() 方法,而函数的最后一步则是自 otherNode 开始向上遍历 DOM 结构,递归地取得 parentNode 并检查是否与 refNode 相等。在文档树的顶端,parentNode 的值等于 null,于是循环结束。这是针对旧版本 Safari 设计的一个后备策略。

10.2.5 操作内容

DOM 虽然为访问 HTML 文档的各个部分提供了极大的便利,但诸如插入文本和 HTML 代码之类的常见操作,仍然需要编写很多行代码。为此,IE4率先引入了 innerText、innerHTML、outerText 和 outerHTML 等属性,所有元素使用这几个属性都可以方便地向 HTML 页面中插入代码,或修改现有代码。
1.innerText 属性
通过 innerText 属性可以操作元素中包含的所有文本内容,无论文本位于子文档树中的什么位置。在通过 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> 元素而言,其 innerText 属性会返回下列字符串:
This is a paragraph with a list following it.
Item 1
Item 2
Item 3
由于不同浏览器处理空白符的方式不同,因此输出的文本可能会也可能不会包含原始HTML代码中的缩进。
使用 innerText 属性设置这个 <div> 元素的内容,则只需一行代码:
div.innerText = "Hello world!";
执行这行代码后,页面的 HTML 代码就会变成如下所示:
<div id="content">Hello world!</div>
可见,设置 innerText 属性移除了先前存在的所有子节点,完全改变了 DOM 子树。
设置 innerText 永远只会生成当前节点的一个子文本节点,而为了确保只生成一个子文本节点,就必须要对文本进行 HTML 编码。此外,还可以利用 innerText 属性过滤掉 HTML 标签。方法是将 innerText 设置为等于 innerText,这样就可以去掉所有 HTML 标签,如下所示:
div.innerText = div.innerText;
执行这行代码的结果就是用原来的文本替换容器元素中的所有内容。
IE、Safari、Opera 和 Chrome 支持 innerText 属性。Firefox 虽然不支持 innerText,但支持作用类似的 textContent 属性。textContent 是 DOM3 级规定的一个属性,而且也得到了 Safari、Opera 和 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;
}
}

这两个函数都接受一个元素,然后检查这个元素是不是有 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 &amp; welcome, <b>&quot;reader&quot;!</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>&nbsp;</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 执行了一次赋值操作。

原创粉丝点击