JavaScript权威指南--脚本化文档

知识要点   

脚本化web页面内容是javascript的核心目标。

第13章和14章解释了每一个web浏览器窗口、标签也和框架由一个window对象所示。每个window对象有一个document对象,document对象表示窗口的内容,它就是本章的主题。尽管如此,Document对象并非独立的,它是一个巨大的API的核心对象,叫做文档对象模型(Document Object Model ,DOM),它代表和操作文档的内容。

本章开始部分解释DOM的基本框架,然后进一步解释以下内容:

  • 如何在文档中查询或选取单独元素
  • 如何将文档作为节点树来遍历,如何找到文档元素的祖先、兄弟和后代元素
  • 如何查询和设置文档的元素的属性
  • 如何查询、设置和修改文档内容
  • 如何通过创建、插入和删除节点来修改文档结构
  • 如何与html表单一起工作

本章最后一节涵盖其它各种文档特性,包括referrer属性、write()方法和查询当前文档中选取的文档文本的技术等。

1.DOM概览

文档对象模型(DOM)是表示和操作HTML和XML文档内容的基础API。API不是特别复杂,但是需要了解大量的构架细节。首先:应该理解HTML或XML文档的嵌套元素在DOM树对象中的表示。HTML文档树的结构包含表示HTML标签或元素(如body,<p>)和表示文本字符串的字节,它也可能包含HTML注释的节点。考虑一下简单的HTML文档:

<html>
    <head>
        <title>title name</title>
    </head>
    <body>
        <h1>an html Document</h1>
        <p>this is a <i>simple</i>docuent</p>
    </body>
</html>    

在一个节点之上的直接节点是其父节点,在其下一层的直接节点是其子节点。在同一层上具有相同父节点的是兄弟及诶单。在一个节点之下的所有层级的一组节点是其后代节点。一个节点任何父节点和其上层的所有节点是祖先节点。

上图的每个方框是文档的一个节点,它表示一个Node对象,后续和第四部分会讲。上图中包含三种类型不同的节点。树形的根部是Document节点,它代表整个文档。 代表HTML元素的节点是Element节点,代表文本的节点是Text节点。Document、Element和Text是Node的子类,在第四部分中它们有自己的条目。Document和Element是两个重要的DOM类,本章大部分内容将删除它们的属性和方法。

下图展示了Node及其在类型层次结构中的子类型。注意,通用的Document和Element类型与HTMLDocument和HTMLElement类型之间有严格的区别。Document类型代表一个HTML或XML文档 ,Element类型代表了该文档中的一个元素。HTMLDocument和HTMLElement子类只是针对于HTML文档和元素。

值得注意的是,上图中HTMLElement的很多子类型代表HTML元素的具体类型。每个类型定义多个javascript属性,它们对应具体的元素或元素组,(本章15.4.1节)的HTML属性。有些具体元素也定义额外的属性和方法,它们并不是简单地映射HTML语法。第四部分涵盖这些类型及其额外的特性。

最后,上图还展示了到目前还未接触的一些节点类型,Comment节点代表HTML或XML的注释。由于注释基本上是文本字符串,因此它们很像表示文档中显式文本的Text节点。CharacterData通常是Text和Conmment的祖先,它定义这两种节点所共享的方法。Attr节点类型代表XML或HTML属性,但它几乎从不使用,因为和文档节点不同,Element类型定义了将属性当做“名/值”对使用方法。DocumentFragment类(未在上图显式)在实际文档中并不存在的一种节点:它代表一系列没有常规父节点的节点,对一些文档操作来说DocumentFragment非常有用,15.6.4节将涵盖这部分内容。DOM也定义了一些不常使用的类型,如像代表Doctype声明和xml处理指令等类型。

2.选取文档元素

大多数客户端javascript程序运行时总是在操作一个或多个文档元素,这些程序启动时,可以使用全局变量document来引用Document对象。但是,为了操作文档中的元素,必须通过某种方式来获得或选取这些文档元素的Element对象,DOM定义了很多方式来选取元素,查询文档的一个或多个元素有如下的方法:

2.1.通过id选取元素

id属性在文档中值必须唯一,可以用Document对象的getElementById()方法选取一个基于唯一ID的元素。

如果需要通过ID查找多个元素,可用下面的getElements()函数:

/**
 * 函数接受任意多的字符串参数
 * 每个参数将当做元素的id传给document.getElementById()
 * 返回一个对象,它把这些id映射到对应的Element对象
 * 如任何一个id对应的元素未定义,则抛出一个Error对象
 **/
function getElement( /*ID(s)*/ ) {
    var elements = {}; //开始是一个map映射对象
    for (var i = 0; i<arguments.length; i++) { //循环每个参数
        var id = arguments[i]; //参数是元素的id
        var elt = document.getElementById(id); //查找元素
        if (elt == null)
            throw new Error("No element with id: " + id); //抛出异常
        elements[id] = elt; //id和元素之间的映射
    }
    return elements; //对于元素映射返回id
}

在低于IE8版本的浏览器中,getElementById()对匹配元素不区分大小写,而且也返回匹配name属性的元素。

2.2.通过name名字获取元素

HTML的name属性最初打算为表单元素分配名字,在表单数据提交到服务器时使用该属性的值。类似id属性,name是给元素分配的名字,但是区别id,name的属性值不是必须唯一:多个元素可能有同样的名字,在表单中,单选和复选按钮通常是这样的情况。而且和id不一样的是name属性值是在少数HTML元素中有效,包括表单、表单元素、<iframe>和<img>元素。

基于name属性的值选取html元素,可以使用Document对象的getElementByName()方法。

getElementsByName()定义在HTMLDocument类中,而不在Document类中,所有它只征对HTML文档可用,在xml文档中不可用。它返回一个NodeList对象,后者的行为类似若干Element对象的只读数组在IE中,getElementByname()也返回id属性匹配中只读的元素。为了兼容,应该小心谨慎,不要将ID和name同名。

在14章7节中我们看到,为某些HTML元素设置name属性将自动为window对象中创建对于的属性,对Document对象也类似。为<form><img><iframe><applet><embed><object>元素(其中只有<object>元素没有后背对象)设置name属性值, 即在Document对象中创建以此name属性值为名字的属性。

如果给定的名字只有一个元素,自动创建的文档属性对于的值该是元素本身。如果有多个元素,该文档属性的值是一个NodeList对象,它表现为一个包含这些元素的数组。如14章7节所示,为若干命名<iframes>元素创建的文档属性比较特殊:它们指代这些框架的window对象而不是Element对象。这就意味这有些元素可以作为Document属性仅通过名字来选取:

 //征对<form name="shipping">元素,得到Element对象
var form = document.shipping;

在14章7节介绍了为什么不要用为窗口对象自动创建的属性,这同样适用用为文档对象自动创建的属性。如果需要查找命名的元素,最好显式地调用getElementByName()来查找它们。

2.3.通过标签名选取元素

Document对象的getElementsByTagName()方法可用来选取指定类型(标签名)的所有HTML或XML元素。元素不区分大小写。

类似于getElementByName(),getElementByTagName()返回一个NodeList对象(关于NodeList类,见本节补充的信息)。在NodeList中返回的元素按照在文档中的顺序排序的。

给getElementByTagName()传递通配符参数"*"将获得一个代表文档中所愿元素的NodeList对象。

Element定义getElementByTagName()方法,其原理和Document版本一样,但是它只选取调用该方法的元素的后代元素。因此,要查找文档的第一个<p>元素里所有<span>元素,代码如下:

var firstpara = document.getElementsByTagName("p")[0];
var firstParaSpan = firstpara.getElementsByTagName("span");

由于历史的原因,HTMLDocument类定义的一些快捷属性来访问各种各样的节点。例如images、forms和links等属性行为执行类似只读数组<img>、<from>和><a>(但只包含哪些有href属性的<a>标签)元素结合(可以在控制台输入document.images试试)。这些属性指代HTMLCollection对象,它们很像NodeList对象,但是除此之外它们可以用元素的id或名字来索引,我们已经看到用法如下的表达式来索引一个命名的<form>元素:

document.shiping;

用document.forms属性也可以更具体地引用命名(或有ID)表单,如下:

document.forms.shipping

还定义embeds和plugins属性,它们是同义词,是HTMLCollection类型的<embed>元素的集合。anchors是非标准属性,指代有一个name属性的<a>元素而不是一个href属性scripts在HTML5中是标准属性,是<script>元素的集合。

HTMLDocument对象还定义两个属性,它们指代包含特殊的单个元素而不是元素的集合。document.body是一个HTML文档的 <body>元素,document.head是<head>元素。这些属性总是会定义,如果文档没有显式包含它们,浏览器将隐式地创建它们。Document类的documentElement属性指代文档的根元素,在HTML文档中,它总指代<HTML>元素。

节点列表和HTML集合

节点列表和HTML集合 getElementByName()和getElementByTagName()都返回NodeList对象,类似document.images和document.forms的属性为HTMLCollection对象 这些对象都是只读的类数组对象(7章11节)

for (var i = 0; i < document.images.length; i++) //循环所有的图片
document.images[i].style.display = "none";

可以间接地使用调用Array的方法。

由于历史元素,NodeList和HTMLCollection对象也都能当做函数:以数字或字符串为参数调用它就如同使用数字或字符串索引它们一般,不鼓励这种怪异的方法。

