张啸


世界上最快乐的事,莫过于为理想而奋斗。


JS(10) 脚本化文档

客户端JavaScript的存在使得静态的HTML文档变成了交互式的Web应用。脚本化Web页面内容是JavaScript的核心目标。每一个Web浏览器窗口、标签页和框架由一个Window对象所表示。每个Window对象有一个document属性引用了Document对象。Document对象表示窗口的内容,Document对象并非独立的,它是一个巨大的API中的核心对象,叫做文档对象模型(Document Object Model,DOM),它代表和操作文档的内容。

本文主要介绍以下内容:

  • 如何在文档中查询或选取单独的元素

  • 如何将文档作为节点树来遍历,如何找到任何文档元素的祖先、兄弟和后代元素

  • 如何查询和设置文档元素的属性

  • 如何通过创建、插入和删除节点来修改文档结构

  • 如何与HTML表单一起工作


一、DOM概览

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

1
2
3
4
5
6
7
8
9
<html>
<head>
<title>Sample Document</title>
</head>
<body>
<h1>An HTML Document</h1>
<p>This is a <i>simple</i> document.
</body>
</html>

二、选取文档元素

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

  • 用指定的id属性

  • 用指定的name属性

  • 用指定的标签名字

  • 用指定的css类

  • 匹配指定的css选择器

1. 通过ID获取元素

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

1
var section1 = document.getElementById('section1');

这是最简单和常用的选取元素的方法。如果想要操作某一组指定的文档元素,提供这些元素的id属性值,并使用id查找这些Element对象。如果需要通过ID查找多个元素,会发现下面的getElements()函数非常有用

1
2
3
4
5
6
7
8
9
10
11
function getElements(/*ids...*/)
var elements = {};
for(var i = 0; i < arguments.length; i++) {
var id = arguments[i];
var elt = document.getElementById(id);
if (elt == null)
throw new Error('No element with id: ' + id);
elements[id] = elt;
}
return elements;
}

2. 通过名字选取元素

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

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

1
var radiobuttons = document.getElementsByName('favorite_color');

getElementsByName()定义在HTMLDocument类中,而不再Document类中,所以它只针对HTML文档可用,在XML文档中不可用。它返回一个NodeList对象,后者的行为类似一个包含若干Element对象的只读数组。在IE9及以下版本中,getElementsByName()也返回id属性匹配指定值的元素。为了兼容,应该小心谨慎,不要将同样的字符串同时用作名字和id。

3. 通过标签名选取元素

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

1
var spans = document.getElementsByTagName('span');

类似于getElementsByName()getElementsByTagName()返回一个NodeList对象,返回的元素按照在文档中的顺序排序。给getElementsByTagName()传递通配符参数“*”将获得一个代表文档中所有元素的NodeList对象。

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

1
2
var firstpara = document.getElementsByTagName('p')[0];
var firstParaSpans = firstpara.getElementsByTagName('span');

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

4. 通过css类选取元素

HTML元素的class属性值是一个以空格隔开的列表,可以为空或包含多个标识符。它描述一种方法来定义多组相关的文档元素:在它们的class属性中有相同标识符的任何元素属于改组的一部分。在JavaScript中clsss是保留字,所以客户端JavaScript使用className属性来保存HTML的class属性值。class属性通常与css样式表一起使用,对某组内的所有元素应用相同的样式。

类似getElementsByTagName(),在HTML文档和HTML元素上都可以调用getElementsByClassName(),它的返回值是一个实时的NodeList对象,包含文档或元素所有匹配的后代节点。getElementsByClassName()只需要一个字符串参数,但是该字符串可以由多个空格隔开的标识符组成。只有当元素的class属性值包含所有的指定的标识符时才匹配,但是标识符的顺序是无关紧要的。

1
2
var warinings = document.getElementsByClassName('warning');
var log = document.getElementsByClassName('fatal error');

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

css样式表有一种非常强大的语法,那就是选择器,它用来描述文档中的若干或多组元素。Document对方提供了querySelectorAll()方法,用于根据选择器获取元素。它接收包含一个css选择器的字符串参数,返回一个表示文档中匹配选择器的所有元素的NodeList对象。与前面描述的选取元素的方法不同,querySelectorAll()返回的NodeList对象并不是实时的:它包含在调用时刻选择器所匹配的元素,但它并不更新后续文档的变化。如果没有匹配的元素,querySelectorAll()将会返回一个空的NodeList对象。如果选择器字符串非法,querySelectorAll()将抛出一个异常。

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

