使用 JavaScript 遍历文档对象模型

有效利用 DOM 脚本编程和 JavaScript 库

当然 Web 开发人员熟悉 JavaScript 和文档对象模型 (DOM)。DOM 为抽象的 XML/HTML 文档提供中立的接口,而 JavaScript 提供该接口的实现,让您与网页进行交互。 本文探讨了 DOM 的 JavaScript 绑定,学习如何管理 web 文档以实现最优性能。文中使用一个示例应用程序演示了 DOM 方法和属性以及如何向 DOM 事件附加处理程序。

Sebastiano Armeli-Battana, 软件工程师, 自由职业者

/developerworks/i/p-sarmeli.jpgSebastiano Armeli-Battana 是一名 JavaScript 和 Java 开发领域的软件工程师,他非常热衷于 Web 技术。他在 SMS Management & Technology 担任 Java 技术采用顾问。Sebastiano 也是一名 Web 领域的自由职业者。他是 jQuery plug-in JAIL 的作者。他的个人站点是 http://www.sebastianoarmelibattana.com



2012 年 6 月 25 日

简介

万维网联盟 (W3C) 已经在不同的规范组(DOM 1 级、DOM 2 级和 DOM 3 级)对文档对象模型 (DOM) 进行了定义。 DOM 将 HTML 或 XML 表示为一棵树,该树由具有属性和方法的不同层次的节点组成。使用客户端语言例如 JavaScript,您可以为该树中的节点添加、修改、删除以及附加事件,从而产生交互、动态的 Web 页面。

使用客户端脚本语言 (JavaScript) 修改 DOM 的行为称为 DOM 脚本编程。DOM 脚本编程用来代替通用术语动态 HTML (DHTML),DHTML 在 Web 开发中用来表示使用 HTML、CSS 和 JavaScript 的交互 Web 页面的构造。

本文探讨了 DOM API 中最常用的方法和属性。使用一个详细的示例来展示如何使用 JavaScript 遍历 DOM。使用一个较复杂的模型阐述了在何处需要考虑事件和监听程序。了解如何利用 JavaScript 库与 DOM 进行交互。

您可以 下载 本文使用的源代码。参考资料 为那些想要深入了解本文所讨论的概念的读者提供了链接。


DOM 脚本编程

在 DOM 术语中,document 表示树根。在 JavaScript 中表示为 window.document,或者简单地表示为 document(因为它被附加到 Window 对象)。这是 JavaScript 实现的起始点。 清单 1 显示了一个 HTML 片段示例。

清单 1. HTML 代码
<body>
   <p id="paragraph1">
      <span>This is some text</span>
      <a href="/index.html" title="Click here">Click here</a>
   <p>
</body>

从 DOM 角度看,以上示例中 p 标签由 DOM Element 接口表示。它是 span 标签和 a 标签的父标签。span 标签和 a 标签是同级标签。

假如您想要获取 清单 1 中代码定位点的 href 属性。访问 DOM 中某个元素的简单方法是使用 getElementById 方法。 以下代码串显示了包含使用接口定义语言 (IDL) 编写的 getElementById 签名的文档接口定义的一部分: Element getElementById (in DOMString elementId)

JavaScript 使用 String 对象实现 DOMString 接口,所以该方法以字符串的形式接受元素 ID 并将其作为一个参数。 在示例片段中,带有 id 属性的惟一元素是 p 标签,所以可以使用 var paragraph = document.getElementById("paragraph1"); 检索它。

使用 childNodes 属性您可以获得嵌入在 p 标签中的定位点。该属性属于 Node 接口,并返回一个 NodeList 类型的对象。 在 JavaScript 中,该对象是一个数组式的对象。数组式的对象没有方法,比如 pop()push(),但是它们有 length 属性。 从 childNodes 属性中返回的对象在节点元素(HTML 标签)、文本节点或注释节点之间没有任何不同。 如果您只要寻找节点元素,您可以考虑 children 属性。不考虑文本和注释节点的话,它比 childNodes 更能满足我们的目的。 在该示例中,定位点是段落的第二个子节点,可以使用 var aElement = paragraph.children[1]; 获得。

给定一个元素,要获取 href 属性的值,您可以采用 getAttribute 方法,将属性名作为一个参数进行传递(在本例中,为 href)。 包含 getAttribute 方法的 IDL 定义部分为: DOMString getAttribute (in DOMString name)

在该示例中,您可以像这样实现以上接口:var aHref = aElement.getAttribute("href"); // "index.html"

与在 JavaScript 中一样,您可以将方法链接起来。若要通过一行获取 a 标签 href 属性的值,请使用:var aHref = document.getElementById("paragraph1").children[1].getAttribute("href"); // index.html */