NodeList和HTMLCollection对象都定义了item()方法,期望输入一个整数,并返回此处索引的元素。类似的,HtmlCollection定义了namedItem()方法,它返回指定属性名的值。但这些都没必要,因为可以直接用索引。

NodeList和HTMLCollection对象不是历史文档状态的一个静态快照,而通常是实时的,并且当文档变化时它们所包含的元素能随之改变,这是其中一个重要和令人惊讶的特性。假设在一个没有<div>元素的文档中调用getElementByTagName("div"),此时返回值是一个length为0的NodeList对象。如果再在此文档中插入一个新的<div>,元素将自动成为NodeList的一个成员,并且它的length属性变成1。

通常,NodeList和HTMLCollection的实时性非常有用,如果要迭代一个NodeList对象时再文档中添加或删除元素,首先要对NodeList对象生成一个静态的副本:

var snapshot = Array.prototype.slice.call(nodelist,0)

2.4.通过css类选取元素

HTML的class属性是用空格分开的有0个或者多个标识符的列表class是javascript的保留词,因此客户端JavaScript使用className属性来获得HTML中class的属性。class属性通常和CSS样式表一同使用将同样的样式作用在一系列元素之上,我们将在16章继续了解它。另外,HTML5定义了一个方法,getElementsByClassName(),允许我们通过class属性的标识符选择一系列文档中的元素。

类似getElementsByTagName(),可以作用在HTML文档和HTML元素上调用getElementsByClassName() ,返回一个实时的NodeList对象,包含所有文档或元素节点中匹配的后代节点getElementByClassName()只需要传入一个字符串参数,字符串参数可以使用空格隔开的标识符。只有当元素的class属性完全包含标识符的元素才会被匹配,但是标识符的顺序无关紧要。注意class属性和getElementsByClassName()方法都是用空格进行分隔,而不是逗号。下面是使用getElementsByClassName()的一些例子:

 // 找到所有class属性值为"warning"的元素
var warnings = document.getElementsByClassName("warning");
 // 查找奕log命名且包含有"error"和"fatal"类的元素的所有后端
var log = document.getElementById("log");
var total = log.getElementsByClassName("error fatal");

其中一个怪异行为就是在class属性中和CSS样式表中的类标识符是不区分大小写的。getElementsByClassName()使用样式表的匹配算法。当一个文档在怪异模式下运行时是大小写不敏感的,否则,该比较区分大小写。

2.5.通过css选择器选取元素

选择器用来描述文档中的若干或多组元素。本书不会详细介绍。

1.元素可以用ID、标签名或类描述;

2.元素可以给予属性值来选取:

p[lang = "fr"] //所有使用语法段落,如:<p lang="fr">
* [name = "x"] //所有包含name = "x"属性的元素

3.这些基本的选择器可以组合使用

span.fatal.error //其class中 包含"fatal"和"error"的所有<span>元素
span[lang = "fr"].warning //所有使用语法且class中包含"warning"的<span>元素

4.选择器可以指定文档结构

#log span //id="log"元素中所有的<span>元素
#log>span //id="log"元素的子元素中的所有<span>元素
body>h1:first-child //<body>的子元素中的第一个<h1>元素

5.选择器可以组合起来选取多个或多组元素

div,#log //所有<div>元素,以及id="log"的元素

CSS3选择器规范