这两个方法在Element节点中也有定义。在元素上调用时,指定的选择器仍然在整个文档中进行匹配,然后过滤出结果集以便它只包含指定元素的后代元素。这看起来是违反常规的,因为它意味着选择器字符串能包含元素的祖先而不仅仅是上述所匹配的元素。

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

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

jquery的css选择器匹配代码已经作为一个独立的标准库提出来并发布了,命名为Sizzle。它已经被Dojo和其他一些客户端库所采纳,使用一个类似Sizzle的库的好处就是在老式浏览器中选取元素也能正常工作,并保证一个基准的选择器集合在所有的浏览器中都能运行。

6. document.all

在DOM标准化之前,IE引入了document.all集合来表示所有文档中的元素(除了Text节点)。document.all已经被标准的方法所取代,现在已经废弃不应该再使用了。


三、文档结构和遍历

一旦从文档中选取了一个元素,有时需要查找文档中与之在结构上相关的部分(父亲、兄弟和子女)。文档从概念上可以看作是一颗节点对象树。

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属性,可以用以下类似的表达式得到文档的第一个子节点下面的第二个子节点的引用

1
2
document.childNodes[0].childNodes[1];
document.firstNode.firstChild.nextSibling;

假设上述提到的文档代码如下:

1
<html><head><title>Test</title></head><body>Hello World!</body></html>

那么第一个子节点下面的第二个子节点就是<body>元素,它的nodeType为1,nodeName为“BODY”。

但请注意,该API对文档文本的变化极为敏感。例如,如果修改了文档,在<html><head>标签之间插入一个新行,那么表示该新行的Text节点就是文档的第一个子节点下面的第一个子节点,并且<head>元素就是第二个子节点而不是<body>元素了。

2. 作为元素树的文档

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

该API的第一部分是Element对象的children属性。类似ChildNodes,他也是一个NodeList对象,但不同的是children列表只包含Element对象。children并非标准属性,但是它在所有当前的浏览器中都能工作。

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

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

firstElementChild,lastElementChild:类似firstChildlastChild,但只代表子Element。

nextElementSibling,previousElementSibling:类似nextSiblingpreviousSibling,但只代表兄弟Element。

childElementCount:子元素的数量。返回的值和children.length值相等。

子元素和兄弟元素的属性是标准属性。由于逐个元素的文档遍历的API并未完全标准化,我们仍然可以通过如下的遍历函数来实现这种功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
/*
返回元素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.parent;
}
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) {
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.previousSibling; e && e.nodeType !== 1; e = e.previousSibling) {
// 空循环
}
}
n++;
}
}
return e;
}

/*
返回元素e的第n代子元素,如果不存在则为null
负值n代表从后往前技术。0表示第一个子元素,而-1代表最后一个
*/
function child(e, n) {
if (e.children) {
if (n < 0) {
// 转换负的n为数组索引
n += e.children.length;
}
if (n < 0) {
return null;
}
return e.children[n];
}

if (n >= 0) {
if (e.firstElementChild) {
e = e.firstElementChild;
} else {
for (e = e.firstChild; e && e.nodeType !== 1; e = e.nextSibling) {
// 空循环
}
}
return sibling(e, n);
} else {
if (e.lastElementChild) {
e = e.lastElementChild;
} else {
for (e = e.lastChild; e && e.nodeType !== 1; e = e.previousSibling) {
// 空循环
}
}
return sibling(e, n + 1);
}
}

3. 自定义Element的方法

所有当前浏览器都实现了DOM,故类似Element和HTMLDocument等类型都像String和Array一样是类。他们不是构造函数,但它们有原型对象,可以用自定义方法扩展它。

1
2
3
4
5
6
7
8
9
Element.prototype.next = function() {
if (this.nextElementSibling)
return this.nextElementSibling;
var sib = this.nextSibling;
while(sib && sib.nodeType !== 1) {
sib = sib.nextSibling;
}
return sib;
};

