5.7 修改DOM
了解如何修改DOM后,你就可以干任何事了,从即时创建自定义的XML文档到建立接受用户键入的动态表单,几乎所有可能的操作。修改DOM有3个步骤:首先要知道怎么创建一个新元素,然后要知道如何把它插入DOM中,最后要学习如何删除它。
5.7.1 使用DOM创建节点
修改DOM的主要方法是使用createElement函数,它可以让你即刻创建新元素。但是,已经创建的新元素并不会马上插入到DOM中(刚开始使用DOM的人常会对此感到迷惑)。所以,我先把重点放在创建DOM元素上。
createElement方法带有元素的标记名称,并返回该元素的实际DOM引用,这里没有特性和样式。如果你要开发使用XSLT驱动的XHTML页面(或者使用正确的MIME侍服的XHTML页面)的应用程序,必须记住,你使用的是XML文档,所以创建的元素必须使用正确的XML命名空间来关联它们。为无缝地解决问题,你可以使用一个简单的函数,用它来测试你正使用的HTML DOM文档是否支持使用命名空间(XHTML DOM文档的一个特点)来创建新的元素。在这种情况下,你必须使用正确的XHTML命名空间来创建新的DOM元素,如代码清单5-22所示。
代码清单5-22 创建新DOM元素的通用函数
function create( elem ) {
return document.createElementNS ?
document.createElementNS( 'http://www.w3.org/1999/xhtml', elem ) :
document.createElement( elem );
}
例如,使用这个函数你就可以创建一个简单的<div>元素,并附上一些额外的信息:
var div = create("div");
div.className = "items";
div.id = "all";
此外需要注意的是,一个创建新文本节点的DOM方法叫做createTextNode。它需要传入一个参数,即你需要插入到节点中的文本,并返回已创建的文本节点。
使用新建立的DOM元素和文本节点,就可以把它们插入到DOM文档所需的位置中去了。
5.7.2 插入到DOM中
即使是经验丰富的DOM老手,有时也难免会感到插入元素到DOM中非常麻烦。在你的JavaScript工具库中准备以下两个函数可以让你把事情做好。
第一个函数是insertBefore,可以在另一个子元素前插入一个元素。使用该函数的方法类似如下例子:
parentOfBeforeNode.insertBefore( nodeToInsert, beforeNode );
我使用了一个助记诀窍辅助记忆它的参数顺序:“insert第一个元素,before第二个。”没错吧,你只需片刻就记住了它。
有了一个能在其他节点前插入节点(可以是元素,也可以是文本节点),你是否该考虑考虑:“如何插入一个父节点中最后一个子节点?”一个名为appendChild的函数可以做到这个。appendChild调用一个元素参数,追加指定的节点到子节点列表中的最后一个。这个函数的用法大致如下:
parentElem.appendChild( nodeToInsert );
为避免死记硬背insertBefore和appendChild的参数顺序,你可以使用我写的两个辅助函数解决问题,如代码清单5-23和代码清单5-24所示,参数以相关的元素/节点然后是需插入的元素/节点的顺序调用。此外,before函数还有一个传入父节点的可选项,它可以为你节约一些代码。最后,两个函数都允许你传入需要插入/追加的字符串并自动地帮你转化为文本节点。建议你提供父元素作为引用(防止elem变成null)。
代码清单5-23 在另一个元素之前插入元素的函数
function before( parent, before, elem ) {
// 检查parent是否传入
if ( elem == null ) {
elem = before;
before = parent;
parent = before.parentNode;
}
parent.insertBefore( checkElem( elem ), before );
}
代码清单5-24 为另一个元素追加一个子元素的函数
function append( parent, elem ) {
parent.appendChild( checkElem( elem ) );
}
代码清单5-25的辅助函数允许你插入元素和文本(自动转化为正确的文本节点)。
代码清单5-25 before和append的辅助函数
function checkElem( elem ) {
// 如果只提供字符串,则把它转化成文本节点
return elem && elem.constructor == String ? document.createTextNode( elem ) : elem;
}
现在,使用before和append函数,通过创建新的DOM元素,你就可以在DOM中增添更多的信息呈现给用户了,如代码清单5-26所示。
代码清单5-26 使用append和before函数
// 创建一个新的<li>元素
var li = create("li");
attr( li, "class", "new" );
// 创建文本内容并添加到<li>中
append( li, "Thanks for visiting!" );
// 把<li>插入到有序列表的顶端
before( first( tag("ol")[0] ), li );
// 运行这些语句会转化空<ol>
<ol></ol>
// 为以下:
<ol>
<li class='new'>Thanks for visiting!</li>
</ol>
一将这些信息“插入”到DOM(包括使用insertBefore和appendChild),它马上就会渲染并呈现在用户前。因此,你可用之作实时的反馈。这对于需要用户输入的交互应用尤其有用。
现在你看到的只是基于DOM的方法来创建和插入节点,进一步了解其他向DOM注入内容的方法将更有用。
5.7.3 注入HTML到DOM
比创建普通DOM元素并插入到DOM中更为流行的技术是,直接向文档注入HTML。这个最为简便的实现方法使用了前面讨论的innerHTML。除了从元素获取HTML之外,它同时也是设置元素内的HTML的一条途径。作为一个展示其简单性的例子,假设有一个空的<ol>元素并需要往里加入<li>,那么的代码大致如下:
// 给ol 元素加入部分li
tag("ol")[0].innerHTML = "<li>Cats.</li><li>Dogs.</li><li>Mice.</li>";
难道这不比繁复地创建大量的DOM元素和相应的文本节点要简单得多?更好的是,它还比使用DOM方法要快得多(根据http://quirksmode.org的说法)。但世事无完美,使用innerHTML注入方法也伴随着一些棘手的问题:
l 如前所述,XML DOM文档中并不存在innerHTML方法,就是说你得继续使用通用的DOM 创建方法。
l 使用客户端XSLT创建的XHTML文档也不存在innerHTML方法,因为它们也是纯粹的XML文档。
l innerHTML完全删除了元素内容的任何节点,意味着没有像DOM方法一样追加或者插入的简便方法。
最后一条尤为棘手,因为在另一个元素之前插入元素或者追加到子元素列表末尾特别有用。但是,编织一些DOM魔法,去改造append和before方法,除了常规的DOM元素,还可以让它跟常规的HTML字符串和平共处。改造过程分两步走。第一步建立一个checkElem函数,它可以处理HTML字符串、DOM元素以及DOM元素数组,如代码清单5-27所示。
代码清单5-27 转化一个DOM节点/HTML字符串混合型参数数组为纯粹的DOM节点数组
function checkElem(a) {
var r = [];
// 如果参数不是数组, 则强行转换
if ( a.constructor != Array ) a = [ a ];
for ( var i = 0; i < a.length; i++ ) {
// 如果是字符串
if ( a[i].constructor == String ) {
// 用一个临时元素来存放HTML
var div = document.createElement("div");
// 注入HTML, 转换成DOM结构
div.innerHTML = a[i];
// 提取DOM结构到临时 div 中
for ( var j = 0; j < div.childNodes.length; j++ )
r[r.length] = div.childNodes[j];
} else if ( a[i].length ) { // If it's an array
// 假定是DOM节点数组
for ( var j = 0; j < a[i].length; j++ )
r[r.length] = a[i][j];
} else { // 否则,假定是DOM节点
r[r.length] = a[i];
}
}
return r;
}
第二步,你需要修改这两个插入函数以适应新的checkElem,接受数组元素,如代码清单5-28所示。
代码清单5-28 插入和追加内容到DOM的改进函数
function before( parent, before, elem ) {
// 检查是否提供 parent 节点参数
if ( elem == null ) {
elem = before;
before = parent;
parent = before.parentNode;
}
// 获取元素的新数组
var elems = checkElem( elem );
// 向后遍历数组,
// 因为我们向前插入元素
for ( var i = elems.length - 1; i >= 0; i-- ) {
parent.insertBefore( elems[i], before );
}
}
function append( parent, elem ) {
// 获取元素数组
var elems = checkElem( elem );
// 把它们所有都追加到元素中
for ( var i = 0; i <= elems.length; i++ ) {
parent.appendChild( elems[i] );
}
}
现在,使用这些新函数为有序列表追加<li>就非常简单了:
append( tag("ol")[0], "<li>Mouse trap.</li>" );
// 运行该简单的一行就能追加HTML到这个<ol>中
<ol>
<li>Cats.</li>
<li>Dogs.</li>
<li>Mice.</li>
</ol>
// 把它变成了这样:
<ol>
<li>Cats.</li>
<li>Dogs.</li>
<li>Mice.</li>
<li>Mouse trap.</li>
</ol>
// 而用before()运行一个类似的语句
before( last( tag("ol")[0] ), "<li>Zebra.</li>" );
// 则会变成这样:
<ol>
<li>Cats.</li>
<li>Dogs.</li>
<li>Zebra.</li>
<li>Mice.</li>
</ol>
这将能帮助你开发简洁和清晰的代码。但是,如果从DOM中移动元素和删除节点要怎么办呢?当然,肯定也会有其他方法处理这些问题。
5.7.4 删除DOM节点
删除DOM节点的操作几乎与创建和插入一样频繁。根据用户的需求允许创建无限量的项目,那么允许用户能够删除它们之中再也不需要处理的部分也就变得很重要了。删除节点的能力可封装成一个函数:removeChild。它跟appendChild用法一致效果相反。该函数的用法大致如下:
NodeParent.removeChild( NodeToRemove );
记住这点,你就可以创建两个独立的函数来删除节点了,如代码清单5-29所示。
代码清单5-29 删除DOM节点的函数
// 删除一个独立的DOM节点
function remove( elem ) {
if ( elem ) elem.parentNode.removeChild( elem );
}
代码清单5-30展示了从一个元素中删除所有子节点的函数,仅需要DOM元素的引用作为参数。
代码清单5-30 从一个元素中删除所有子节点的函数
// 在DOM中删除一个元素的所有子节点
function empty( elem ) {
while ( elem.firstChild )
remove( elem.firstChild );
}
举个例子,要删除上一节例子中添加的<li>,同时假设你已经给用户充足的时间来浏览<li>了,在没有提示的情况下被删除。可以使用以下的JavaScirpt代码来完成这个操作:
// 删除<ol>的最后一个<li>
remove( last( tag("ol")[0] ) )
// 将会把
<ol>
<li>Learn Javascript.</li>
<li>???</li>
<li>Profit!</li>
</ol>
// 转换成:
<ol>
<li>Learn Javascript.</li>
<li>???</li>
</ol>
// 如果要求执行empty()函数而不是remove()
empty( last( tag("ol")[0] ) )
// 它将会简单地清空<ol>, 只留下:
<ol></ol>
学习完删除DOM节点,你已经清楚了我们的文档对象模型是如何运作的,并学会如何充分运用它。