与css3选择器的标准化一起的另一个称作“选择器API”的w3c标准定义了获取匹配一个给定选择器元素的javascript方法(选择器API标准不是HTML5的一部分,但与之有相关紧密联系,更多http://www.w3.org/TR/selectors-api/)该API的关键是Document方法querySelectorAll()。它接受包含一个css选择器的字符串参数,返回一个表示文档中匹配选择器的所有元素的NodeList对象。与前面描述的选取元素的方法是不同的,querySelectorAll()返回的nodeList对象并不是实时的:它包含在调用时刻选择器所匹配的元素,但它并不更新后续文档的变化。如果没有匹配的元素,querySelectorAll()将返回一个空的NodeList对象。如果选择器字符串非法,querySelectorAll()将抛出一个异常。

document.querySelector("#doc")      //选择id="doc"

除了querySelectorAll(),文档对象还定义了querySelector()方法。与querySelectorAll()工作原理相似,但它只返回第一个匹配的元素(以文档顺序)。或者如果没有匹配的元素则返回null。

css还定义了一些伪元素,":first-line"和"first-letter"等。在css中它们匹配的文本节点一部分不是实际元素。如果和querySelectorAll()或querySelector()一起使用它们是不匹配的。而且很多浏览器会拒绝返回“:link”和":visited"等伪类匹配结果,因为这会泄露用户的浏览历史记录。

当期所有的浏览器都支持querySelectorAll()和querySelector()方法。但是注意,这些方法的规范并不要求支持css3浏览器:鼓励浏览器和在样式表中一样的选择器集合。当前的浏览器除IE都支持css3选择器,IE7和IE8支持css2选择器。

querySelectorAll()是终级的选取元素的方法:它是一种强大的技术 ,通过让客户端javascript程序能够选择它们想要操作的元素。幸运的是,甚至在没有querySelectorAll()的原生支持的浏览器都可以使用css3选择器。jQuery库使用这种基于css选择器的查找作为它的核心编程规范式。基于jQuery的web应用程序使用一个轻便的、跨浏览器的、和querySelectorAll()等效的方法,命名为$().

jQuery的css3选择器匹配代码以及作为一个独立的标准库并发布了,命名为Sizzle(http://sizzlejs.com/)。它已经被Dojo和其它一些客户端库所采纳.

2.6.document.all[]

在DOM标准化之前,IE4引入了document.all[]集合来表示所有文档中的元素(除了Text节点)。document.all[]已经成被标准的方法,因为有上述方法可以获得元素,现在已经废弃不使用了,但是引入它是革命性的,它在以各种方式使用的已有代码中仍然能看到:

document.all[0] //文档中的第一个元素
document.all["nav"] //id或name为"nav"的元素(或多个元素)
document.all.nav //同上
document.all.tags("div") //文档中的所有div元素
document.all.tags("p")[0] //文档中的第一个<p>元素

3.文档结构和遍历

一旦从文档中选取了一个元素,有时需要查找文档中与之在结构上相关的部分(父亲、兄弟和子女)。

3.1.作为节点树的文档

Document对象、它的Element对象和文档中表示文本的Text对象都是Node对象。Node定义了以下重要属性:

  • parentNode:该节点父节点,或者针对类似Document对象的应该是null,因为它没有父节点
  • childNodes:只读的类数组对象(NodeList对象),它是该节点的子节点的实时表示
  • firstChildlastChild:该节点子节点的第一个和最后一个,如果该节点没有子字节点则为null
  • nextSiblingpreviousSibling:该节点的兄弟节点的前一个和下一个。具有相同父节点的两个节点为兄弟节点。节点的顺序反映了它们在文档中出现的顺序。这两个属性将节点之间的以双向链表的形式连接起来。
  • nodeType:该节点的类型,9代表Document节点,1代表Element节点,3代表Text节点,8代表Comment节点,11代表DocumentFragment节点
  • nodeValue:Text节点或Comment节点的文本内容
  • nodeName:元素的标签名,以大写形式表示

使用这些Node属性,可以用下面类似的表达式得到文档的第一个节点下面的第二个子节点的引用:

document.childNodes[0].childNodes[1];
document.firstChild.firstChild.nextSibling;
<html>
    <head>
        <title>test</title>
    </head>
    <body>
        hello world!
    </body>
</html>

那么第一个字节点下的第二个元素就是BODY,它的nodeType为1,nodeName为BODY。

3.2.作为元素树的文档

当将主要的兴趣点集中在文档中的元素上而并非它们之间的文本(和它们之间的空白)上时,我们可以使用另外一个更有用的API.它将文档看做是Element对象树,忽略部分文档:Text和Comment节点。

该API的第一部分是Element对象的children属性。类似ChildNodes,它是一个NodeList对象,但不同是的children列表只包含Element对象。Children非标准属性,但是在所有当前浏览器里都能工作。IE也实现好一段时间了,大多数浏览器也如法炮制,最后采纳的是Firefox3.5。

注意:Text和Comment节点没有children属性,它意味着上述Node.parentNode属性不可能返回Text或Comment节点。任何Element的parentNode总是另一个Element,或者追溯到树根的Document或DocumentFragment节点

基于元素的文档遍历API的第二部分是Element属性,后者类似Node对象的子属性和兄弟属性:

  • firstElementChild,lastElementChild:类似firstChild和lastChild,但只代表Element
  • nextElementSibling,previousElementSibing:类似nextSibing和previousSibling,但只代表兄弟Element
  • childElementCount:子元素的数量。返回的值和chilren.length值相等

子元素和兄弟元素的属性是标准属性,并在除IE之外的浏览器已经实现http://www.w3.org/TR/ElementTraversal/

由于逐个元素的文档遍历的API并未完全标准化,我们任然可以通过像下面的例子可移植的遍历函数那样实现这种功能:

 /*****可移植的遍历函数******/

/**
 * 返回元素e的第n层祖先元素,如果不存在此类祖先或祖先不是Element,例如(Document或者DocumentFragment)则返回null
 * 如果n为0,则返回e本身,如果n为1(或省略),则返回父元素。如果n为2,则返回祖父元素,依次类推
 **/
function parent(e, n) {
        if (n === undefined) n = 1;
        while (n-- && e) e = e.parentNode;
        if (!e || e.nodeType !== 1) return null;
        return e;
    }


    /**
     *返回元素e的第n个兄弟元素,如果n为正,返回后续的第n个兄弟元素;
     * 如果n为负,返回前面n个兄弟元素,如果n为零,返回e本身
     **/
function sibling(e, n) {
        while (e && n !== 0) { //如果e未定义,立刻返回它
            if (n > 0) { //查找后续的兄弟元素
                if (e.nextElementSibling) e = e.nextElementSibling;
                else {
                    for (e = e.nextSibling; e && e.nodeType !== 1; e = e.nextSibling)
                    /*空循环*/
                    ;
                }
                n--;
            } else { //查找前边的兄弟元素
                if (e.previousElementSibling) e = e.previousElementSibling;
                else {
                    for (e = e.previousElementSibling; e && e.nodeType !== 1; e = e.previousElementSibling)
                    /*空循环*/
                    ;
                }
                n++
            }
        }
        return e;
    }


    /**
     * 返回元素e的第n代子元素,如果不存在则为null
     * 负值n代表从后往前计数,0表示第一个子元素,而-1代表最后一个,-2代表倒数第二,依次类推
     **/
function child(e, n) {
    if (e.children) { //如果children数组存在
        if (n < 0) n += e.children.length; //转换负的n为数组索引
        if (n < 0) return null; //如果它仍为负,说明没有子元素
        return e.children[n]; //返回值指定的子元素
    }
    //如果e没有children数组,找到第一个字元素并向前数,或找到最后一个子元素并往回鼠
    if (n >= 0) { //非负,从第一个元素向前数
        //找到e的第一个子元素
        if (e.firstElementChild) e = e.firstElementChild;
        else {
            for (e = e.firstElementChild; e && e.nodeType !== 1; e = e.nextSibling)
            /*空循环*/
            ;
        }
        return sibling(e, n); //返回第一个子元素的第n个兄弟严肃
    } else { //n为负数,从第一个元素往回数
        if (e.lastElementChild) e = e.lastElementChild;
        else {
            for (e.lastElementChild; e && e.nodeType !== 1; e = e.previousElementSibling)
            /*空循环*/
            ;
        }
        return sibling(e, n + 1); //+1来转化最后1个子元素的最后1个兄弟元素
    }
}

自定义Element的方法

所有当前浏览器,都实现了DOM,故类似Element和HTMLDocument(IE8支持Element、HTMLDocument和Text的可扩展属性,但不支持Node,Document、HTMLDocument或HTMLElement更具体的子类型的可扩展属性)等类型都是想String和Array都是类。它们不是构造函数(将在本章后面看到如何创建新的Element对象),但它们有原型,可以用自定义方法扩展它。

。。。。。。

4.属性

HTML元素包含一个标签和一组称为属性(Attribute)的名/值对组成。例如:<a>元素定义了一个超链接,通过使用href属性来指向链接的目的地址。HTML元素的属性值在代表这些元素的HtmlElement对象的属性(property)中是可用的。DOM同样定义了一些API来设置和获取XML属性和非标准化的HTML属性。下面将详细介绍这些特性。

4.1.HTML属性作为Element属性

表示HTML文档中元素的HTMLElement对象定义了读/写对的属性,它们映射了元素的HTML属性。HTMLElement定义了通用的HTTP属性(id,title ,lang,dir)和像onclick这样的事件处理属性,特定Element的子类型定义了这些元素的特定属性。为了查询一个图片的URL地址,你可以使用表示<img>元素的HTMLElement的src属性:

var image = document.getElementById("myimage");
var imgurl = image.src;//src是图片的url
image.id === "myimage"; //true

相似地,你通过下面这样的代码设置<form>元素提交的属性:

var f = document.forms[0]; // First <form> in the document
f.action = "http://www.example.com/submit.php"; // Set URL to submit it to.
f.method = "POST";

HTML属性名是大小写不敏感的,但是JavaScript的属性名是大小写敏感的。从HTML属性名转化为JavaScript属性名应该采用小写。但是如果一个属性名包含不止一个单词,以驼峰方式命名:如defaultChecked和tabIndex。

有些HTML属性名在JavaScript中是保留字,所以一般都需要加"html"前缀。例如HTML中的for属性(<label元素>)在JavaScript中变成htmlFor属性。有一个例外:class属性在JavaScript用className属性,16章会讲。

有些HTML的属性的值通常是字符串。当属性为布尔值或数值(例如,<input>元素的defalultChecked和maxLength属性,)属性也是布尔值或数字,而不是字符串。事件处理程序属性值总是Function对象(或null)。HTML5规范定义了一个新的属性(如<input>和相关元素的form属性)用以ID转换为实际的Element对象。最后,任何元素的style属性值是CSSStyleDeclaration对象,而不是字符串,我们将在16章看到这个重要的属性值的更多信息。

注意:这个基于属性的API用来获取和设置属性,但没有定义任何从元素删除属性的方法。奇怪的是,delete操作符也无法完成此项目。下一节描述可以实现此目的方法。

4.2.取得和设置非标准的HTML属性

像上面描述的那样,HTMLElement及其子类型定义了一些属性,它们对应于元素的标准HTML属性。Element类型同样定义了getAttribute()和setAttribute()方法来查询和设置非标准的HTML属性,当然也可以用来查询和设置XML文档中元素上的属性。

var image = document.images[0];
var width = parseInt(image.getAttribute("width"));
image.setAttribute("class","firstImage");

从上面看出这些方法和前面的基于属性的API方法两个很重要的区别。首先,属性值都被视作字符串。getAttribute()不会返回数值、布尔值或者对象。其次,这些方法是用标准的属性名,简基于属性的API,甚至这些名称为javascript保留字时都不例外。对于HTML元素来说,属性名不区分大小写。

Element还定义了两个相关方法,hasAttribute()和removeAttribute(),检查命名属性是否存在和完全地删除这个属性。当属性为布尔值时这些方法特别有用:有些属性(如HTML的表单元素的disabled属性)在一个元素中是否存在是重点关键,而其他值无关紧要。

如果操作包含来自其他命名空间中属性的XML文档,你可以使用四种方法的命名空间版本:getAttributeNS(),setAttributeNS,hasAttributeNS()和removeAttributeNS()。这些方法需要两个属性名字符串作为参数。第一个是标识命名空间的URI,第二个通常是属性的本地名字,在命名空间中是无效的。但特别的,setAttributeNS()地第二个参数应该是属性的有效名字,它包含命名空间的前缀。可以在本书的第四部分中阅读更多关于命名空间的属性的方法。

4.3.数据集属性

有时在HTML元素上绑定上额外的信息是很有用的,特别是当JavaScript代码需要选择这些元素并按一定方法操纵他们的时候。有时可以通过为class属性添加特定的标识符来完成一些工作。其他时候,对于某些复杂的数据而言,客户端程序员会借助非标准的属性。如上文提到的,你可以使用getAttribute()和setAttribute()方法来读取和改写非标准属性的值。当然你所付出的代价是文档将不再是合法有效的HTML。

HTML5提供了一个解决方法,任意以小写data-作为前缀的属性名字都是合法的。这些“数据集属性”不会影响元素的表现,它们定义了标准的、附加额外数据的方法,并不是在文档合法性上做出让步。

HTML5也在Element对象上定义了dataset属性。这个属性指代一个对象,它的各个属性对应于去掉前缀data-属性因此dataset.x应该保持data-x属性的值。带连字符的属性对应驼峰命名的属性名:data-jquery-test属性变成dataset.jqueryTest属性。

<span class="sparkline" data-ymin="0" data-ymax="10">
 1 1 1 2 2 3 4 5 5 4 3 5 6 7 7 4 2 1
</span>

火花线是一个图像——通常是一条线——设计用来在文本流中显示。为了创建一个条火花线,也许可以如下代码提取上述的dataset属性的值 。

//假设ES5 Array.map() 方法 (或类似的方法)有定义
var sparklines = document.getElementsByClassName("sparkline");
for (var i = 0; i < sparklines.length; i++) {
    var elt = sparklines[i];
    var ymin = parseFloat(elt.getAttribute("data-ymin"));
    var ymin = parseFloat(elt.getAttribute("data-ymax"));
    var points = elt.getAttribute("data-points");
    var data = elt.textContent.split(" ").map(parseFloat);
    drawSparkline(elt, ymin, ymax, data); // 此方法未实现
}

或者可以用上述提到的获取属性的方法。

注意:dataset属性是(实现的时候会这样)元素的data-属性的实时、双向接口。设置或删除dataset的一个属性就等同于设置或移除对应元素的data-属性。

上面例子中的drawSparkline()函数是虚构的,21章会给出<canvas>绘制类似火花线的标记代码。

4.4.作为Attr节点的属性

还有一种使用Element的属性的方法。Node类型定义了attributes属性。针对非Element对象的任何节点,该属性为null。对于Element对象,attributes属性是只读的类数组对象,它代表元素的所有属性。类似NodeLists,attributes对象是实时的。它可以用数字索引访问,这意味着可枚举元素的所有属性。并且,它也可以用属性名索引:

document.body.attributes[0]; //<body>元素的第一个属性
document.body.attributes.bgColor //<body>元素的bgColor属性
document.body.attributes["ONLOAD"] //<body>元素的onload属性

当索引attributes对象时得到的值是Attr对象。Attr对象的一类特殊的Node,但从来不会像Node一样去用。Attr的name和value属性返回属性的名字和值。

5.元素的内容

看上面树状图的列子,并问自己一个问题<p>元素的“内容”是什么?回答这个问题有三个方法

  • 内容是HTML字符串"This is a <i>simle</i> document"
  • 内容是纯文本字符串“This is a simple document”.
  • 内容是一个Text节点、一包含了一个Text子节点的Element节点和另外一个Text节点

每一种回到都有效,后面几节我们将解释如何使用HTML表示、纯文本和元素内容树状表示

5.1.作为HTML的元素内容

读取Element的innerHTML属性作为字符串标记返回那个元素的内容。在元素上设置该属性调用了web浏览器的解释器,用新的字符串内容的解析展现形式替换元素当前内容。

web浏览器很擅长解析HTML,通常设置innerHTML效率非常高,甚至在指定的值需要解析时效率也相当不错,注意对innerHTML属性用“+=”操作符重复追加一小段文本的效率非常低下,因为它即要序列化又要解析。

html5到来它才使得innerHTML变得标准化。HTML5还标准化了outerHTML属性。当查询outerHTML时,返回HTML或XML标记字符串包含被查询元素的开头和结尾标签当设置元素的outerHTML时,元素本身被新内容所替换。只有Element节点定义了outerHTML属性,Document节点则无。

IE引入的另外一个特性是insertAdjacentHTML()方法,在HTML5标准化,它将任意HTML标记字符串插入到指定的元素“相邻”的位置。标记是该方法的第二个参数。并且相邻的精确含义依赖于第一个参数的值,第一个参数为具有以下值之一的字符串:"beforebegin"、"afterbegin"、"beforeend"、afterend、.这些值对于如下

后续有相关内容。

5.2.作为纯文本的元素内容

另一种方法处理元素的内容是当做一个子节点列表,每个子节点都可能有它自己一组子节点,当考虑元素的内容时,通常感兴趣的是它的Text节点。在XML文档中,你必须准备好处理CDATSection节点——它是Text的子类型,代表CDATA段的内容。

下面的例子展示了一个textContent()函数,它递归地遍历元素的子节点,然后连接后代节点中所有的Text节点文本,为了理解代码,必须回想一下nodeValue属性(定义在Node类型中),它保存Text节点的内容。

 /**查找元素的后代节点中所有的Text节点**/
 //返回元素e的纯文本内容,递归进入其子元素
 //该方法的效果类似于textContent属性
function textContent(e) {
    var child, type, s = ""; //s保存所有字节点文本
    for (child = e.firstChild; child != null; child = child.nextSibling) {
        type = child.nodeType;
        if (type === 3 || type === 4) //text和CDATASection节点
            s += child.nodeValue;
        else if (type === 1)
            s += textContent(child);
    }
    return s;
}

nodeValue属性可以读/写,设置它可以改变Text或CDATASection节点所显示的内容。Text和CDATA都是CharacterData子类型,可以在第四部分查看相关信息。CharacterData定义了data属性,它和nodeValue的文本相同,以下函数通过设置data属性将Text节点的内容转换为大写。

 //递归把n的后代子节点中的所有Text节点内容转换为大写形式
cd = document.getElementsByTagName("p")[0];

function upCase(n) {
    if (n.nodeType == 3 || n.nodeType == 4) //如果n是Text或CDATA节点
        n.data = n.data.toUpperCase(); //转换为大写
    else
        for (var i = 0; i < n.childNodes.length; i++)
            upCase(n.childNodes[i]);
}

 6.创建、插入、和删除节点

我们已经看到用HTML和纯文本字符串如何来查询和修改文档内容,也已经看到我们能够遍历Document来检查组成Document的每个Element和Text节点,在每个节点基本修改文档也是有可能的。Document类型定义了创建Element和Text对象的方法,Node类定义了在节点树中插入、删除和替换的方法。

function loadsync(url){
    var hand = document.getElementsByTagName("head")[0];
    var s = document.createElement("script");
    s.src = url;
    hand.appendChild(s);
}

6.1.创建节点

创建新的Element节点可以使用Document对象的createElement()方法。给方法传递元素的标签名:对HTML文档来说名字不容易区分大小写,对XML文档则区分大小写。

Text节点用类似的方法创建

var newnode = document.createTextNode("Text node");

Document也定义了一些其他的工厂方法,如不经常使用的createComment(),15.6.4节使用了createDocumentFragment()方法。在使用了XML命名空间的文档中,可以使用createElementNS()来同时指定命名空间的URI和待创建的Element标签的名字。

另一种创建新文档的方法是复制已经存在的节点。每个节点上有一个cloneNode()方法来返回该节点的一个全新副本。给方法传递参数true也能够递归地复制所有后代及节点,或传递参数false只是执行一个浅复制。在除了IE的其它浏览器中,Document对象还定义了一个类似的方法叫importNode().如果给它传递另一个文档的一个节点,它将返回一个适合本文档插入的节点的副本。传递true为第二参数,此方法将递归导入所有的后代节点。

6.2.插入节点

一旦有了有了一个新的节点,就可以用Node的方法appendChild()或insertBefore()将它插入到文档中.appendChild()是在需要插入的Element节点上调用的,它插入指定的节点将其成为那个节点的最后一个节点。

insertBefore()就像appendChild()一样,除了它接受两个参数,第一个是待插入的节点,第二个参数是已经存在的节点,新节点将插入该节点的前边该方法应该是在新节点的父节点上调用,第二个参数必须是该父节点的子节点。如果传递null作为第二个参数,insertBefore()的行为类似appendChild(),它将节点插入在最后。

这是一个在数字索引的位置插入节点的简单函数。同时展示了appendChild()和insertBefore()方法

 //将child节点插入到parent中,使其成为第n个子节点
function inserAt(parent, child, n){
    if (n < 0 || n > parent.childNodes.length) throw new Error("invalid index");
    else if (n == parent.childNodes.length) parent.appendChild(child);
    else parent.insertBefore(child, parent.childNodes[n]);
}

如果调用appendChild或insertBefore()将已存在的文档中的一个节点再次插入。那个节点将自动从它当前的位置删除并在新的位置重新插入:没必要显式删除改节点。下面的的例子展示了一个函数,基于表格指定列中单元格的值来进行行排序。它没有创建任何新的节点。只是用appendChild()改变已存在的节点。

 /**表格的行排序**/
 //根据指定表格每行第n个单元格的值,对第一个<tbody>中的进行排序
 //如果存在comparator函数则使用它,否则按字母表顺序比较
function sortrows(table, n, comparator) {
        var tbody = table.tBodies[0]; //第一个<tbody>,可能是隐式窗口的
        var rows = tbody.getElementsByTagName("tr"); //tbody中所有行
        rows = Array.prototype.slice.call(rows, 0); //真实的数组
        //基于第n个<td>元素的值对行排序
        rows.sort(function(row1, row2) {
            var cell1 = row1.getElementsByTagName("td")[n]; //获得第n个单元格
            var cell2 = row2.getElementsByTagName("td")[n]; //两行都是
            var val1 = cell1.textContent || cell1.innerText; //获得文本内容
            var val2 = cell2.textContent || cell2.innerText; //同上,两单格都是
            if (comparator) return comparator(val1, val2); //    进行比较
            if (val1 < val2) return -1;
            else if (val1 > val2) return 1;
            else return 0;
        });
        //在tobody中按他们的顺序把行添加到最后
        //这将自动把它们从当前位置移走,故没必要预先删除它们
        //如果<tbody>还包含除了<tr>的任何其他元素,这些节点都将会悬浮到顶部位置
        for (var i = 0; i < rows.length; i++) tbody.appendChild(rows[i]);
    }
    //查找表格的<th>元素,假设只有一行,它们可以单击
    //以便单击列标题,按列对行排序。

function makeSortable(table) {
    var headers = table.getElementsByTagName("th");
    for (var i = 0; i < headers.length; i++) {
        (function(n) { //嵌套函数来创建本地域
            headers[i].onclick = function() {
                sortrows(table, n);
            };
        }(i)); //将i的全局变量赋值给局部变量n
    }
}

6.3.删除和替换节点

removeChild()就是从文档树中删除一个节点,但是请小心:该方法不是在待删除的节点上调用,而是(就像其名字的一部分“child”所暗示的一样)在其父节点上调用。在父节点上调用该方法,并将需要删除子节点作为方法参数传递给它。在文档中删除n节点。代码可以这样写:

n.parentNode.removeChild(n);

replaceChild()方法删除一个子节点并用一个新的节点取而代之。在父节点上调用该方法,第一个参数是新节点,第二个参数是要替代的节点,例如用一个文本字符串来代替节点n,可以这样写:

n.parentNode.replaceChild(document.createTextNode("[redactd]"),n);

以下的函数展示了replaceChild()的另一种用法

//用一个新的<b>元素替换n节点,并使n成为该元素的子节点
function embolden(n) {
    //假设参数为字符串而不是节点,将其当做元素的id
    if (typeof n == "string") n = document.getElementById(n);
    var parent = n.parentNode; // 获得n的父节点
    var b = document.createElement("b"); //创建一个b元素
    parent.replaceChild(b, n); //使用<b>元素替换节点n
    b.appendChild(n); //使你成为<b>元素的子节点
}

本章5.1介绍过元素的outerHTML属性,也解释了一些版本的firefox还未实现它。下面的例子将展示在firefox(和其它任何支持innerHTML的浏览器,要有一个可扩展的Element.prototype对象,还有一些方法来定义属性的getter和setter)如何来实现该属性。同时,代码还展示了removeChild()和cloneNode()的实际用法

。。。。。。

6.4.使用DocumentFragment

DocumentFragment是一种特殊的Node,它为其它节点创建一个临时的容器。像这样创建一个DocumentFragment:

var frag = document.createDocumentFragment();

像Document节点一样,DocumentFragment是独立的,而不是任何其它文档的一部分。它的parentNode总为null。但类似Element,它可以有任意多的子节点,可以用appendChild(),insertBefore()等方法来操作他们。

DocumentFragment的特殊之处在于它使得一组节点被当做一个节点来看待:如果给appendChild()、insertBefore()或replaceChild()传递一个DocumentFragment,其实是将该文档片段的所有子节点插入到文档中,而非片段本身。(文档片段的子节点从片段移动到文档中,文档片段清空以便重用)。下面的例子是函数使用DocumentFragment来倒序排列的一个节点的子节点。

 //倒序排列节点n的子节点
function reverseDome(n) {
    //创建一个DocumentFragment()
    var f = document.createDocumentFragment();
    //从后至前循环子节点,将每一个字节点移动到文档片段中
    //n的最后一个子节点变成第一个子节点
    //注意,给f添加一个节点,该节点自动会从n中删除
    while (n.lastChild) f.appendChild(n.lastChild);
    
    //最后将所有的子节点一次移动回n中
    n.appendChild(f);
}

下面的例子使用innerHTML属性和DocumentFragment实现insertAdjacentHTML()方法(5.1节)。还定义了一些名字更符合逻辑的HTML插入函数,可以替换让人迷惑的insertAdjacentHTML()API。内部工具函数fragment()代码中最有用的部分:它反映对一个指定的HTML字符串文本解析后的DocumentFragment。

。。。。。。

7.例子:生成目录表

本例子说明了如何为文档动态的创建一个目录表。它展示了上一节所描述的文档脚本化的很多概念、元素选取、文档遍历、元素属性设置、innerHTML属性设置和在文档中创建于插入新节点等。本例子的注释也比较详尽。

。。。。。。

8.文档和元素的几何形状和滚动

但有时候。判断一个元素精确的几个形状也是非常有必要的。例如:在16章我们看到利用css元素指定位置。如果想要css动态定位一个元素(如工具的提示或插图)到某个已经由浏览器定位后的普通元素的旁边,首先要判断那个元素的当前位置。

本节阐述了在浏览器窗口中完成文档的布局后,怎么才能在抽象的基于树的文档模型与几何形状的基于坐标的视图之间来回变换。本节描述的属性和方法以及在浏览器中实现有很长一段时间了,有些是IE特有的,有些到IE9才实现。大家可以参考W3C的标准化流程,作为CSSOM-View模块www.w3.org/TR/cssom-view/

8.1.文档坐标和视口坐标

元素的位置是以像素度量的,向右代表X坐标的增加,向下代表Y坐标的增加,但是,有两个不同的点作为坐标系的原点:元素的X和Y坐标可以相对于文档的左上角或者相对于在其中显示文档的视口的左上角。在顶级窗口和标签页中,”视口“只是实际显示文档内容的浏览器的一部分:它不包括浏览器的“外壳”(如菜单、工具条和标签页)。针对框架页中显示的文档,视口是定义了框架页的<iframe>元素。无论在何种情况下,当讨论元素的位置是,必须弄清楚所使用的坐标是文档坐标还是视口坐标。(注意,视口坐标有时也叫作窗口坐标)

如果文档比视口要小,或者说还未出现滚动,则文档的左上角就是视口的左上角,文档和视口坐标系统是同一个。但是一般来说,要在两种坐标系之间互相转换,必须加上或减去滚动的偏移量(scroll offset)。例如,在文档坐标中如果一个元素的Y坐标是200像素,并且用户已经把浏览器向下滚动了75像素,那么视口坐标中元素的Y坐标就是125像素。同样,在视口坐标中如果一个元素的X坐标是400像素,并且用户已经水平滚动了视口200像素,那么文档坐标中像素的X坐标中元素的X坐标就是600像素。

文档坐标比视口坐标更加基础,并且在用户滚动是他们不会发生变化。不过,在客户端编程中使用视口坐标是非常常见的。当使用CSS指定元素的位置时运用了文档坐标(16章)。但是最简单的查询元素位置的方法(15.8.2节)返回视口坐标中的位置。类似的,当为鼠标事件注册事件处理程序函数时,报告的鼠标指针的坐标是在视口坐标系中。

为了在坐标系中转换,我们需要判定浏览器窗口的滚动条的位置。Window对象的pageXoffset和pageYOffset属性在所有的浏览器中提供这些值,除了IE8及更早的版本以外。IE(和所有现代浏览器)也可以通过scrollLeft和scrollTop属性来获得滚动条的位置。令人迷惑的是,正常的情况下通过查找文档的根节点(document.documentElement)来获取这些属性,但是在怪异模式下,必须在文档的<body>元素(documeng.body)上查询它们。以下显示了如何简便的查询滚动条的位置。

/*查询窗口滚动条的位置*/
//以一个对象的x和y属性的方法返回滚动条的偏移量
function getScrollOffsets(w) {
    //使用指定才窗口,如果不带参数则使用当前窗口
    w = w || window;

    //除了IE8及更早的版本以外,其它的浏览器都能用
    if (w.pageXOffset != null) return {x: w.pageXOffset, y:w.pageYOffset};

    //对标准模式下的IE,或任何浏览器
    var d = w.document;
    if (document.compatMode == "CSS1Compat")
        return {x:d.documentElement.scrollLeft, y:d.documentElement.scrollTop};

    //怪异模式下的浏览器
    return { x: d.body.scrollLeft, y: d.body.scrollTop };
}

有的时候判断视口的尺寸也是非常有用的,例如:为了确定文档的那些部分是当前可见的,利用滚动偏移量查询视口的简单方法在IE8及更早的版本中无法工作。而且该技术在IE中的运行方式还要取决于浏览器的模式。下面的例子便捷的查询视口尺寸,注意,它和上面的代码十分相似。

 /*查询窗口的视口尺寸*/
 //作为一个对象的w和h属性返回视口的尺寸
function getViewportSize(w) {
    //使用指定的窗口,如果不带参数则使用当前窗口
    w = w || window;
    //除了ie8和更早的版本,其它浏览器都能用
    if (w.innerWidth != null) return {
        w: w.innerWidth,
        h: w.innerHeight
    };
    //对于标准模式下的IE或其任何浏览器
    var d = w.document;
    if (document.compatMode == "CSS1Compat")
        return {
            w: d.documentElement.clientWidth,
            h: d.documentElement.clientHeight
        };
    //对于怪异模式下的浏览器
    return{w:d.body.clientWidth,h:d.body.clientHeight};
}

8.2.查询元素的几何尺寸

判定一个元素尺寸和位置最简单的方法是调用它的getBoundingClientRect()方法。这个方法是在IE5中引入的。而现在所有的浏览器都实现 了,它不需要参数,返回一个有left,right,top,bottom的属性对象。left和top表示左上角的x和y坐标。right和bottom属性表示元素右下角的x和y坐标。

这个方法返回元素在窗口坐标的位置,为了转换甚至用户滚动浏览器窗口以后仍然有效的文档坐标,需要加上滚动的偏移量:

var box = e.getBoundingClientRect(); //获得视口在坐标中的位置
var offsets = getScrollOffsets(); //上面定义的工具函数
var x = box.left + offsets.x;
var y = box.top + offsets.y;

在很多浏览器(和w3c标准中),getBoundingClientRect()对象还包含width和height属性,但在原始的ie中未实现,为了简便,可以这样计算width和height属性

var box = e.getBoundingClientRect(); //获得视口在坐标中的位置
var w = box.width || (box.right - box.left);
var h = box.height || (box.bottom - box.top);

getBoundingClientRect()所返回的坐标包含元素的边框和内边距,但不包含元素的外边距。

"Client"指定了返回的矩形的坐标系,对于"Bounding"?浏览器在布局时块状元素(如图片)总是为矩形。但是,内联元素可能跨了多行,因此可能由多个矩形组成。比如一个被断成两行的斜体文本<i>,它的形状是由第一行的右边部分和第二行的左边部分两个矩形组成。如果内联元素调用getBoundingClientRect(),它返回"边界矩形"。如上述的<i>元素,边界矩形会包含两行的宽度。

如果想查询内联元素每个独立的矩形,调用getClientRects()方法来获得一个只读的类数组对象,它的每个元素类似于getBoundingClientRect()返回的矩形对象。

我们知道,例如getElementByTagName()这样的DOM方法返回的结果是“实时的”,当文档变化时这些结果变化时能自动更新,但是getBoundingClientRect()和getClientRects()所返回的矩形对象(和矩形列表)并不是实时的,他们只是调用方法时视觉状态的静态快照。用户在滚回或改变浏览器大小的时候并不会更新他们。

8.3.判断元素在某点

getBoundingClientRect()方法使我们能在视口中判定元素的位置,但有时我们反过来想,判定在视口中指定的位置有什么元素。这时我们可以用Document对象的elementFormPoint()方法来判定传递(x,y)坐标(视口坐标而非文档坐标),该方法返回指定位置的一个元素。由于该方法的算法还没有具体定下来,但是这个方法意图就是它返回在那个点里和最上面(z-index属性)的元素如果指定的点在视口以外,elementFromPoint()返回null.即使改点在转换为文档坐标后是完美有效的,返回值也一样。

8.4.滚动

例15-8展示了关于滚动条的位置查询,还有一种更简单的方法。Window对象的scrollTo()(和其同义词scroll())方法接受一个点的X和Y坐标(文档坐标),并作为滚动条的偏移量设置它们。也就是,窗口滚动到指定的点出现在视口的左上角。如果指定的点太接近文档的下边缘或右边缘,浏览器将尽量保证它和视口的左上角之间最近。但无法达到一致

window的scrollBy()和scroll和scrollTo类似,但是它的参数是相对的,并在当前滚动条的偏移量上增加

//每200毫秒向下滚动10像素,注意,他无法关闭
javascript:void setInterval(function(){scrollBy(0,10)},200);

通常,除了滚动到文档中用数字表示的位置,我们只是想它滚动使得文档中的某个元素可见。可以利用getBoundingClientRect计算元素的位置,并转换为文档坐标,然后用scrollTo方法达到目的。但是在需要显示的HTML元素上调用scrollIntoView方法更加方便。该方法保证了元素在视口中可见。默认情况下,它试图将元素的上边缘放在或尽量接近视口的上边缘。如果只传递false作为参数,它将试图将元素的下边缘放在或尽量接近视口的下边缘。只要有助于元素在视口内可见。浏览器也会水平滚动视口。

scrollIntoView()的行为与设置window.location.hash为一个命名锚点的名字后浏览器产生的行为类似。

8.5.关于元素尺寸、位置和溢出的更多信息

getBoundingClientRect()方法在所有当前浏览器都有定义,面对老式浏览器只能使用更老的技术来判定元素的尺寸和位置元素的尺寸比较简单:任何HTML元素的只读属性offsetWidth和offsetHeight以css像素返回它的屏幕尺寸。返回的尺寸包含元素的边框和内边距,除去了外边距。

所有HTML元素拥有offsetLeft和offsetTop属性来返回元素的x坐标和y坐标。对于很多元素,这些值是文档坐标,并直接指定元素的位置。但对于已定义元素的后代元素和一些其他元素(如表格单元),这些属性返回的坐标是相对于祖先元素的而不是文档。offsetParent属性指定这些属性所相对的父元素。如果offsetParent为null,这些属性都是文档坐标,因此,一般来说,用offsetLeft和offsetTop来计算元素e的位置需要一个循环:

function getElementPosition(e){
  var x=0,y=0;
  while(e!=null){
    x+=e.offsetLeft;
    y+=e.offsetTop;
    e+=e.offsetParent;
  }
  return {x:x,y:y};
}

通过循环offsetParent对象链来累加偏移量,该函数计算指定元素的文档坐标。但是这个函数也不总是返回正确的值,下面看看如何修复。

除了这些名字以offset开头的属性以外,所有的文档元素定义了其他两组属性,其名称一组以client开头,另一组以scroll开头。即,每个HTML元素都有一些这些属性:

为了理解这些client和scroll属性,你需要知道HTML元素的实际内容有可能比分配用来容纳内容的盒子更大,因此单个元素可能有滚动条。内容区域是视口,就像浏览器的窗口,当实际比视口更大时,需要把元素的滚动条位置考虑进去

clientWidth和clientHeight类似offsetWidth和offsetHeight,不同的是它们不包含边框大小,只包含内容和它的内边距。同时,如果浏览器咋内边距和边框之间添加了滚动条,clientWidth和clientHeight在其返回值中也不包含滚动条。注意对于类似<i> <code> <span>这些内联元素,clientWidth和clientHeight总是返回0。例15-9有用的。有一个特殊的案例,在文档的根元素上查询这些属性时,它们的返回值和窗口的innerWidth和innerHeight属性值相等。

clientLft和clientTop属性没什么作用:它们返回元素的内边距的外边缘和它的边框的外边缘之间额水平距离和垂直距离,通常这些值就等于左边和上边的边框宽度如果元素有滚动条,并且浏览器将这些滚动条放置在左侧或顶部,这两属性也就包含滚动条的宽度。对于内联元素,它们总是为0

scrollWidth和scollHeight是元素的内容区域加上它内边距再加上任何溢出内容的尺寸当内容正好和内容区域匹配而没有溢出时,这些属性和clientWidth和cliengHeight是相等的。但当溢出时,它们就包含溢出的内容,返回值比clientWidth和clientHeight要大

最后,scrollLeft和scrollTop指定元素的滚动条的位置。注意,scrollLeft和scrollTop是可写的属性,通过设置它们来让元素中的内容滚动。(HTML元素并没有类似Window对象的scrollTo方法)

当文档包含可滚动的且有溢出内容的元素时,需要修改上面的getElementPosition()方法为getElementPos:从累计的偏移量中减去滚动条的位置,返回的位置从文档坐标转换为视口坐标:

。。。。。。

getElementPos()方法和getBoundingClientRect()的返回值一样。但是还是有较多兼容性问题,jQuery很好地解决这方面的问题。

9.HTML表单

HTML的<form>元素和各种各样的表单输入元素,如input,seclect和button。他们在客户端编程中有着重要的地位。这些html元素可以追溯到web最开始,比javascript更早。html表单就是第一代web应用程序背后的运行机制,它根不需要javascript。用户的输入从表单元素来收集;表单这些输入递交给服务器,服务器处理并生成一个新的HTML页面(通常有一个新的表单元素)并显示在客户端。

即使当整个表单数据都是由客户端javascript来处理并不会提交到服务器时,html表单仍是收集用户数据很好的方法。 在服务端程序中,表单必须要有一个“提交”按钮,否则它就没有用处。另一方面,在客户端编程中,“提交”按钮不是必须的。服务端程序是基于表单提交动作的——他们按表单大小的块处理数据——这限制了它们的交互性。客户端程序时基于事件的——他们可以对单独的表单元素上的事件作出相应——这使得它们有更好的响应度。比如用户打字时客户端程序能校验有效性。

表单由HTML元素组成就像HTML文档的其它部分一样,可以用文章中介绍的DOM来操作它们。但是表单是第一批脚本化的元素,在早期的客户端编程中它们还支持比DOM更早的其它API。

注意,本节是关于脚本化的HTML表单,而不是HTML本身。下文的列出常用的表单元素,更详细的内容参考第四部分的表单和表单元素的API,在form、input、option、select、textarea、请参考下面。

9.1.选取表单和表单元素

表单和它们所包含的元素可以用如getElementById()和getElementByTagName()等标准方法从文档中来选取。

在支持querySelectorAll()的浏览器中,从一个表单中选取所有的单选按钮或所有同名的元素代码如下

 //id为"shipping"的表单中的单选按钮
document.querySelectorAll('#shipping input[type="radio"]');
 //id为"shipping"的表单中所有name为"method"的单选按钮
document.qierySelectorAll('#shipping input[type="radio"][name="method"]')

尽管如此,如同在14.7节,15.2.2节和15.2.3所描述的,有name或id属性的<form>元素能够通过很多方法来选取。name="address"属性的form可以用下面任何的方法来选取

window.address //不可靠,不要使用
document.address //仅当表单有name属性时可用
document.forms.address //仅当方法有name或id的表单
document.forms[n] //不可靠:n是表单的序号

15.2.3节阐述了document.forms是一个HTMLCollection对象,可以通过数字序号或id或name来选取表单元素Form对象的本身行为类似多个表单元素组成的HTMLCollection集合,也可以通过那么或数字来索引。如果名为"address"的表单的第一个元素的name是"street",可以使用以下任何的一种表达式来引用该元素

document.forms.address[0];
document.forms.address.street;
document.address.street //当有name = "address",而不是id="address"

如果要明确选取一个表单元素,可以索引表单对象的elements属性:

document.forms.address.elements[0]
document.forms.address.elements.street

一般来说指定文档元素的方法用id属性要比name属性更佳。但是,name属性在html表单提交中有特殊的目的,它在表单中较为常用,在其它元素中较少使用。它应用于相关的复选按钮组和强制共享name属性值的、互斥的单选按钮组。请记住,当用name来索引一个HTMLCollection对象并并且包含多个元素来共享name时,返回值为一个类数组对象,它包含所有匹配的元素。考虑以下表单,它包含多个单选按钮来选择运输方式。

<form name="shipping">
    <fieldset>
        <legend>shipping method</legend>
        <label><input type="radio" name="method" value="1st">第一次</label>
        <label><input type="radio" name="method" value="2day">第二次</label>
        <label><input type="radio" name="method" value="3rd">第三次</label>
    </fieldset>
</form>

对于该表单,用如下代码来引用单选按钮的元素数组

var methods = document.forms.shipping.elements.method;

注意,<form>元素本身有一个HTML属性和对于的javascript属性叫"method",所有在此案例中,必须要用该表单的elements属性而非method属性。为了判定用户选取哪种运输方式,需要遍历数组中的表单元素,并检测他们的checked属性

var shipping_method;
for (var i = 0; i < methods.length; i++)
    if (methods[i].checked) shipping_method = method[i].value;

9.2.表单和元素的属性

上面描述的elements[]数组是Form对象中最有趣的属性 。Form对象中其他属性相对没有如此重要。action、encoding、method和target属性(property)直接对应于<form>元素的action、encoding、method个target等HTML属性(attribute)。这些属性都控制了表单是如何来提交数据倒web服务器并如何显示的。客户端javascript能够设置这些属性值,不过仅当表单真的会将数据提交到一个服务端程序时它们才有用。

在javascript产生之前,要有一个转义的“提交”按钮来提交表单,用一个专用的“重置”按钮来重置各元素的值。javascript的Form对象支持两个方法:submit()和reset(),它们完成同样的目的。调用Form对象的submit()方法来提交表单,调用reset()方法来重置表单元素的值。

所有表单元素一般都会有一下属性:

  • type:标识表单元素类型的只读的字符串。针对<input>标签定义的表单元素而言,就是其type属性的值。其它表单元素(如<textarea>和<select>)定义type属性是为了轻松标识它们。与input元素在类型检测时相互区别。
  • form:对包含元素Form对象的只读引用,或者如果元素没有包含在一个<form>元素中的值则为null
  • name:只读的字符串,由HTML属性name指定
  • value:可读/写的字符串,指定了表单元素包含或代表的“值”。它就是当提交表单时发送到web服务器的字符串,也是javascript程序有时候会感兴趣的内容。针对Text和Textarea元素,该属性值包含了用户输入的文本。针对用<input>标签创建的按钮元素(除了用<button>标签创建的按钮),该属性值指定了按钮显示的文本。但是,针对单选和复选按钮,该属性用户不可见也不能编辑。它仅是用HTML的value属性来设置一个字符串。它在表单提交的时候使用,但在关联表单元素的额外数据时也很有用。在本章后面有关不同类目的表单元素小节中将深入讨论value属性。

 9.3.表单和元素的事件处理程序

每个form元素都有一个onsubmit事件处理来完成侦测表单提交,还有一个onreset事件处理程序侦测表单重置。表单提交前调用onsubmit程序;它通过返回false能取消提交动作。这给javascript程序一个机会检查用户的输入错误。目的是为了避免或不完整或无效的数据提交到服务端程序。注意,onsubmit只能通过单击“提交”按钮来触发,直接调用表单的submit()方法不触发onsubmit事件处理程序

onreset事件处理程序和onsubmit是类似的。它在表单重置之前调用,通过返回false能够阻止表单元素被重置在表单中很少需要“重置”按钮,但如果有,你可能需要提醒用户来确认是否重置。

<form...
      onreset = "return confirm('你确定重置所有的输入从新输入码?')">
    ...
    <button type="reset">清楚并重新输入</button>
</form>

类似onsubmit事件处理程序,onreset它只能通过单击重置按钮来触发,直接调用表单的reset方法不能触发onreset事件处理程序。

当用户在表单元素交互时往往会触发click或change事件。通过定义onclick或onchange事件处理程序可以处理这些事件。表15-1有给出各个元素主要的事件处理程序。当用户改变其他表单元素所代表的值时会触发change事件。注意,在一个文本域中改事件不是每次输入一个值就会触发,它仅当用户改变元素的值然后将焦点移到其他元素时才会触发。

表单元素在收到键盘的焦点时也会触发 focus 事件,失去焦点时会触发blur事件

重要的一点:this的引用(17章)。它是触发该事件的文档元素的一个引用。form元素的事件处理程序能通过this.form来得到Form对象的引用。

9.4.按钮

按钮是常见的表单元素之一。按钮的本身没有默认的行为,除非它有onclick实际处理程序。否则它没有什么用处。以<input>定义的按钮会将value属性的值以纯文本显示。注意,超级链接与按钮一样提供了onclick事件处理程序。当onclick事件所触发的动作可以概念化为“跟随此链接”是就用一个链接;否则,用按钮。

提交和重置元素本就是按钮,不同的是它们之间有关联的默认动作(提交和重置)。

本书的第四部分末包含input按钮,关于按钮表单元素详细内容参看input项,它包含了用button元素创建的按钮。

9.5.开关按钮

复选框和单选元素都是开关按钮,或有两种视觉状态的按钮:选中或未选中HTML属性name的值都相同。注意,当复选框利用作为表单属性的名字来选中这些元素时,返回的是一个类数组对象而不是单个元素。

单选和复选框都定义了它的checked属性该属性是可读/写的布尔值,它指定了元素当前是否选中defaultChecked属性也是布尔值,它是HTML属性的checked值;它指定了元素第一次加载页面时是否被选中。

单选和复选框元素本身不显示任何文本,它们通常和相邻的HTML文本一起显示(或与<label>元素相关联)。这意味着设置复选框或单选元素的value属性不改变元素的视觉表现,设置value只改变提交表单时发送到web服务器的字符串。

当用户单击或复选开关按钮,单选或复选框元素触发onclick事件。如果由于单击开关按钮改变了它的状态,它也触发onchange事件。注意,当用户单击其他单选按钮而导致这个单选按钮状态的改变,后者不触发onchange事件。

9.6.文本域

“text”和“textarea”文本输入域是常见常用的元素,用户可以输入简短的文本字符串。value属性表示用户输入的内容。通过设置该属性值可以显示地指定应该在输入域中显示的文本。

在HTML5中,placeholder属性表示用户在输入框中提示的信息。

文本输入域的onchange事件处理程序表面用户完成编辑并将焦点移出文本域

“password”输入时显示星号,但这只是防止眼睛看见而已,网络上传输还是有可能被看到的。

“file”上传文件到服务器。有onchange事件处理程序。跟普通的输入域不同的是它的value是只读,这个防止恶意的JavaScript程序欺骗用户上传本意不想共享的文件。

不同的文本输入元素定义onkeypress、onkeydown和onkeyup事件处理程序。可以设置为返回false来防止记录用户的按键。

9.7.选择框和选项元素

select元素可以让用户做出一组选择(Option元素表示),浏览器通常将其渲染为下拉菜单的形式。但当指定size属性大于1时,它将显式为列表中的选项(可能有滚动)。<select>元素可能有两种不同的方式运作,这取决于type的属性值是如何设置的。如果<select>元素有multiple属性,也就是select对象的type属性值为"select-multiple",那就允许用户有多个选项。否则,如果没有多选项属性,那只能选择单个项目,它的type属性值为“select-one”。

某种程度上“select-multiple”元素与一组复选框元素相似,“select-one”元素与一组单选框元素相似。但是select元素不是开关按钮:它们有<option>元素定义。Select元素定义了options属性,它是一个包含多个option元素的类数组对象。

有onchange事件处理程序。

针对“select-one”,它的可读/写属性selectedIndex指定了哪个选项当前被选中。针对“select-multiple”元素,selectedIndex不足以表示一组选项。这时要判定哪些选项被选中,就必须遍历options[]数组的元素,并检测每个option对象的selected属性值

除了其select属性,每个option对象都有一个text属性它指定了select元素中的选项所显示的纯文本字符串。设置该属性可以改变显示给用户的文本value属性指定了在提交表单时仿到服务器的文本字符串,它可读写。甚至在写纯客户端程序并且不可能有表单提交时,value属性(或它所在的HTML属性value)是用来保存任何数据的好地方,在用户选取特定的选项时可以使用这些数据。

注意:Option元素并没有与表单相关的事件处理程序:用包含Select元素的onchange事件处理程序来代替。

通过设置options.length为一个希望的值可以截断option元素数组,而设置options。length为0可以从select元素移除所有的选项。设置options[]数组中某点的值为null可以从Select元素中移除某个option对象。这将删除该option对象,options[]数组中高端的元素自动移下来填补空缺.

为select元素增加一个新的选项,首先用Option()构造函数创建一个Option对象,然后将其添加到options[]属性中.

 //创建一个新的属性
var zaire = new Option("Zaire", // text属性
    "zaire", //value属性
    false, //defaultSelected属性
    false); //selected属性
//通过添加到options数组中,在Select元素中现在改选项
var countries = document.address.country;//得到Select对象
countries.options[countries.options.length] = zaire;

请牢记这一点,这些专用的Select元素的API已经很老了。可以用那行标准的调用更明确的插入和移除选项元素:Document.createElement(),Node.insertBefore(),Node.removeChild()等

10.其它文档特性

10.1.Document的属性

除了body、documentElement、forms外,还有其他有趣的属性:

  • cookie:允许javascript程序读、写HTTP cookie的特殊的属性。20章涵盖该属性
  • domain:该属性允许当前web页面之间的交互,相同域名下相互信任的web服务器之间协作放宽同源策略的安全限制(13.6.2节)
  • lastModified:包含文档修改的时间的字符串
  • location:与window对象的location属性引用同一个Location对象
  • referrer:如果有,它表示浏览器导航到当前链接的上一个文档。该属性和HTTP的Referer头部信息相同,只是拼写有两个r.
  • title:文档的<title></title>标签之间的内容
  • URL:文档的URL,只读字符串而不是Location对象。该属性与Location.href初始值相同,只是不包含Location对象的动态变化。例如,如果用户在文档中导向一个新的片段,Location.href会发生变化,但document.URL则不会。

referrer是这些属性中最有趣的属性之一:它包含用户连接到当前文档的上一个文档的URL,可以用如下代码使用该属性:

if (document.referrer.indexOf("http://www.google.com/search?") == 0) {
    var args = document.referrer.substring(ref.indexOf("?") + 1).split("&");
    for (var i = 0; i < args.length; i++) {
        if (args[i].substring(0, 2) == "q=") {
            document.write("<p>welcome google User.");
            document.write("You searched for:" + unescape(args[i].substring(2)).replace('+', ''))
                break;
        }
    }
}

10.2.document.write()方法

已经较少使用了。

document.write()方法会将其的字符串连接起来,然后让结果字符串插入到文档中调用它的脚本元素的位置。当脚本执行结束,浏览器解析生成的输出并显示它。如下

<script>
    document.write("<p>document title :" + document.title);
    document.write("<p>URL :" + document.URL);
    document.write("<p>Referred:" + document.referrer);
    document.write("<p>Modified on :" + document.lastModified);
    document.write("<p>Accessed :" + new Date());
</script>

只有在解析文档的时候才能使用write()方法输出HTML到文档中,理解这点非常重要,也就是说能在<script>元素的顶层代码中调用document.write(),就是因为这些脚本是文档解析流程的一部分。如果将其放在一个函数的定义中,而该函数的调用是从一个事件处理程序中发起的,它会擦除当前文档和它包含的脚本。同理,在设置了defer或async属性的脚本中不要使用。

还可以使用write()方法在其他的窗口或框架页中来创建整个全新文档。第一次调用其他文档的write()方法即会擦除该文档的所有内容。可以多次调用来逐步建立新文档的内容,会缓存起来知道对象的close()方法结束它。值得一提的是,Document对象还有支持writeln()方法。除了在其它参数输出之后加一个换行符以外它和wirte()方法完全一样。例如在<pre>元素内输出预格式化的文本时非常有用。

在当今的代码中document.wirte()方法并不常用,innerHTML属性和其它DOM技术提供了更好的方法增加内容。另一方面,某些算法的确能够使得他们本身称为很好的I/O API。如同write()方法提供的API一样。如果你正在书写在运行时计算和输出文本代码,可能会下面的例子感兴趣,它利用指定元素的innerHTML书写包装了简单的write()和close()方法。

 /*征对innerHTML属性的流式API*/
 //设置元素的innerHTML定义简单的“流式”API
function ElementStream(elt) {
        if (typeof elt === "string")
            elt = document.getElementById(elt);
        this.elt = elt;
        this.buffer = "";
    }
    //连接所有的参数,添加到缓存中
ElementStream.prototype.wirte = function() {
    this.buffer += Array.prototype.join.call(arguments, "");
};
 //类似write(),只是添加了换行符
ElementStream.prototype.writeln = function() {
    this.buffer += Array.prototype.join.call(arguments, "") + "\n";
};
 //从缓存中设置元素的内容,然后清空缓存
ElementStream.prototype.close = function() {
    this.elt.innerHTML = this.buffer;
    this.buffer = "";
};

10.3.查询和选取的文本

有时判定用户在文档中选取了那些文本非常有用,可以用类似如下的函数达到目的。

function getSelectedText() {
    if (window.getSelection) //HTML5标准API
        return window.getSelection().toString();
    else if (document.selection) //IE独有技术
        return document.selection.createRange().text;
}

标准的window.getSelection()方法返回一个Selection对象,后者描述了当前选取的一系列一个或多个Range对象。Selection和Range定义了一个不常用的的较为复杂的API.本书并没有记录。toString()方法是Selection对象中的最重要的广泛实现了(除了IE)特性,他返回选取的纯文本。

IE定义了一个不同的API,它在本书中也没有文档记录。Document.selection对象代表了用户的选择。该对象的createRange()方法返回IE独有的TextRange对象,它的text属性包含了选取的文本。

如上的代码在书签工具(13.2.5节)中特别有用,它选取操作的文本,然后利用搜索引擎或参考某个单词,例如,如下的HTML链接在Wikipedia上查找选取的文本,收藏书签后,该链接和它包含的javascript URL就编成了一个书签工具

<a href="javascript: var q;
  if (window.getSelection) q = window.getSelection().toString();
  else if (document.selection) q = document.selection.createRange().text;
  void window.open('https://www.baidu.com/s?wd=' + q)">选取后点击链接查找</a>

上述的代码兼容性不佳:window对象的getSelection()方法无法返回那些表单元素input或textarea内部的文本,它只返回在文档主体本身中选取的文本。另一方面,iE的Document.selection属性可以返回任意地方上选取的文本。

从文本输入域或textarea输入的元素可以获取的文本可以使用如下代码

elt.value.substring(elt.selectionStart,elt.selectionEnd);

IE8以及更早的版本浏览器不支持selectionStart,selectionEnd属性。

10.4.可编辑的内容

所有当今的web浏览器支持简单的HTML编辑功能;你已经看到在这个页面上使用了,如博客评论。它嵌入了一个文本编辑器,包含了一系列的按钮工具栏来设置排版样式等。

有两种办法启用编辑功能。其一,设置任何标签的HTML contentEditable属性;其二,设置对于元素的javascript contentEditable属性,这都是使元素的内容变得可编辑,当用户点击该元素的内容就会出现插入光标。用户敲击键盘就可以将内容插入其中。如下,一个HTML元素创建一个可编辑的区域:

<div id="editor" contenteditable>
click to edit
</div>

浏览器可能为表单字段和contenteditable元素支持自动拼写检查。在支持该功能的浏览器中,检查可能默认的开启或关闭。为元素添加spellcheck元素来显式开启拼写检查,而使用spellcheck=false来显式关闭该功能(例如在一个textarea将显式源代码或其它内容包含了字典找不到的标识符时。)

将Document对象的designMode属性设置为字符串"on"是的整个文档可编辑。(设置为off将恢复为只读文档),designMode属性并没有对应的HTML属性。如下代码使得<iframe>内部文档可编辑

 <iframe id="editor" src="about:blank"></iframe>    //空iframe
 <script>
onload = function() {
    var editor = document.getElementById("editor");
    editor.contentDocument.designMode = "on"; //开启编辑
}
</script>

所有当今浏览器都支持contenteditable和designMode属性。但还是不太兼容,比如enter有些另起一行有些开启新段落。

浏览器定义了多项文本编辑器命令,大部分没有键盘快捷。为了执行这些命令,应该使用Document对象execCommand()方法。(注意,这是Document的方法,而不是设置了contenteditable属性的元素方法。如果文档的元素中有多个可编辑的元素,命令将自动应用到选区或插入光标所在那个元素上),用execCommand()执行的命令名字都是如"bold","subscript","justifycenter","或"insertimage"之类的字符串。命令名是execCommand()第一个参数。有些命令还需要一个值的参数,例如:"createlink"需要一个超级链接URL。理论上,execCommand()第二个参数为true,浏览器会自动提示用户输入的所需值。但为了提高可移植性,你应该提示用户输入,并传递false为二个参数,传递用户输入的值作为第三个参数。

function bold() {
    document.execCommand("blod", false, url);
}

function link() {
    var url = prompt("输入link描述");
    if (url) document.execCommand("createlink", false, url)
}

execCommand()所支持的命令通常是工具栏上的按钮触发的。当要触发的命令不可用时,良好的UI会使对应的按钮无效。可以给document.queryCommandSupport()传递命令查询浏览器是否支持该命令。调用document.queryCommanEnabled()来查询当前所使用的命令。(例如,一条需要文本选择区域的命令在无选区的情况下,可能是无效的)有一些 命令,如"bold"和"italic"有一个布尔值状态,开关取决于当前选区或可以使用document.queryCommandState()。最后,有些命令,(如“fontname”)有一个相关的值(字体系列名)。用document.queryCommandValue()查询该值。如果当前文本选区了两种不同的字体,"fontname"是不确定的。使用document.querycpmmandIndeterm()来检测这种情况。

不同的浏览器实现了不同的编辑命令组合,只有一少部分得到了很好的支持。如“bold”,“italic”,“createlink”,“undo”和“redo”(互操作命令表,请参考http://www.quirksmode.org/dom/execCommand.html),HTML5草案定义了以下命令,但他们还没有没被普遍的支持,就不做详细的文档记录:

如果web需要一个富文本编辑器,就需要采纳一个预先构建的解决浏览器之间各种差异的解决方法。

一旦用户编辑了某元素,该元素的设置了conteneditable属性,就可以使用innerHTML属性得到已编辑内容的HTML标记。如何处理富文本自己决定(YUI和Dojo框架包含了编辑器组件,还有一些可选的方案,参考:http://en.wikipedia.org/wiki/online_rich-text_editor)。可以把它存储在隐藏的表单字段中。并通过提交表单发送到服务器。可以使用18章描述的技术直接把已编辑的文本发送到服务器。或者使用20章的计算在本地保存用户的编辑文本。

 

posted @ 2017-04-02 23:41  chenxj  阅读(792)  评论(0编辑  收藏  举报