四、属性

HTML元素由一个标签和一组称为属性(attribute)的名值对组成。HTML元素的属性值在代表这些元素的HTMLElement对象的属性(property)中是可用的。DOM还定义了另外的API来获取或设置XML属性值和非标准的HTML属性。

1. HTML属性作为Element的属性

表示HTML文档元素的HTMLElement对象定义了读/写属性,它们影射了元素的HTML属性。HTMLElement定义了通用的HTTP属性(如id、标题lang和dir)的属性,以及事件处理程序属性(如onclick)。特定的Element子类型为其元素定义了特定的属性。例如,查询一张图片的url,可以使用表示<img>的HTMLElement对象的src属性:

1
2
3
var img = document.getElementById('myimage');
var imgurl = image.src;
image.id === 'myimage';

同样的,可以为一个<form>元素设置表单提交的属性

1
2
3
var f = document.forms[0];
f.action = 'http://www.example.com/submit.php';
f.method = 'post';

HTML属性名不区分大小写,但JavaScript属性名则大小写敏感。从HTML属性名转换到JavaScript属性名应该采用小写。但是,如果属性名包含不止一个单词,则将除了第一个单词以外的单词的首字母大写,例如defaultChecked和tabIndex。

有些HTML属性名在JavaScript中是保留字。对于这些属性,一般的规则是为属性名加前缀“html”。例如,HTML的for属性在JavaScript中变为htmlFor属性。但“class”在JavaScript中变为className。

表示HTML属性的值通常是字符串。当属性为布尔值或数值,属性也是布尔值或数值,而不是字符串。事件处理程序属性值总是Function对象(或null)。H5规范定义了一个新的属性(如<input>和相关元素的form属性)用以将元素ID转换为实际的Element对象。最后,任何HTML元素的style属性值是CSSStyleDeclaration对象,而不是字符串。

注意,这个基于属性的API用来获取和设置属性值,但没有定义任何从元素中删除属性的方法。奇怪的是,delete操作符也无法完成此目的。

2. 获取和设置非标准HTML属性

如上所述,HTMLElement和其子类型定义了一些属性,它们对应于元素的标准HTML属性。Element类型还定义了getAttribute()setAttribute()方法来查询和设置非标准的HTML属性,也可用来查询和设置XML文档中元素上的属性。

1
2
3
var image = document.images[0];
var width = parseInt(image.getAttribute('width'));
image.setAttribute('class', 'thumbnail');

上述代码给出了这些方法和前面的基于属性的API之间两个重要的区别。首先,属性值都被看作是字符串。getAttribute()不返回数值、布尔值或对象。其次,方法使用标准属性名,甚至当这些名称为JavaScript保留字时也不例外。对HTML元素来说,属性名不区分大小写。

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

3. 数据集属性

有时候在HTML元素上绑定一些额外的信息也是很有帮助的,当JavaScript选取这些元素并以某种方式操纵这些信息时就是很典型的情况。有时可以通过给class属性添加特殊的标识符来完成。其他时候针对更复杂的数据,客户端程序员会借助使用非标准的属性。如上所述,可以使用getAttribute()setAttribute()来读和写非标准属性的值。

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

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

4. 作为Attr节点的属性

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

1
2
3
document.body.attributes[0]
document.body.attributes.bgColor;
document.body.attributes['onload'];

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


五、元素的内容

1. 作为HTML的元素内容

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

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

innerHTML是IE4中引入的,虽然所有的浏览器都支持他已经有很长时段时间了,但随着H5的到来它才变得标准化。H5说innerHTML应该在Document节点以及Element节点上工作正常,但这还未被普遍地支持。

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

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

|<div id="target">|This is the element content|</div>|
↑                 ↑                           ↑      ↑
beforebegin   afterbegin                  beforeend  afterend

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

有时需要查询纯文本形式的元素内容,或者在文档中插入纯文本(不必转义HTML标记中使用的尖括号和&符号)。标准的方法是用Node的textContent属性来实现

1
2
3
var para = document.getElementsByTagName('p')[0];
var text = para.textContent;
para.textContent = 'hello world!';