探究 DOM 脚本编程:示例应用程序

本节探究 DOM 脚本编程的一些特性。Sticky Notes 示例应用程序是一个交互的 Web 页面,该页面可让用户不需重新加载页面即可添加 “sticky” 注释。图 1 显示了该页面。

图 1. Sticky Notes 应用程序前端
Sticky Notes 应用程序前端

图 1 所显示页面的 HTML 代码显示在 清单 2 中。在清单 2 中 head 标签是对 CSS 和 JS 文件的引用。 在 body 标签中您已经可以看到页面中注释的结构:textarea 标签和触发新注释创建的定位点。

清单 2. HTML 代码
<!DOCTYPE html>
<html>
    <head>
        <meta charset=utf-8">
        <title>
            Dom Scripting
        </title>
        <link rel="stylesheet" href="css/master.css" />
        <script src="js/script.js"></script>
    </head>
    <body>
        <div class="wrapper">
            <h1> Sticky Notes </h1>
            <div class="links">
                <textarea id="contentArea" cols="10"> </textarea>
                <a href="/random.html" class="add">Click here</a> 
<span>to add a sticky note</span>
            </div>
            <div id="notes">
                <div class="note">
                    <p>
                        This is a note
                    </p>
                </div>
            </div>
        </div>
    </body>
</html>

让我们分析一下包含在该页面所加载的 script.js 文件中的 JavaScript 代码。一旦加载了页面或构建了文档,就需要触发脚本的逻辑。为此,一种可选方法是将某个函数绑定到 onload 窗口属性,如 清单 3 所示。

清单 3. onload 属性
window.onload = init;
function init() {}

onload 属性与 DOM 事件加载相关联,这是在 DOM 0 级下如何将事件绑定到一个监听程序函数的典型(这是所有浏览器都支持的 “规范”,而不是标准)。 相反,一个标准的 DOM 事件模型是在 DOM 2 级规范中定义的。在该规范中,定义了 addEventListener 方法(来自 EventTarget 接口)来为目标元素注册一个事件处理程序。 以下代码公开了该方法的签名:object.addEventListener(eventType, eventHandler, useCapture);

eventType 是对象上要注册的事件时,eventHandler 便是绑定至特定事件的函数。useCapture 是一个可选的布尔量,它定义了在事件流的哪个阶段调用函数(冒泡阶段还是捕捉阶段)。 以下代码使用 addEventListener 函数将加载方法绑定到该窗口:window.addEventListener("load", init, false);

遗憾的是,版本 9 之前的 Internet Explorer (IE) 版本不支持以上的 W3C 方法,但是具有自己的实现: object.attachListener(eventType, eventHandler);。有关 IE 对 DOM 3 级事件的支持的信息,参见 参考资料

eventType 需要为事件名加上前缀 on。IE 中的事件默认进行冒泡,所以不显示 useCapture 参数。

清单 4 显示了 script.js 中的 addEvent 函数,该函数处理绑定在所有浏览器中的事件。它是一个名为 SA 的全局对象的一个方法。此方法适用于所有以前讨论过的方法。

清单 4. addEvent 函数
window.SA  = {
addEvent : function(element, evType, fn, useCapture) { 
        if (element.addEventListener) { 
            element.addEventListener (evType, fn, useCapture); 
            return true; 
        } else if (element.attachEvent) { 
            var r = element.attachEvent('on' + evType, fn); 
            return r; 
        } else { 
            element['on' + evType] = fn; 
        } 
    }
}

如果您使用 addEvent 函数,可以将函数(我们称之为 SA.load)绑定到 load 事件,如 清单 5 所示。

清单 5. 绑定函数
SA.addEvent(window, "load", SA.load, false);
SA = {
...
           load : function() {
                       // init block
           }
}

以上的 SA.load 函数只有当所有资源都被下载后才被触发,因为它是附加到 load 事件的。在通常情况下,附加到加载事件的函数在被执行前可能需要一段时间,尤其是当页面上有很多要下载的图像时。 将初始化脚本的函数附加到 DOMContentLoaded 事件的做法是很好的,现有的浏览器都支持这种做法,并且在构建 DOM 时予以触发。在下载外部资源之前执行该函数,从而使得页面具有更好的响应性。 版本 9 之前的 IE 默认不包括 DOMContentLoaded 事件,所以需要一个解决方案让它就像其他浏览器一样工作。在该示例中,页面中没有任何图像,所以您可以保留加载方法(页面的性能不会受到很大的影响)。

