JavaScript基础

跨浏览器、跨语言
文档对象模型是一种 W3C 标准。因此,所有现代 Web 浏览器都支持 DOM —— 至少在一定程度上支持。虽然不同的浏览器有一些区别,但如果使用 DOM 核心功能并注意少数特殊情况和例外,DOM 代码就能以同样的方式用于任何浏览器。修改 Opera 网页的代码同样能用于 Apple's Safari®、Firefox®、Microsoft® Internet Explorer® 和 Mozilla®。

DOM 也是一种跨语言 的规范,换句话说,大多数主流编程语言都能使用它。W3C 为 DOM 定义了几种语言绑定。一种语言绑定就是为特定语言定义的让您使用 DOM 的 API。比如,可以使用为 C、Java 和 JavaScript 定义的 DOM 语言绑定。因此可以从这些语言中使用 DOM。还有几种用于其他语言的语言绑定,尽管很多是由 W3C 以外的第三方定义的。

本系列文章主要讨论 JavaScript 的 DOM 绑定。这是因为多数异步应用程序开发都需要编写在 Web 浏览器中运行的 JavaScript 代码。使用 JavaScript 和 DOM 可以即时修改用户界面、响应用户事件和输入等等 —— 使用的完全是标准的 JavaScript。

总之,建议您也尝试一下其他语言中的 DOM 绑定。比如,使用 Java 语言绑定不仅能处理 HTML 还可处理 XML,这些内容将在以后的文章中讨论。因此本文介绍的技术还可用于 HTML 之外的其他语言,客户端 JavaScript 之外的其他环境。

节点的概念
节点是 DOM 中最基本的对象类型。实际上,您将在本文中看到,基本上 DOM 定义的其他所有对象都是节点对象的扩展。但是在深入分析语义之前,必须了解节点所代表的概念,然后再学习节点的具体属性和方法就非常简单了。

在 DOM 树中,基本上一切都是节点。每个元素在最底层上都是 DOM 树中的节点。每个属性都是节点。每段文本都是节点。甚至注释、特殊字符(如版权符号 ©)、DOCTYPE 声明(如果 HTML 或者 XHTML 中有的话)全都是节点。因此在讨论这些具体的类型之前必须清楚地把握什么是节点。

节点是……
用最简单的话说,DOM 树中的任何事物都是节点 。之所以用 “事物” 这个模糊的字眼,是因为只能明确到这个程度。比如 HTML 中的元素(如 img)和 HTML 中的文本片段(如 “Scroll down for more details”)没有多少明显的相似之处。但这是因为您考虑的可能是每种类型的功能,关注的是它们的不同点。

但是如果从另一个角度观察,DOM 树中的每个元素和每段文本都有一个父亲,这个父节点可能是另一个元素(比如嵌套在 p 元素中的 img)的孩子,或者 DOM 树中的顶层元素(这是每个文档中都出现一次的特殊情况,即使用 html 元素的地方)。另外,元素和文本都有一个类型。显然,元素的类型就是元素,文本的类型就是文本。每个节点还有某种定义明确的结构:下面还有节点(如子元素)吗?有兄弟节点(与元素或文本 “相邻的” 节点)吗?每个节点属于哪个文档?

显然,大部分内容听起来很抽象。实际上,说一个元素的类型是元素似乎有点冒傻气。但是要真正认识到将节点作为通用对象类型的价值,必须抽象一点来思考。

通用节点类型
DOM 代码中最常用的任务就是在页面的 DOM 树中导航。比方说,可以通过其 “id” 属性定位一个 form,然后开始处理那个 form 中内嵌的元素和文本。其中可能包含文字说明、输入字段的标签、真正的 input 元素,以及其他 HTML 元素(如 img)和链接(a 元素)。如果元素和文本是完全不同的类型,就必须为每种类型编写完全不同的代码。

如果使用一种通用节点类型情况就不同了。这时候只需要从一个节点移动到另一个节点,只有当需要对元素或文本作某种特殊处理时才需要考虑节点的类型。如果仅仅在 DOM 树中移动,就可以与其他节点类型一样用同样的操作移动到元素的父节点或者子节点。只有当需要某种节点类型的特殊性质时,如元素的属性,才需要对节点类型作专门处理。将 DOM 树中的所有对象都看作节点可以简化操作。记住这一点之后,接下来我们将具体看看 DOM 节点构造应该提供什么,首先从属性和方法开始。