textContent属性在除了低版本IE的所有当前浏览器中都支持。在IE中,可以用Element的innerText属性来代替。微软在IE 4中引入了innerText属性,它在除了低版本的FireFox的所有当前浏览器都支持。

textContentinnerText属性非常相似,通常可以互相替换使用。不过要小心空元素(在JavaScript中字符串””是假值)和未定义的属性之间的区别

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/*
一个参数,返回元素的textContent或innerText
两个参数,用value参数的值设置元素的textContent或innerText
*/
function textContent(element, value) {
var content = element.textContent;
if (value === undefined) {
if (content !== undefined)
return content;
else
return element.innerText;
} else {
if (content !== undefined)
element.textContent = value;
else
element.innerText = value;
}
}

textContent属性就是将指定元素的所有后代Text节点简单地串联在一起。innerText没有一个明确指定的行为,但是和textContent有一些不同。innerText不返回<script>元素的内容。它忽略多余的空白,并试图保留表格格式。同时,innerText针对某些表格元素(<table><tbody><tr>)是只读的属性。

3. 作为Text节点的元素内容

另一种方法处理元素的内容是当作一个子节点列表,每个子节点可能有它字迹的一组子节点。当考虑元素的内容时,通常感兴趣的是它的Text节点。

下面展示一个textContent()函数,它递归地遍历元素的子节点,然后连接后代节点中所有的Text节点的文本。

1
2
3
4
5
6
7
8
9
10
11
function textContent(e) {
var child, type, s = '';
for(child = e.firstChild; child != null; child = child.nextSibling) {
type = child.nodeType;
if (type === 3 || type === 4)
s += child.nodeValue;
else if (type === 1)
s += textContent(child);
}
return s;
}

nodeValue属性可以读/写,设置它可以改变Text或CDATASection节点所显示的内容。Text和CDATASection都是CharacterData的子类型。CharacterData定义了data属性,它和nodeValue的文本相同。以下函数通过设置data属性将Text节点的内容转换成大写形式

1
2
3
4
5
6
7
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]);
}

CharacterData还定义了一些在Text或CDATASection节点不太常用的方法来添加、删除、插入和替换文本。除了修改已存在Text节点的内容,还可以在Element中插入全新的Text节点或用新Text节点来替换已有节点。


六、创建、插入和删除节点

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

1
2
3
4
5
6
function loadasync(url) {
var head = document.getElementsByTagName('head')[0];
var s = document.createElement('script');
s.src = url;
head.appendChild(s);
}

1. 创建节点

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

Text节点用类似的方法创建:

1
var newnode = document.createTextNode('text node content');

Document也定义了一些其他的工厂方法,如不经常使用的createComment()createDocumentFragment()

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

2. 插入节点

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

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

如果调用appencChild()insertBefore()将已存在文档中的一个节点再次插入,那么节点将自动从它当前的位置删除并在新的位置重新插入:没有必要显式地删除该节点。

3. 删除和替换节点

removeChild()方法是从文档树中删除一个节点。但是请小心:该方法不是在待删除的节点上调用,而是在其父节点上调用。在父节点上调用该方法,并将需要删除的子节点作为方法参数传递给它。

1
n.parentNode.removeChild(n);

replaceChild()方法删除一个子节点并用一个新的节点取而代之。在父节点上调用该方法,第一个参数是新节点,第二个参数是需要代替的节点。

1
n.parentNode.replaceChild(document.createTextNode('[ REDACRTED ]', n));

4. 使用DocumentFragment

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

1
var frag = document.createDocumentFragment();

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

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

1
2
3
4
5
6
7
8
9
10
11
12
// 倒序排序节点n的子节点
function reverse(n) {
var f = document.createDocumentFragment();
// 从后至前循环子节点,将每一个子节点移动到文档片段中
// n的最后一个节点变成f的第一个节点,反之亦然
// 注意,给f添加一个节点,该节点自动地会从n中删除
while(n.lastChild)
f.appendChild(n.lastChild);

// 最后,把f的所有子节点一次性全部移回n中
n.appendChild(f);
}