现在您可以将函数处理程序关联到目标定位点上的点击事件。当用户点击定位点时,将会执行指定的行为。在本例中,将会创建一个新的注释。 第一项任务是遍历 DOM 以检索我们的目标定位点,如 清单 6 所示。

清单 6. 使用类名 add 检索得到定位点
load : function() {
  var anchorSelected;
        
  if (document.getElementsByClassName) {
    anchorSelected = document.getElementsByClassName("add")[0];
  } else {
    var anchors = document.getElementsByTagName("a"),
        alenght = anchors.length;
        
    for (var i = 0; i < alenght; i++ ) {
      var anchor = anchors[i];
            
      if (anchor.className === "add") {
      anchorSelected = anchor;
    }
    }
  }
}

在清单 6 中,您或许可以预测到,document.getElementsByClassName 方法可让您使用给定的类名检索元素。该方法返回 HTML 元素的集合,但遗憾的是并非所有浏览器都支持该方法,例如 IE6 和 IE7。 对于这些浏览器,需要编写不同的逻辑。您可以首先通过 document.getElementsByTagName 方法获得定位点列表,然后循环该列表以获得名为 add 的 CSS 类的定位点。 GetElementsByTagName 方法通常返回一个 NodeList 对象,幸运的是在所有主流浏览器上都完全支持该方法。

清单 6 中,可以看到如何在 alength 变量中存储定位点数组的大小,这样在 for 循环中就只能查询 DOM 一次。 修改和使用 DOM 是一种开销很大的操作,所以您应当尽量减少与其交互的次数。

此时,一旦您检索到定位点,就可以将 click 事件绑定到负责添加注释的监听程序函数,如 清单 7 所示。

清单 7. 绑定事件
load : function() {
  ...
  SA.addEvent(anchorSelected, "click", SA.addNote, false);
}

清单 7 显示了名为 SA.addNote、附加到点击事件的事件监听程序。该函数有以下几个目标:

  • 克隆最新创建的注释
  • 将用户输入的文本加入刚刚克隆的注释
  • 将新注释附加到注释列表

清单 8 显示了对第一个目标的实现。

清单 8. 克隆最新创建的注释
addNote : function(event) {
     var notes = document.getElementById("notes");
        
     // Clone the node
     var newNode = notes.children[0].cloneNode(true);
},

通过 getElementById 方法获得带有注释 ID 的 div 标签后,您可以检索嵌入在 div 中的第一个子节点,并使用 cloneNode 方法对其进行克隆。 将刚刚克隆的 DOM 节点存储在名为 newNode 的变量中。

选择嵌入在 newNode 中的段落节点,在克隆的节点上调用 getElementsByTagName 方法。DOM 提供一个名为 textContent 的属性来获取节点的内容。 遗憾的是,并非所有浏览器都完全支持该属性。您需要使用不同的方法:通过段落访问 firstChild 属性,然后通过该属性检索 nodeValue 属性。 刚刚获得的 nodeValue 现在被设置为页面中显示的 textarea 标签的内容。 textarea 的内容来自于 textarea DOM 元素的值属性。通过 getElementById 方法实现。 清单 9 显示了如何将用户输入的文本加入到刚刚克隆的注释中(第二个目标)。

清单 9. 将文本区的文本加入到刚刚克隆的注释
addNote : function(event) {
  ...
 // Set the content of the node
 newNode.getElementsByTagName("p")[0].firstChild.nodeValue =  
 document.getElementById("contentArea").value;
        
 notes.appendChild(newNode);
}

对于最后一个目标,使用 appendChild 方法将新创建的注释附加到注释列表,如 清单 10 所示。

清单 10. 将新注释附加到注释列表
addNote : function(event) {
  ...

 notes.appendChild(newNode);
}

最后,您需要阻止点击事件的默认行为(对于定位点来说,是将用户重定向至 href 属性中指定的 URL)。DOM 指定应用于事件的 preventDefault() 方法,通过将参数传递给处理函数来完成该任务。 此外,IE 版本 9 之前的版本不支持该方法。要实现该目标,在 IE 9 之前的版本中,您可以将 event.returnValue 属性设置为 false。清单 11 显示了相关代码。

清单 11. 阻止点击事件的默认行为
addNote : function(event) {
  ...

 event.preventDefault ? event.preventDefault() : event.returnValue = false;
}

清单 12 显示了包含在 script.js 文件中的所有 JavaScript 代码。