节点的属性
使用 DOM 节点时需要一些属性和方法,因此我们首先来讨论节点的属性和方法。DOM 节点的属性主要有:

  • nodeName 报告节点的名称(详见下述)。

  • nodeValue 提供节点的 “值”(详见后述)。

  • parentNode 返回节点的父节点。记住,每个元素、属性和文本都有一个父节点

  • childNodes 是节点的孩子节点列表。对于 HTML,该列表仅对元素有意义,文本节点和属性节点都没有孩子。

  • firstChild 仅仅是 childNodes 列表中第一个节点的快捷方式。

  • lastChild 是另一种快捷方式,表示 childNodes 列表中的最后一个节点。

  • previousSibling 返回当前节点之前 的节点。换句话说,它返回当前节点的父节点的 childNodes 列表中位于该节点前面的那个节点(如果感到迷惑,重新读前面一句)。

  • nextSibling 类似于 previousSibling 属性,返回父节点的 childNodes 列表中的下一个节点。

  • attributes 仅用于元素节点,返回元素的属性列表。


其他少数几种属性实际上仅用于更一般的 XML 文档,在处理基于 HTML 的网页时没有多少用处。

不常用的属性
上述大部分属性的意义都很明确,除了 nodeName 和 nodeValue 属性以外。我们不是简单地解释这两个属性,而是提出两个奇怪的问题:文本节点的 nodeName 应该是什么?类似地,元素的 nodeValue 应该是什么?

如果这些问题难住了您,那么您就已经了解了这些属性固有的含糊性。nodeName 和 nodeValue 实际上并非适用于所有 节点类型(节点的其他少数几个属性也是如此)。这就说明了一个重要概念:任何这些属性都可能返回空值(有时候在 JavaScript 中称为 “未定义”)。比方说,文本节点的 nodeName 属性是空值(或者在一些浏览器中称为 “未定义”),因为文本节点没有名称。如您所料,nodeValue 返回节点的文本。

类似地,元素有 nodeName,即元素名,但元素的 nodeValue 属性值总是空。属性同时具有 nodeName 和 nodeValue。下一节我还将讨论这些单独的类型,但是因为这些属性是每个节点的一部分,因此在这里有必要提一提。

现在看看 清单 1,它用到了一些节点属性。

清单 1. 使用 DOM 中的节点属性


// These first two lines get the DOM tree for the current Web page,
// and then the element for that DOM tree
var myDocument = document;
var htmlElement = myDocument.documentElement;
// What's the name of the element? "html"
alert("The root element of the page is " + htmlElement.nodeName);
// Look for the element
var headElement = htmlElement.getElementsByTagName("head")[0];
if (headElement != null) {
alert("We found the head element, named " + headElement.nodeName);
// Print out the title of the page
var titleElement = headElement.getElementsByTagName("title")[0];
if (titleElement != null) {
 // The text will be the first child node of the <title> element</title>
 var titleText = titleElement.firstChild;
 // We can get the text of the text node with nodeValue
 alert("The page title is '" + titleText.nodeValue + "'");
}
// After is
var bodyElement = headElement.nextSibling;
while (bodyElement.nodeName.toLowerCase() != "body") {
 bodyElement = bodyElement.nextSibling;
}
// We found the element...
// We'll do more when we know some methods on the nodes.
}




节点方法
接下来看看所有节点都具有的方法(与节点属性一样,我省略了实际上不适用于多数 HTML DOM 操作的少数方法):

  • insertBefore(newChild, referenceNode) 将 newChild 节点插入到 referenceNode 之前。记住,应该对 newChild 的目标父节点调用该方法。

  • replaceChild(newChild, oldChild) 用 newChild 节点替换 oldChild 节点。

  • removeChild(oldChild) 从运行该方法的节点中删除 oldChild 节点。

  • appendChild(newChild) 将 newChild 添加到运行该函数的节点之中。newChild 被添加到目标节点孩子列表中的末端。

  • hasChildNodes() 在调用该方法的节点有孩子时则返回 true,否则返回 false。

  • hasAttributes() 在调用该方法的节点有属性时则返回 true,否则返回 false。


注意,大部分情况下所有这些方法处理的都是节点的孩子。这是它们的主要用途。如果仅仅想获取文本节点值或者元素名,则不需要调用这些方法,使用节点属性就可以了。清单 2 在 清单 1 的基础上增加了方法使用。

清单 2. 使用 DOM 中的节点方法