下面代码使用innerHTML属性和DocumentFragment实现insertAdjacentHTML()方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
var Insert = (function() {
if (document.createElement('div').insertAdjacentHTML) {
return {
before: function(e, h) { e.insertAdjacentHTML('beforebegin', h); },
after: function(e, h) { e.insertAdjacentHTML('afterend', h); },
atStart: function(e, h) { e.insertAdjacentHTML('afterbegin', h); },
atEnd: function(e, h) { e.insertAdjacentHTML('beforeend', h); }
};
}

function fragment(html) {
var elt = document.createElement('div');
var frag = document.createDocumentFragment();
elt.innerHTML = html;
while(elt.firstChild)
frag.appendChild(elt.firstChild);
return frag;
}

var Insert = {
before: function(elt, html) {
elt.parentNode.insertBefore(fragment(html), elt);
},
after: function(elt, html) {
elt.parentNode.insertBefore(fragment(html), elt.nextSibling);
},
atStart: function(elt, html) {
elt.insertBefore(fragment(html), elt.firstChild);
},
atEnd: function(elt, html) {
elt.appendChild(fragment(html));
}
};

Element.prototype.insertAdjacentHTML = function(pos, html) {
switch(pos.toLowerCase()) {
case 'beforebegin': return Insert.before(this, html);
case 'afterend': return Insert.after(this, html);
case 'afterbegin': return Insert.atStart(this, html);
case 'beforeend': return Insert.atEnd(this, html);
}
};

return Insert;
}());

七、文档和元素的几何形状和滚动

在本文中,到目前为止我们考虑的文档被看作是元素和文本节点的抽象树。但是当浏览器在窗口中渲染文档时,它创建文档的一个视图表现层,在那里每个元素有自己的位置和尺寸。通常,Web应用程序可以将文档看作是元素的树,并且不用关心在屏幕上这些元素是如何渲染的。但有时,判定一个元素精确的几个形状也是非常有必要的。如果想用css动态定位一个元素到某个已经由浏览器定位后的普通元素的旁边,首先需要判定那个元素的当前位置。

本节阐述了在浏览器窗口中完成文档的布局以后,怎样才能在抽象的基于树的文档模型与几何形状的基于坐标的视图之间来回变换。

1. 文档坐标和视口坐标

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

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

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

为了在坐标系之间互相转换,我们需要判定浏览器窗口的滚动条的位置。Window对象的pageXOffset和pageYOffset属性在所有的浏览器中提供这些值。也可以通过scrollLeft和scrollTop属性来获得滚动条的位置。

令人迷惑的是,正常情况下通过查询文档的根节点(document.documentElement)来获取这些属性值,但在怪异模式下,必须在文档的<body>元素(document.body)上查询它们。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function getScrollOffset(w) {
w = w || window;

if (w.pageXOffset != null) {
return {
x: w.pageXOffset,
y: w.pageYOffset
};
}

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中的运行方式还要取决于浏览器处于怪异模式还是标准模式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function getViewportSize(w) {
w = w || window;

if (w.innerWidth != null) {
return {
w: w.innerWidth,
h: w.innerHeight
};
}

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
};
}

2. 查询元素的几何尺寸

判定一个元素的尺寸和位置最简单的方法是调用它的getBoundingClientRect()方法。该方法是在IE5中引入的,而现在当前的所有浏览器都实现了。它不需要参数,返回一个有left、right、top和bottom属性的对象。

这个方法返回元素在视口坐标中的位置。(getBoundingClientRect()方法名中的“Client”是一种间接指代,它就是Web浏览器客户端——专指它定义的窗口或视口。)为了转化为甚至用户滚动浏览器窗口以后仍然有效的文档坐标,需要加上滚动的偏移量

1
2
3
4
var box = e.getBoundingClientRect();
var offsets = getScrollOffsets();
var x = box.left + offsets.x;
var y = box.top + offsets.y;

getBoundingClientRect()返回的对象还包含width和height属性,但是在低版本IE中未实现

1
2
3
var box = e.getBoundingClientRect();
var w = box.width || (box.right - box.left);
var h = box.height || (box.bottom - box.top);

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