清单 12. Script.js
window.SA = {
    
addEvent : function(element, evType, fn, useCapture) { 
        if (element.addEventListener) { 
            element.addEventListener (evType, fn, useCapture); 
            return true; 
        } else if (element.attachEvent) { 
            var r = element.attachEvent('on' + evType, fn); 
            return r; 
        } else { 
            element['on' + evType] = fn; 
        } 
    },
    
    load : function() {
        
        var anchorSelected;
        
        if (document.getElementsByClassName) {
anchorSelected =  document.getElementsByClassName("add")[0];

        } else {
            var anchors = document.getElementsByTagName("a"),
                alenght = anchors.length;
        
            for (var i = 0; i < alenght; i++ ) {
                var anchor = anchors[i];
            
                if (anchor.className === "add") {
                    anchorSelected = anchor;
                }
            }
        }
        
        SA.addEvent(anchorSelected, "click", SA.addNote, false);
    },
    
    addNote : function(event) {
        
        var notes = document.getElementById("notes");
        
        // Clone the node
        var newNode = notes.children[0].cloneNode(true);
            
        // Set the content of the node
        newNode.getElementsByTagName("p")[0].firstChild.nodeValue     
= document.getElementById("contentArea").value;
        
        notes.appendChild(newNode);
        
event.preventDefault ? event.preventDefault() : event.returnValue = false;
    }
}

SA.addEvent(window, "load", SA.load, false);

JavaScript 库和 DOM

开发人员编写 JavaScript 代码时,通常会使用 JavaScript 库或框架,这些库或框架处理 DOM 在不同浏览器中的不同实现。注意如何使用流行的 jQuery 库重新编写 清单 13

清单 13. Script-jquery.js
$(function(){
    $('a.add').click(function(){
        var newNote = $('.note').eq(0).clone();
        newNote.find('p').text($('#contentArea').val());
        $('#notes').append(newNote);
        return false;
    });
});

该脚本中保存了多行代码,且代码是整洁干净的。

由于为了使用 jQuery,您需要在 HTML 中导入库,如 清单 14 所示,将发送一个附加的 HTTP 请求,该请求需要更多的时间来执行该库。 该过程可能会让应用程序变的较慢,所以由您来决定在使用库和编写较少的代码之间进行权衡。

清单 14. 将库导入 HTML
<html>
    <head>
        ...
        <script src="http://ajax.googleapis.com/ajax/libs/jquery
/1.6.1/jquery.min.js"></script>
    </head>

JavaScript 库是非常强大的工具,它可以让您的生活变得更加方便。然而,您也需要了解 DOM 脚本编程,因为使用库并非总是处理 DOM 的最有效方法。同时也建议您了解某个库场景背后发生了什么。


结束语

DOM 对于 web 开发人员来说很重要,因为它是 JavaScript 访问 web 页面的方法。在浏览器供应商实现 DOM API 的方法上有些问题和限制。 并非所有的浏览器都支持一些属性和方法(例如,addEventListener()textContent) 或在某些情况下它们表现的行为不同。

性能是 DOM 脚本编程需要考虑的另一个重要因素。正如本文所演示的,只要您知道 JavaScript 与 DOM 是如何进行交互的,您就可以利用某些 JavaScript 框架来操作和遍历 DOM。


下载

描述名字大小
文章源代码DomScripting.zip5KB

参考资料

学习

获得产品和技术

  • 免费 试用 IBM 软件。下载试用版本,登录在线试用,在沙箱环境中使用产品,或是通过云来访问。有超过 100 种 IBM 产品试用版可供选择。

讨论

  • 加入 developerWorks 中文社区。查看开发人员推动的博客、论坛、组和维基,并与其他 developerWorks 用户交流。

条评论

developerWorks: 登录

标有星(*)号的字段是必填字段。


需要一个 IBM ID?
忘记 IBM ID?


忘记密码?
更改您的密码

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件

 


在您首次登录 developerWorks 时,会为您创建一份个人概要。您的个人概要中的信息(您的姓名、国家/地区,以及公司名称)是公开显示的,而且会随着您发布的任何内容一起显示,除非您选择隐藏您的公司名称。您可以随时更新您的 IBM 帐户。

所有提交的信息确保安全。

选择您的昵称



当您初次登录到 developerWorks 时,将会为您创建一份概要信息,您需要指定一个昵称。您的昵称将和您在 developerWorks 发布的内容显示在一起。

昵称长度在 3 至 31 个字符之间。 您的昵称在 developerWorks 社区中必须是唯一的,并且出于隐私保护的原因,不能是您的电子邮件地址。

标有星(*)号的字段是必填字段。

(昵称长度在 3 至 31 个字符之间)

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件.

 


所有提交的信息确保安全。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Web development
ArticleID=822596
ArticleTitle=使用 JavaScript 遍历文档对象模型
publish-date=06252012