// These first two lines get the DOM tree for the current Web page,
// and then the <html> element for that DOM tree
var myDocument = document;
var htmlElement = myDocument.documentElement;
// What's the name of the <html> element? "html"
alert("The root element of the page is " + htmlElement.nodeName);
// Look for the <head> element
var headElement = htmlElement.getElementsByTagName("head")[0];
if (headElement != null) {
    alert("We found the head element, named " + headElement.nodeName);
    // Print out the title of the page
    var titleElement = headElement.getElementsByTagName("title")[0];
    if (titleElement != null) {
        // The text will be the first child node of the <title> element
        var titleText = titleElement.firstChild;
        // We can get the text of the text node with nodeValue
        alert("The page title is '" + titleText.nodeValue + "'");
    }
    // After <head> is <body>
    var bodyElement = headElement.nextSibling;
    while (bodyElement.nodeName.toLowerCase() != "body") {
        bodyElement = bodyElement.nextSibling;
    }
    // We found the <body> element...
    // Remove all the top-level <img> elements in the body
    if (bodyElement.hasChildNodes()) {
        for (i=0; i<bodyElement.childNodes.length; i++){
            var currentNode = bodyElement.childNodes[i];
            if (currentNode.nodeName.toLowerCase() == "img") {
                bodyElement.removeChild(currentNode);
            }
        }
    }
}



测试一下!
目前虽然只看到了两个例子,清单 1 和 2,不过通过这两个例子您应该能够了解使用 DOM 树能够做什么。如果要尝试一下这些代码,只需要将 清单 3 拖入一个 HTML 文件并保存,然后用 Web 浏览器打开。

清单 3. 包含使用 DOM 的 JavaScript 代码的 HTML 文件


<html>
<head>
<title>JavaScript and the DOM</title>
<script language="JavaScript">
    function test() {
    // These first two lines get the DOM tree for the current Web page,
    // and then the <html> element for that DOM tree
    var myDocument = document;
    var htmlElement = myDocument.documentElement;
    // What's the name of the <html> element? "html"
    alert("The root element of the page is " + htmlElement.nodeName);
    // Look for the <head> element
    var headElement = htmlElement.getElementsByTagName("head")[0];
    if (headElement != null) {
        alert("We found the head element, named " + headElement.nodeName);
        // Print out the title of the page
        var titleElement = headElement.getElementsByTagName("title")[0];
        if (titleElement != null) {
            // The text will be the first child node of the <title> element
            var titleText = titleElement.firstChild;
            // We can get the text of the text node with nodeValue
            alert("The page title is '" + titleText.nodeValue + "'");
        }
        // After <head> is <body>
        var bodyElement = headElement.nextSibling;
        while (bodyElement.nodeName.toLowerCase() != "body") {
            bodyElement = bodyElement.nextSibling;
        }
        // We found the <body> element...
        // Remove all the top-level <img> elements in the body
        if (bodyElement.hasChildNodes()) {
            for (i=0; i<bodyElement.childNodes.length; i++) {
                var currentNode = bodyElement.childNodes[i];
                if (currentNode.nodeName.toLowerCase() == "img") {
                    bodyElement.removeChild(currentNode);
                }
            }
        }
    }
}
</script>
</head>
<body>
<p>JavaScript and DOM are a perfect match. You can read more in <i>Head Rush Ajax</i>.</p>
<img src="http://www.headfirstlabs.com/Images/hraj_cover-150.jpg" />
<input type="button" value="Test me!" onClick="test();" />
</body></html>



API 设计问题
再看一看各种节点提供的属性和方法。对于那些熟悉面向对象(OO)编程的人来说,它们说明了 DOM 的一个重要特点:DOM 并非完全面向对象的 API。首先,很多情况下要直接使用对象的属性而不是调用节点对象的方法。比方说,没有 getNodeName() 方法,而要直接使用 nodeName 属性。因此节点对象(以及其他 DOM 对象)通过属性而不是函数公开了大量数据。

其次,如果习惯于使用重载对象和面向对象的 API,特别是 Java 和 C++ 这样的语言,就会发现 DOM 中的对象和方法命名有点奇怪。DOM 必须能用于 C、Java 和 JavaScript(这只是其中的几种语言),因此 API 设计作了一些折衷。比如,NamedNodeMap 方法有两种不同的形式:

  • getNamedItem(String name)

  • getNamedItemNS(Node node)