如果getBoundingClientRect()方法名中的“Client”指定了返回的矩形的坐标系,那么方法名中的“Bounding”作何解释呢?浏览器在布局时块状元素(如图片、段落和div元素等)总是为矩形。但是,内联元素(如span、code和b等)可能垮了多行,因此可能由多个矩形组成。想像一下,一些被断成两行的斜体文本(用<i></i>标记的)。它的形状是由第一行的右边部分和第二行的左边部分两个矩形组成的。如果在内联元素上调用getBoundingClientRect(),它返回“边界矩形”。对于如上描述的<i>元素,边界矩形会包含整整两行的宽度。

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

注意,getBoundingClientRect()getClientRects()所返回的矩形对象并不是实时的。在用户滚动或改变浏览器窗口大小时不会更新它们。

3. 判定元素在某点

getBoundingClientRect()方法使我们能在视口中判定元素的位置。但有时我们想反过来,判定在视口中的指定位置上有什么元素。这可以用Document对象的elementFromPoint()方法来判定。传递X和Y坐标(使用视口坐标而非文档坐标),该方法返回在指定位置的一个元素。选取元素的算法的意图是它返回在那个点的最里面的和最上面的元素。如果指定的点在视口以外,elementFromPoint()返回null,即使该点在转换为文档坐标后是完美有效的,返回值也一样。

elementFromPoint()方法看上去很有用,典型的案例是将鼠标指针的坐标传递给它来判定鼠标在哪个元素上。但是鼠标事件对象已经在target属性中包含了这些信息,因此实际上elementFromPoint()不经常使用。

4. 滚动

下文示例展示了如何在浏览器窗口中查询滚动条的位置,该例子中的scrollLeft和scrollTop属性可以用来设置让浏览器滚动。

1
2
3
4
var documentHeight = document.documentElement.offsetHeight;
var viewportHeight = window.innerHeight;

window.scrollTo(0, documentHeight - viewportHeight);

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

1
2
3
setInterval(function() {
scrollBy(0, 10)
}, 200);

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

scrollIntoView()的行为与设置window.location.hash为一个命名锚点(<a name="">元素)的名字后浏览器产生的行为类似。

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

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

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

1
2
3
4
5
6
7
8
9
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对象链来累加偏移量,该函数计算指定元素的文档坐标。这里不能对元素的位置就一锤定音,尽管如此——这个getElementPosition()函数也不总是计算正确的值。

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

1
2
3
4
5
offsetWidth     clientWidth     scrollWidth
offsetHeight clientHeight scrollHeight
offsetLeft clientLeft scrollLeft
offsetTop clientTop scrollTop
offsetParent

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

clientWidth和clientHeight类似offsetWidth和offsetHeight,不同的是它们不包含边框大小,只包含内容和它的内边距。同时,如果浏览器在内边距和边框之间添加了滚动条,clientWidth和clientHeight在其返回值中也不包含滚动条。注意,对于类似<i><code><span>这些内联元素,clientWidth和clientHeight总是返回0。

有一个特殊的案例,在文档的跟元素上查询这些属性时,它们的返回值和窗口的innerWidth和innerHeight属性值相等。

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

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

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


八、HTML表单

HTML的<form>元素和各种各样的表单输入元素在客户端编程中有着重要的地位。这些HTML元素可以追述到Web的最开始,比JavaScript更早。HTML表单就是第一代Web应用程序背后的运作机制,它根本就不需要JavaScript。用户输入从表单元素来收集;表单将这些输入递交给服务器;服务器处理输入并生成一个新的HTML页面显示在客户端。

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

HTML元素 类型属性 事件处理程序 描述和事件
<input type="button"><button type="button"> “button” onclick 按钮
<input type="checkbox"> “checkbox” onchange 复选按钮
<input type="file"> “file” onchange 载入Web服务器的文件的文件名输入域;它的value是只读的
<input type="hidden"> “hidden” none 数据由表单提交,但对用户不可见
<option> none none Select对象中的单个选项;事件处理程序在Select对象上,而非单独的Option对象上
<input type="password"> “password” onchange 密码输入框,输入的字符不可见
<input type="radio"> “radio” onchange 单选按钮,同时只能选定一个
<input type="reset"><button type="reset"> “reset” onclick 重置表单的按钮
<select> “select-one” onchange 选项只能单选的列表或下拉菜单
select multiple “select-multiple” onchange 选项可以多选的列表
<input type="submit"><button type="submit"> “submit” onclick 表单提交按钮
<input type="text"> “text” onchange 单行文本输入域;type属性缺少或无法识别时默认的input元素
<textarea> “textarea” onchange 多行文本输入域

1. 选取表单和表单元素

表单和它们所包含的元素可以用如getElementbyId()getElementsByTagName()等标准的方法从文档中来选取。在支持querySelectAll()的浏览器中,也可以通过这个方法获取对应的元素。有name或id属性的<form>元素能够通过很多方法来选取。name="address"属性的<form>可以用以下任何方法来选取

1
2
3
4
window.address;                                 // 不可靠:不要使用
document.address; // 仅当表单有name属性时可用
document.forms.address; // 显式访问有name或id的表单
document.forms[0]; // 不可靠:n是表单的序号

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

1
2
document.forms.address.elements[0];
document.forms.address.elements.street;

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

1
2
3
4
5
6
7
8
<form name="shipping">
<fieldset>
<legend>Shipping Method</legend>
<label><input type="radio" name="method" value="1st">First-class</label>
<label><input type="radio" name="method" value="2day">2-day Air</label>
<label><input type="radio" name="method" value="overnite">Overnight</label>
</fieldset>
</form>

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

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

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

1
2
3
4
var shipping_method;
for (var i = 0; i < methods.length; i++)
if (methods[i].checked)
shipping_method = methods[i].value;

2. 表单和元素的属性

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


九、其他文档特性

1. Document的属性

本文已经介绍的Document属性有body、documentElement和forms等这些特殊的文档元素。文档还定义了一些其他有趣的属性:

  • cookie

    允许JavaScript程序读、写HTTP cookie的特殊的属性

  • domain

    该属性允许当Web页面之间交互时,相同域名下互相信任的Web服务器之间协作放宽同源策略安全限制

  • lastModified

    包含文档修改时间的字符串

  • location

    与Window的location属性引用同一个Location对象

  • referrer

    如果有,它表示浏览器导航到当前链接的上一个文档。该属性值和HTTP的Referer头信息的内容相同,只是拼写上有两个r

  • title

    文档的<title>标签之间的内容

  • URL

    文档的URL,只读字符串而不是Location对象。该属性值与location.href的初始值相同,只是不包含Location对象的动态变化。例如,如果用户在文档中导向到一个新的片段,location.href会发生变化,但是document.URL则不会

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

1
2
3
4
5
6
7
8
9
10
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;
}
}
}

2. document.write()方法

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

值得一提的是Document对象还支持writeln()方法,除了在其参数的输出之后追加一个换行符以外它和write()方法完全一样。例如,在<pre>元素内输入预格式化的文本时着非常有用。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function ElementStream(elt) {
if (typeof elt === 'string')
elt = document.getElementById(elt);

this.elt = elt;
this.buffer = '';
}

ElementStream.prototype.write = function() {
this.buffer += Array.prototype.join.call(arguments, "");
};

ElementStream.prototype.writeln = function() {
this.buffer += Array.prototype.join.call(arguments, "") + "\n";
};

ElementStream.prototype.close = function() {
this.elt.innerHTML = this.buffer;
this.buffer = '';
};

3. 查询选取的文本

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

1
2
3
4
5
6
function getSelectedText() {
if (window.getSelection) // H5标准API
return window.getSelection().toString();
else if (document.selection) // IE特有的技术
return document.selection.createRange().text;
}

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

4. 可编辑的内容

有两种方法来启用编辑功能。其一,设置任何变迁的HTML contenteditable属性;其二,设置对应元素的JavaScript contentEditable属性;这都将使得元素的内容变成可编辑。

1
2
3
<div id="editor" contenteditable>
Click to edit
</div>

浏览器可能为表单字段和contenteditable元素支持自动拼写检查。在支持该功能的浏览器中,检查可能默认开启或关闭。为元素添加spellcheck属性来显式开启拼写检查,而使用spellcheck=false来显式关闭该功能。

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

1
2
3
4
5
6
7
<iframe id="editor" src="about:blank"></iframe>
<script>
onLoad(function() {
var editor = document.getElementById('editor');
editor.contentDocument.designMode = 'on';
});
</script>

参考文献

  1. 《JavaScript权威指南(第6版)》