对于 OO 程序员来说这看起来非常奇怪。两个方法目的相同,只不过一个使用 String 参数而另一个使用 Node 参数。多数 OO API 中对这两种版本都会使用相同的方法名。运行代码的虚拟机将根据传递给方法的对象类型决定运行哪个方法。

问题在于 JavaScript 不支持这种称为方法重载 的技术。换句话说,JavaScript 要求每个方法或函数使用不同的名称。因此,如果有了一个名为 getNamedItem() 的接受字符串参数的方法,就不能再有另一个方法或函数也命名为 getNamedItem(),即使这个方法的参数类型不同(或者完全不同的一组参数)。如果这样做,JavaScript 将报告错误,代码不会按照预期的方式执行。

从根本上说,DOM 有意识地避开了方法重载和其他 OO 编程技术。这是为了保证该 API 能够用于多种语言,包括那些不支持 OO 编程技术的语言。后果不过是要求您多记住一些方法名而已。好处是可以在任何语言中学习 DOM,比如 Java,并清楚同样的方法名和编码结构也能用于具有 DOM 实现的其他语言,如 JavaScript。

让程序员小心谨慎
如果深入研究 API 设计或者仅仅非常关注 API 设计,您可能会问:“为何节点类型的属性不能适用于所有节点?” 这是一个很好的问题,问题的答案与政治及决策关系更密切,而非技术原因。简单地说,答案就是,“谁知道!但有点令人恼火,不是吗?”

属性 nodeName 意味着允许每种类型的节点都有一个名字,但是很多情况下名字要么未定义,要么是对于程序员没有意义的内部名(比如在 Java 中,很多情况下文本节点的 nodeName 被报告为 “#text”)。从根本上说,必须假设您得自己来处理错误。直接访问 myNode.nodeName 然后使用该值是危险的,很多情况下这个值为空。因此与通常的编程一样,程序员要谨慎从事。

通用节点类型
现在已经介绍了 DOM 节点的一些特性和属性(以及一些奇特的地方),下面开始讲述您将用到的一些特殊节点类型。多数 Web 应用程序中只用到四种节点类型:

  • 文档节点表示整个 HTML 文档。

  • 元素节点表示 HTML 元素,如 a 或 img。

  • 属性节点表示 HTML 元素的属性,如 href(a 元素)或 src(img 元素)。

  • 文本节点表示 HTML 文档中的文本,如 “Click on the link below for a complete set list”。这是出现在 p、a 或 h2 这些元素中的文字。


处理 HTML 时,95% 的时间是跟这些节点类型打交道。因此本文的其余部分将详细讨论这些节点。(将来讨论 XML 的时候将介绍其他一些节点类型。)

文档节点
基本上所有基于 DOM 的代码中都要用到的第一个节点类型是文档节点。文档节点 实际上并不是 HTML(或 XML)页面中的一个元素而是页面本身。因此在 HTML Web 页面中,文档节点就是整个 DOM 树。在 JavaScript 中,可以使用关键字 document 访问文档节点:

// These first two lines get the DOM tree for the current Web page,
// and then the <html> element for that DOM tree
var myDocument = document;
var htmlElement = myDocument.documentElement;

JavaScript 中的 document 关键字返回当前网页的 DOM 树。从这里可以开始处理树中的所有节点。

也可使用 document 对象创建新节点,如下所示:

  • createElement(elementName) 使用给定的名称创建一个元素。

  • createTextNode(text) 使用提供的文本创建一个新的文本节点。

  • createAttribute(attributeName) 用提供的名称创建一个新属


这里的关键在于这些方法创建节点,但是并没有将其附加或者插入到特定的文档中。因此,必须使用前面所述的方法如 insertBefore() 或 appendChild() 来完成这一步。因此,可使用下面的代码创建新元素并将其添加到文档中:
var pElement = myDocument.createElement("p");
var text = myDocument.createTextNode("Here's some text in a p element.");
pElement.appendChild(text);bodyElement.appendChild(pElement);
一旦使用 document 元素获得对 Web 页面 DOM 树的访问,就可以直接使用元素、属性和文本了。

元素节点
虽然会大量使用元素节点,但很多需要对元素执行的操作都是所有节点共有的方法和属性,而不是元素特有的方法和属性。元素只有两组专有的方法:

  1. 与属性处理有关的方法:

    • getAttribute(name) 返回名为 name 的属性值。
    • removeAttribute(name) 删除名为 name 的属性。

    • setAttribute(name, value) 创建一个名为 name 的属性并将其值设为 value。

    • getAttributeNode(name) 返回名为 name 的属性节点(属性节点在 下一节 介绍)。

    • removeAttributeNode(node) 删除与指定节点匹配的属性节



  2. 与查找嵌套元素有关的方法:
    • getElementsByTagName(elementName) 返回具有指定名称的元素节点列表。


这些方法意义都很清楚,但还是来看几个例子吧。

处理属性

处理元素很简单,比如可用 document 对象和上述方法创建一个新的 img 元素:
var imgElement = document.createElement("img");
imgElement.setAttribute("src", "http://www.headfirstlabs.com/Images/hraj_cover-150.jpg");
imgElement.setAttribute("width", "130");
imgElement.setAttribute("height", "150");
bodyElement.appendChild(imgElement);
现在看起来应该非常简单了。实际上,只要理解了节点的概念并知道有哪些方法可用,就会发现在 Web 页面和 JavaScript 代码中处理 DOM 非常简单。在上述代码中,JavaScript 创建了一个新的 img 元素,设置了一些属性然后添加到 HTML 页面的 body 元素中。

查找嵌套元素
发现嵌套的元素很容易。比如,下面的代码用于发现和删除 清单 3 所示 HTML 页面中的所有 img 元素:
// Remove all the top-level <img> elements in the body
if (bodyElement.hasChildNodes()) {
    for (i=0; i<bodyElement.childNodes.length; i++) {
    var currentNode = bodyElement.childNodes[i];
    if (currentNode.nodeName.toLowerCase() == "img") {
        bodyElement.removeChild(currentNode);
    }
}
}
也可以使用 getElementsByTagName() 完成类似的功能:
// Remove all the top-level <img> elements in the body
var imgElements = bodyElement.getElementsByTagName("img");
for (i=0; i<imgElements.length; i++) {
    var imgElement = imgElements.item[i];
    bodyElement.removeChild(imgElement);
}
属性节点
DOM 将属性表示成节点,可以通过元素的 attributes 来访问元素的属性,如下所示:
// Remove all the top-level <img> elements in the body
var imgElements = bodyElement.getElementsByTagName("img");
for (i=0; i<imgElements.length; i++) {
    var imgElement = imgElements.item[i];
    // Print out some information about this element
    var msg = "Found an img element!";
    var atts = imgElement.attributes;
    for (j=0; j<atts.length; j++) {
        var att = atts.item(j);
        msg = msg + "\n " + att.nodeName + ": '" + att.nodeValue + "'";
    }
    alert(msg);
    bodyElement.removeChild(imgElement);
}
需要指出的是,attributes 属性实际上是对节点类型而非局限于元素类型来说的。有点古怪,不影响您编写代码,但是仍然有必要知道这一点
虽然也能使用属性节点,但通常使用元素类的方法处理属性更简单。其中包括:

  • getAttribute(name) 返回名为 name 的属性值。

  • removeAttribute(name) 删除名为 name 的属性。

  • setAttribute(name, value) 创建一个名为 name 的属性并将其值设为 value


这三个方法不需要直接处理属性节点。但允许使用简单的字符串属性设置和删除属性及其值。

文本节点
需要考虑的最后一种节点是文本节点(至少在处理 HTML DOM 树的时候如此)。基本上通常用于处理文本节点的所有属性都属于节点对象。实际上,一般使用 nodeValue 属性来访问文本节点的文本,如下所示:
var pElements = bodyElement.getElementsByTagName("p");
for (i=0; i<pElements.length; i++) {
    var pElement = pElements.item(i);
    var text = pElement.firstChild.nodeValue;
    alert(text);
}
少数其他几种方法是专门用于文本节点的。这些方法用于增加或分解节点中的数据:


  • appendData(text) 将提供的文本追加到文本节点的已有内容之后。

  • insertData(position, text) 允许在文本节点的中间插入数据。在指定的位置插入提供的文本。

  • replaceData(position, length, text) 从指定位置开始删除指定长度的字符,用提供的文本代替删除



都是一些非常基本的关于节点的增删的属性和方法,虽然有一个用的比较多的getEmelentsByName没有讲,但是其他的也都是非常实用的了..

评论

此博客中的热门博文

远程记录OpenWRT日志

用OpenWRT打造自动翻墙路由器(详解篇)

转一下关于Fuck的用法