Skip to content

Latest commit

 

History

History
2236 lines (1397 loc) · 77.1 KB

07.md

File metadata and controls

2236 lines (1397 loc) · 77.1 KB

七、浏览器环境

你知道 JavaScript 程序不会自己运行;他们需要一个宿主环境。到目前为止,本书讨论的几乎所有内容都与核心 ECMAScript/JavaScript 相关,并且可以在许多不同的主机环境中使用。现在让我们将焦点转移到浏览器上,因为这是 JavaScript 程序最流行和最自然的宿主环境。在本章中,您将了解:

  • 材料表(浏览器物件模型)

  • 文档对象模型

  • 收听浏览器事件

  • XMLHttpRequest 对象

在网页中包含 JavaScript

为了在 HTML 页面中包含 JavaScript,您需要使用<script>标记:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html>
<head>
<title>JS test</title>
<script type="text/javascript" src="somefile.js"></script> 
</head>
<body>
<script type="text/javascript">
var a = 1;
a++;
</script> 
</body>
</html>

在本例中,第一个<script>标签包含一个外部文件 somefile.js,其中包含 JavaScript 代码。第二个<script>标签将 JavaScript 代码直接包含在页面的 HTML 代码中。在这两种情况下,<script>标签都带有一个 type属性,这是 XHTML 1.0 中所需要的,尽管没有它代码也能工作。浏览器按照它在页面中找到的顺序执行 JavaScript。这意味着如果在 somefile.js中定义一个变量,它也会存在于第二个<script>块中。

物料清单和 DOM—概述

页面中的 JavaScript 代码可以访问许多对象。这些对象可以分为:

  • 与当前加载的页面(该页面也称为文档)相关的对象,以及

  • 处理页面之外的事物的对象(浏览器窗口和桌面屏幕)

第一组对象组成了文档对象模型,第二组对象组成了浏览器对象模型。

DOM 是一个标准,由万维网联盟(W3C)管理,有不同的版本,称为,如 DOM 级别 1、DOM 级别 2 和——目前为止的最后一个——DOM 级别 3。现代浏览器对标准有不同程度的遵从,但总的来说,它们几乎都完全实现了 DOM Level 1。DOM 是事后标准化的,在浏览器供应商各自实现了自己的方法来访问文档之后。遗留部分(在 W3C 接管之前)仍然存在,被称为 DOM 0,尽管不存在真正的 DOM 0 级标准。DOM 0 的某些部分已经成为事实上的标准,因为所有主要浏览器都支持它们。其中一些被添加到 DOM 级标准中。DOM 0 的其余部分没有找到到 DOM 1 的方法,这是特定于浏览器的,这里就不讨论了。

物料清单不是任何标准的一部分。与 DOM 0 类似,它有一个所有主要浏览器都支持的对象子集,还有一个特定于浏览器的子集。

本章将只讨论跨浏览器的 BOM 和 DOM 级别 1 的子集(除非文中另有说明)。甚至这些“安全”的子集也构成了一个很大的话题,完整的参考超出了本书的范围。您也可以咨询:

材料表(浏览器物件模型)是物件的集合,可让您存取浏览器和电脑屏幕。这些对象可通过全局对象 windowwindow.screen访问。

窗口对象重访

正如您已经知道的,在 JavaScript 中,每个主机环境都提供了一个全局对象。在浏览器环境中,这是 window对象。所有全局变量成为属性window对象。

>>> window.somevar = 1;
1
>>> somevar
1

此外,所有的核心 JavaScript 函数(在第 2 章中讨论)都是 window对象的方法。

>>> parseInt('123a456')
123
>>> window.parseInt('123a456')
123

除了作为全局对象之外, window对象还服务于第二个目的,即提供关于浏览器环境的数据。每个框架、iframe、弹出式菜单或浏览器标签都有一个 window对象。

让我们看看 window对象的一些与浏览器相关的属性。同样,这些在不同的浏览器之间会有所不同,所以我们只关心那些在所有现代浏览器中一致且可靠地实现的属性。

窗口.导航器

navigator是一个对象,它有一些关于浏览器及其功能的信息。一个属性是 navigator.userAgent,是一长串浏览器标识。在火狐中,你会得到这样的信息:

>>> window.navigator.userAgent

Mozilla/5.0(windows;-u;windows nt 5.1;在美国;rv:1.8.1.12)壁虎/2008 201 Firefox/2 . 0 . 0 . 12 "

微软互联网浏览器中的 userAgent字符串类似于:

Mozilla/4.0(兼容;msie 7.0;windows nt 5.1;。CLR 1.1.4322 网络;。CLR 2.0.50727 网络;。NET CLR 3.0.04506.30)

由于浏览器具有不同的功能,开发人员一直使用 userAgent字符串来标识浏览器并提供不同版本的代码。例如,该代码搜索字符串 MSIE的存在以识别互联网浏览器:

if (navigator.userAgent.indexOf('MSIE') !== -1) {
// this is IE
} else {
// not IE
}

最好不要依赖用户代理字符串,而是使用特征嗅探(也叫能力检测)来代替。原因是很难跟踪所有浏览器及其不同版本。简单地检查您打算使用的功能是否确实在用户的浏览器中可用要容易得多。例如:

if (typeof window.addEventListener === 'function') {
// feature is supported, let's use it
} else {
// hmm, this feature is not supported, will have to
// think of another way
}

避免用户代理嗅探的另一个原因是,一些浏览器允许用户修改字符串并假装他们正在使用不同的浏览器。

作为备忘单的萤火虫

Firebug 控制台提供了一种懒惰的方法来检查对象中的内容,这包括所有的物料清单和 DOM 属性。因此,您可以键入:

>>> navigator

然后点击结果。或者,您可以键入:

>>> console.dir(navigator)

结果是属性及其值的列表。

Firebug as a Cheat Sheet

窗口位置

location属性指向一个包含当前加载页面的网址信息的对象。例如 location.href,是完整的网址, location.hostname只是域名。通过一个简单的循环,您可以看到 location对象的完整属性列表。

想象你在一个网页上,网址是这样的:

http://search.phpied.com:8080/search?p=javascript#results

>>> for(var i in location) {console.log(i + ' = "' + location[i] + '"')}

"http://search.phpied.com:8080/search?p = javascript #结果"

hash =“# results”

主机=“search . phpied . com:8080”

hostname = " search . phpied . com "

pathname = "/search"

端口=“8080”

协议=“http:“

search =?p=javascript"

还有 location提供的三种方法——reload(), assign()replace()

有趣的是,您可以通过许多不同的方式导航到另一个页面。以下是部分列表:

>>> window.location.href = 'http://www.packtpub.com'
>>> location.href = 'http://www.packtpub.com'
>>> location = 'http://www.packtpub.com'
>>> location.assign('http://www.packtpub.com')

replace()assign()差不多。不同之处在于,它不会在浏览器的历史列表中创建条目:

>>> location.replace('http://www.yahoo.com')

要重新加载页面,您可以使用:

>>> location.reload()

或者,您可以使用 location.href将其指向自身,如下所示:

>>> window.location.href = window.location.href

或者简单地说:

>>> location = location

历史之窗

window.history允许在同一浏览器会话中对先前访问过的页面进行有限访问。例如,您可以看到用户在进入您的页面之前已经访问了多少页面:

>>> window.history.length

5

但是你看不到真实的网址。出于隐私原因,这不起作用:

>>> window.history[0]

但是,您可以在用户的会话中来回导航,就像用户单击了后退/前进浏览器按钮一样:

>>> history.forward()
>>> history.back()

也可以用 history.go()来回翻页。这跟打 history.back():一样

>>> history.go(-1);

两页后:

>>> history.go(-2);

重新加载当前页面:

>>> history.go(0);

窗框

window.frames是当前页面中所有框架的集合。请注意,它没有区分框架和 iframes。不管页面上有没有边框, window.frames始终存在,指向 window

>>> window.frames === window
true

为了判断页面上是否有框架,可以检查 length属性:

>>> frames.length
1

每个框架包含另一个页面,该页面有自己的全局 window对象。假设你有一个只有一个 iframe 的页面。

<iframe name="myframe" src="about:blank" />

要访问 iframe 的 window,您可以执行以下任一操作:

>>> window.frames[0]
>>> window.frames[0].window
>>> frames[0].window
>>> frames[0]

从父页面,您可以访问子框架的属性,例如,您可以这样重新加载框架:

>>> frames[0].window.location.reload()

您可以从孩子内部访问家长:

>>> frames[0].parent === window
true

使用名为 top的属性,您可以从任何框架中访问最上面的页面(包含所有其他框架的页面):

>>> window.frames[0].window.top === window
true

>>> window.frames[0].window.top === window.top
true
>>> window.frames[0].window.top === top
true

另外, selfwindow相同。

>>> self === window
true
>>> frames[0].self == frames[0].window
true

如果框架具有 name属性,您也可以通过名称访问框架,而不仅仅是通过索引。

>>> window.frames['myframe'] === window.frames[0]
true

窗口.屏幕

screen提供浏览器外的桌面信息。 screen.colorDepth属性包含显示器的颜色位深度(颜色质量)。这对于统计目的可能是有用的。

>>> window.screen.colorDepth
32

您还可以查看可用的屏幕空间(分辨率):

>>> screen.width
1440
>>> screen.availWidth
1440
>>> screen.height
900
>>> screen.availHeight
847

heightavailHeight的区别在于 height是总分辨率, availHeight减去任何操作系统菜单,比如 Windows 任务栏。 widthavailWidth也是如此。

window.open()/close()

探索了 window对象的一些最常见的跨浏览器属性后,让我们转到一些方法。其中一个方法是 open(),它允许你打开新的浏览器窗口(弹出窗口)。各种浏览器策略和用户设置可能会阻止您打开弹出窗口(由于出于营销目的滥用了该技术),但通常情况下,如果是用户启动的,您应该能够打开一个新窗口。否则,如果您试图在页面加载时打开弹出窗口,它很可能会被阻止,因为用户没有显式启动它。

window.open()接受以下参数:

  • 要在新窗口中加载的网址

  • 新窗口的名称,可用作表单的 target属性的值

  • 以逗号分隔的功能列表,例如:

    • resizable—用户是否能够调整新窗口的大小

    • width, height的弹出窗口

    • status—状态栏是否可见

    • 等等

window.open()返回对新创建的浏览器实例的 window对象的引用。这里有一个例子:

var win = window.open('http://www.packtpub.com', 'packt', 'width=300,height=300,resizable=yes');

win指向弹出的 window对象。您可以检查 win是否有错误值,这意味着弹出窗口被阻止。

win.close()关闭新窗口。

出于可访问性和可用性的原因,最好不要打开新窗口。如果你不喜欢网站向你弹出窗口,为什么要对你的用户这样做?可能有合法的目的,例如在填写表单时提供帮助信息,但通常同样的目的可以通过替代解决方案来实现,例如在页面内使用浮动的<div>

窗口. 移动到 (), 窗口. 调整到 ()

继续过去不光彩的做法,这里有更多的方法来激怒你的用户,只要他们的浏览器和个人设置允许。

  • window.moveTo(100, 100)将浏览器窗口移动到屏幕位置 x = 100 和 y = 100(从左上角算起)。

  • window.moveBy(10, -10)将窗口从当前位置向右移动 10 像素,向上移动 10 像素。

  • window.resizeTo(x, y)window.resizeBy(x, y)接受与移动方法相同的参数,但它们调整窗口大小,而不是移动窗口。

同样,尝试解决你面临的问题,不要诉诸这些方法。

window.alert(),window.prompt(),window.confirm()

第 2 章中,我们遇到了函数 alert()。现在你知道全局函数是全局对象的方法,所以 alert('Watch out!')window.alert('Watch out!')是完全一样的。

alert()不是 ECMAScript 函数,而是 BOM 方法。除此之外,还有两种 BOM 方法允许您通过系统消息与用户交互:

  • confirm()给用户两个选项— 确定取消,以及

  • prompt()收集文本输入

看看这是如何工作的:

>>> var answer = confirm('Are you cool?'); console.log(answer);

它会向您显示一个类似于以下内容的窗口(具体外观取决于浏览器和操作系统):

window.alert(), window.prompt(), window.confirm()

你会注意到:

  • 在您关闭此消息之前,不会有任何内容被写入 Firebug 控制台,这意味着任何 JavaScript 代码的执行都会冻结,等待用户的回答。

  • 单击确定返回,单击取消或使用 X 图标(或 ESC 键)关闭消息返回

这对于确认用户操作很方便,例如:

if (confirm('Are you sure you want to delete this item?')) {
// delete
} else {
// abort
}

确保您为禁用了 JavaScript 的人(或搜索引擎蜘蛛)提供了确认用户操作的替代方法。

window.prompt()向用户呈现输入文本的对话框。

>>> var answer = prompt('And your name was?'); console.log(answer);

这将导致以下对话框(在 Windows/火狐上):

window.alert(), window.prompt(), window.confirm()

answer的值将为:

  • 如果单击取消X 图标,或按 ESC ,则为空

  • "" (空字符串)如果点击确定或按进入而不输入任何内容

  • 如果您键入内容,然后单击确定(或按进入)则为文本字符串

该函数还将字符串作为第二个参数,并将其显示为预先填充到输入字段中的默认值。

window.setTimeout()、window.setInterval()

setTimeout()setInterval()允许调度一段代码的执行。 setTimeout()在指定的毫秒数后执行给定的代码一次。 setInterval()经过指定的毫秒数后重复执行。

这将在 2 秒(2000 毫秒)后显示警报:

>>> function boo(){alert('Boo!');}
>>> setTimeout(boo, 2000);
4

如您所见,函数返回整数 4 ,这是超时的标识。您可以使用此 ID 使用 clearTimeout()取消超时。在下面的示例中,如果您足够快,并且在两秒钟过去之前清除了超时,则永远不会显示警报。

>>> var id = setTimeout(boo, 2000);
>>> clearTimeout(id);

让我们把 boo()换成不那么碍眼的:

>>> function boo() {console.log('boo')};

现在,使用 setInterval()你可以安排 boo()每两秒执行一次,直到你用 clearInterval()取消预定的执行。

>>> var id = setInterval(boo, 2000);
boo
boo
boo
boo
boo
boo
>>> clearInterval(id)

请注意,这两个函数都接受指向回调函数的指针作为第一个参数。他们也可以接受一个用 eval()来评价的字符串,但是你知道 eval()是邪恶的,所以应该避免。如果你想把参数传递给函数呢?在这种情况下,您可以将函数调用包装在另一个函数中。

以下代码有效,但不推荐:

var id = setInterval("alert('boo, boo')", 2000);

首选这种替代方案:

var id = setInterval(
function(){
alert('boo, boo')
}, 2000
);

窗口.文档

window.document是指当前加载的文档(页面)的 BOM 对象。它的方法和属性属于对象的 DOM 类别。深呼吸(也许可以看看本章末尾的 BOM 练习),让我们深入 DOM。

DOM(文档对象模型)是一种将 XML 或 HTML 文档表示为节点树的方式。使用 DOM 方法和属性,您可以访问页面上的任何元素,修改或移除元素,或者添加新的元素。DOM 是一个独立于语言的应用编程接口,不仅可以用 JavaScript 实现,也可以用任何其他语言实现。例如,您可以使用 PHP 的 DOM 实现在服务器端生成页面(http://php.net/dom)。

看一下这个示例 HTML 页面:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html>
<head>
<title>My page</title>
</head>
<body>
<p class="opener">first paragraph</p>
<p><em>second</em> paragraph</p>
<p id="closer">final</p>
<!-- and that's about it -->
</body>
</html>

拿第二段( <p><em>second</em> paragraph</p>))来看,是一个 p标签,包含在 body标签中。你可以说 bodyp的父母 p是孩子。第一段和第三段也是 body的孩子,他们也是第二段的兄弟姐妹。 em标签是第二个 p的子标签,所以 p是它的父标签。父子关系可以用一个名为 DOM 的祖先树来图形化表示。

DOM

您可以看到所有标签是如何在树上显示为可扩展的节点的。散布在树中的单词 #text 也代表节点,但是是不同的类型— 文本节点。比如 EM 里面的**#文字就是“秒”字。空白也被认为是一个文本节点,这就是为什么,例如,在 BODY 和第一个 P 之间有一个 #text虽然那里的代码中没有实际的文本,只有空格。HTML 代码内部的注释也是树中的节点;HTML 源代码中的<!-- and that's about it -->注释是树中的节点#注释**。

以上截图取自火狐的 DOM Inspector 功能。默认情况下不会安装此功能,因此,如果您正在运行火狐 2,您可能需要在现有安装的基础上重新安装火狐(不会丢失任何首选项或扩展),当提示安装类型时,选择自定义而不是标准,然后确保选择 DOM Inspector 复选框。如果你运行的是火狐 3,你可以从https://addons.mozilla.org/en-us/firefox/addon/6622获得 DOM Inspector 作为附件。

安装后,可通过工具|DOM 检查器访问 DOM 检查器。

DOM 检查器中,左边是 DOM 树,右边是所选节点的信息。上面的截图显示了所选 HTML 节点的 Javascript 对象面板。 Javascript 对象不是默认视图,但是您可以通过点击小的“视图”图标来打开它,如下图所示。

DOM

DOM 树中的每个节点都是一个对象, DOM 检查器Javascript 对象视图列出了这些对象的所有属性和方法。您还可以看到在幕后用来创建这些对象的构造函数。虽然这对于日常任务不是很有用,但是知道例如 window.document是由 HTMLDocument()构造器创建的可能会很有趣;代表 head标签的对象由 HTMLHeadElement()等创建。但是,您不能直接使用这些构造函数创建对象。

核心 DOM 和 HTML DOM

在转向更实际的例子之前,还有最后一个转移。如您现在所知,DOM 代表了 XML 文档和 HTML 文档。实际上,HTML 文档是 XML 文档,只是具体一点。因此,作为 DOM Level 1 的一部分,有一个适用于所有 XML 文档的核心 DOM 规范,还有一个扩展并建立在核心 DOM 之上的 HTML DOM 规范。当然,HTML DOM 并不适用于所有的 XML 文档,而仅适用于 HTML 文档。让我们看一些核心 DOM 和 HTML DOM 构造函数的例子。

|

构造器

|

继承自

|

核心或 HTML

|

评论

| | --- | --- | --- | --- | | Node | | 核心 | 树上的任何节点。 | | Document | Node | 核心 | document对象;任何 XML 文档的主要入口点。 | | HTMLDocument | Document | 超文本标记语言 | 这是window.document或者简单地说是文档,先前对象的 HTML 特定版本,您将广泛使用它。 | | Element | Node | 核心 | 源中的每个标记都由一个元素表示。这就是为什么你说“这个 P 元素”的意思是“这个**<>

**标记”。 | | HTMLElement | Element | 超文本标记语言 | 通用构造函数;HTML 元素的所有构造函数都从它继承。 | | HTMLBodyElement | HTMLElement | 超文本标记语言 | 表示<body>标签的元素。 | | HTMLLinkElement | HTMLElement | 超文本标记语言 | 一个 A 元素(一个<a href="http://...></a>标签)。 | | .... | HTMLElement | 超文本标记语言 | 所有剩下的 HTML 元素... | | CharacterData | Node | 核心 | 处理文本的通用构造函数。 | | Text | CharacterData | 核心 | 标签内的文本节点。在<em>second</em>中,有元素节点 EM 和值为“秒”的文本节点。 | | Comment | CharacterData | 核心 | <!-任何评论- > | | Attr | Node | 核心 | 表示标签的属性,在<p id="closer">中,id 属性是由Attr()构造函数创建的 DOM 对象。 | | NodeList | | 核心 | 节点列表;具有length属性的类似数组的对象。 | | NamedNodeMap | | 核心 | 同上,但是节点可以通过名称访问,而不仅仅是通过数字索引。 | | HTMLCollection | | 超文本标记语言 | 与上面类似,但特定于 HTML。 |

这些绝不是核心 DOM 和 HTML DOM 对象的全部。完整列表请咨询http://www.w3.org/TR/DOM-Level-1/

现在,DOM 理论已经成为过去,让我们关注事物的实际方面。在以下部分中,您将学习如何:

  • 访问 DOM 节点

  • 修改节点

  • 创建新节点

  • 移除节点

访问 DOM 节点

在验证页面上的表单或交换图像之前,您需要访问要检查或修改的元素。幸运的是,有许多方法可以到达任何元素,要么通过遍历 DOM 树导航,要么通过使用快捷方式。

最好是你开始尝试所有新的对象和方法。您将看到的示例使用了与您在 DOM 部分开头看到的相同的简单文档,您可以在http://www.phpied.com/files/jsoop/ch7.html访问该文档。打开 Firebug 控制台,让我们开始吧。

文档节点

document允许您访问当前文档。为了探索这个对象,您可以再次使用 Firebug 作为备忘单。键入 document并点击结果。

The document Node

这将带您进入 DOM 选项卡,您可以在其中浏览 document对象的所有属性和方法。

The document Node

所有节点(也包括文档节点、文本节点、元素节点和属性节点)都有 nodeType, nodeNamenodeValue属性。

>>> document.nodeType
9

有 12 种节点类型,用整数表示。可以看到,单据节点类型为 9 。最有用的是 1(元素)、2(属性)和 3(文本)。

节点也有名称。对于 HTML 标记,节点名是标记名(标记名属性)。对于文本节点,是**#文本**,对于文档节点:

>>> document.nodeName
"#document"

节点也可以有节点值,例如文本节点;该值是实际文本。文档节点没有值:

>>> document.nodeValue
null

文件元素

现在让我们绕着树走。XML 文档总是有一个根节点来包装文档的其余部分。对于 HTML 文档,根是<html>标签。要访问根,可以使用 document对象的 documentElement属性。

>>> document.documentElement
<html>

nodeType为 1(一个元素节点):

>>> document.documentElement.nodeType
1

对于元素节点, nodeNametagName属性都包含标签的名称。

>>> document.documentElement.nodeName
"HTML"
>>> document.documentElement.tagName
"HTML"

子节点

为了判断一个节点是否有子节点,可以使用 hasChildNodes():

>>> document.documentElement.hasChildNodes()
true

HTML 元素有两个子元素——T0 和 T1 元素。您可以使用 childNodes类似数组的集合来访问它们。

>>> document.documentElement.childNodes.length
2
>>> document.documentElement.childNodes[0]
<head>
>>> document.documentElement.childNodes[1]
<body>

任何子代都可以通过 parentNode属性访问其父代:

>>> document.documentElement.childNodes[1].parentNode
<html>

让我们将对 body的引用分配给一个变量:

>>> var bd = document.documentElement.childNodes[1];

body元素有几个孩子?

>>> bd.childNodes.length
9

作为复习,这里再次是我们正在查看的文档的正文:

<body>
<p class="opener">first paragraph</p>
<p><em>second</em> paragraph</p>
<p id="closer">final</p>
<!-- and that's about it -->
</body>

body怎么有 9 个孩子?好吧,3 段加一条评论就有 4 个节点。这 4 个节点之间的空白又构成了 3 个文本节点。这使得到目前为止总共有 7 个。 body和第一个 p之间的空白是第八个节点。注释和结尾</body>之间的空白是另一个文本节点。这样总共有 9 个子节点。

属性

因为身体的第一个孩子是空格,第二个孩子(索引 1)是第一段:

>>> bd.childNodes[1]
<p class="opener">

可以使用 hasAttributes():检查元素是否有属性

>>> bd.childNodes[1].hasAttributes()
true

有多少属性?在这个例子中,一个是 class属性。

>>> bd.childNodes[1].attributes.length
1

您可以通过索引和名称来访问属性。您也可以使用 getAttribute()方法获得该值

>>> bd.childNodes[1].attributes[0].nodeName
"class"
>>> bd.childNodes[1].attributes[0].nodeValue
"opener"
>>> bd.childNodes[1].attributes['class'].nodeValue
"opener"
>>> bd.childNodes[1].getAttribute('class')
"opener"

访问标签内的内容

我们来看看第一段:

>>> bd.childNodes[1].nodeName
"P"

您可以使用 textContent属性获取段落中包含的文本。 textContent在 IE 中不存在,但是另一个名为 innerText的属性存在,并且返回相同的值。

>>> bg.childNodes[1].textContent
"first paragraph"

还有 innerHTML属性。它不是 DOM 标准的一部分,但是存在于所有主要的浏览器中。它返回节点中包含的任何 HTML 代码。您可以看到这有点不一致,因为 DOM 将文档视为节点树,而不是标签字符串。但是 innerHTML使用起来太方便了,到处都可以看到。

>>> bd.childNodes[1].innerHTML
"first paragraph"

第一段只有文字,所以 innerHTMLtextContent(或 IE 中的 innerText)相同。但是,第二段确实包含一个 em节点,所以可以看出区别:

>>> bd.childNodes[3].innerHTML
"<em>second</em> paragraph"
>>> bd.childNodes[3].textContent
"second paragraph"

获取第一段中包含的文本的另一种方法是使用包含在 p节点内的文本节点的【T0:

>>> bd.childNodes[1].childNodes.length
1
>>> bd.childNodes[1].childNodes[0].nodeName
"#text"
>>> bd.childNodes[1].childNodes[0].nodeValue
"first paragraph"

DOM 访问快捷方式

通过使用 childNodes, parentNode, nodeName, nodeValueattributes,您可以在树中上下导航,几乎可以对文档做任何事情。但事实上,空白是一个文本节点,这使得使用 DOM 的方式有点脆弱。如果页面略有变化,您的脚本可能不再正常工作。此外,如果您想到达树中更深的节点,可能需要一些代码才能到达。这就是为什么你有捷径方法——getElementsByTagName()getElementsByName()getElementById()

getElementsByTagName()取一个标签名(元素节点的名称),返回一个 HTML 集合(类似数组的对象)节点,标签名匹配:

>>> document.getElementsByTagName('p').length
3

您可以通过使用括号符号或方法 item()并传递索引(第一个元素为 0)来访问列表中的项目。不鼓励使用 item(),因为数组括号更一致,键入也更短。

>>> document.getElementsByTagName('p')[0]
<p class="opener">
>>> document.getElementsByTagName('p').item(0)
<p class="opener">

获取第一个 p:的内容

>>> document.getElementsByTagName('p')[0].innerHTML
"first paragraph"

访问最后一个 p:

>>> document.getElementsByTagName('p')[2]
<p id="closer">

为了访问元素的属性,可以使用 attributes集合,或者如上所示的 getAttribute()。但是一个更简单的方法是使用属性名作为您正在使用的元素的属性。因此要获得 id属性的值,您只需将 id用作属性:

>>> document.getElementsByTagName('p')[2].id
"closer"

不过获取第一段的 class属性是行不通的。这是一个例外,因为它只是碰巧“类”是 ECMAScript 中的保留词。可以用 className代替:

>>> document.getElementsByTagName('p')[0].className
"opener"

使用 getElementsByTagName()可以获取页面上的所有元素:

>>> document.getElementsByTagName('*').length
9

在早期版本的 IE 中,'*'不能作为标签名。为了获得所有元素,你可以使用 IE 专有的 document.all集合,尽管选择每个元素很少有用。无论如何,从 IE 第 7 版开始,支持 document.getElementsByTagName('*'),但会返回所有节点,而不仅仅是元素节点。

上面提到的另一个捷径是 getElementById()。这可能是访问元素的最常见方式。您只需为您计划玩的元素分配标识,它们将很容易在以后访问:

>>> document.getElementById('closer')
<p id="closer">

兄弟姐妹、身体、第一个和最后一个孩子

nextSiblingpreviousSibling是导航 DOM 树的另外两个方便的属性,一旦你有了对一个元素的引用:

>>> var para = document.getElementById('closer')
>>> para.nextSibling
"\n "

>>> para.previousSibling
"\n "
>>> para.previousSibling.previousSibling
<p>
>>> para.previousSibling.previousSibling.previousSibling
"\n "
>>> para.previousSibling.previousSibling.nextSibling.nextSibling
<p id="closer">

body元素的使用频率如此之高,以至于它有自己的快捷方式:

>>> document.body
<body>
>>> document.body.nextSibling
null
>>> document.body.previousSibling
<head>

firstChildlastChild也可能有用。 firstChildchildNodes[0]lastChildchildNodes[childNodes.length - 1]

>>> document.body.firstChild
"\n "
>>> document.body.lastChild
"\n "
>>> document.body.lastChild.previousSibling
Comment length=21 nodeName=#comment
>>> document.body.lastChild.previousSibling.nodeValue
" and that's about it "

下图显示了身体和身体中三个段落之间的家庭关系。为了简单起见,所有的空白文本节点都从图中删除了。

Siblings, Body, First, and Last Child

漫步世界

总结一下,这里有一个函数,它接受任何节点,并从给定的节点开始递归遍历 DOM 树。

function walkDOM(n) {
do {
console.log(n);
if (n.hasChildNodes()) {
walkDOM(n.firstChild)
}
} while (n = n.nextSibling)
}

您可以按如下方式测试该功能:

>>> walkDOM(document.documentElement)
>>> walkDOM(document.body)

修改 DOM 节点

现在您已经知道了很多访问 DOM 树的任何节点及其属性的方法,让我们看看如何修改这些节点。

让我们给变量 my分配一个指向最后一段的指针。

>>> var my = document.getElementById('closer');

现在更改段落的文本可以像更改 innerHTML值一样简单:

>>> my.innerHTML = 'final!!!';
"final!!!"

因为 innerHTML接受一串 HTML 源代码,所以还可以在 DOM 树中创建新的 em节点:

>>> my.innerHTML = '<em>my</em> final';
"<em>my</em> final"

新的 em节点成为树的一部分:

>>> my.firstChild
<em>
>>> my.firstChild.firstChild
"my"

更改文本的另一种方法是获取实际的文本节点并更改其 nodeValue:

>>> my.firstChild.firstChild.nodeValue = 'your';
"your"

修改样式

通常,您不会更改节点的内容,而是更改其表示形式。元素有一个 style属性,该属性又有一个映射到每个 CSS 属性的属性。例如,更改段落样式以添加红色边框:

>>> my.style.border = "1px solid red";
"1px solid red"

CSS 属性通常有破折号,但在 JavaScript 名称中破折号是不可接受的。在这种情况下,您可以跳过破折号,将下一个字母大写。所以 padding-top变成 paddingTop, margin-left变成 marginLeft,以此类推。

>>> my.style.fontWeight = 'bold';
"bold"

您也可以修改属性,无论它们最初是否被设置:

>>> my.align = "right";
"right"
>>> my.name
>>> my.name = 'myname';
"myname"
>>> my.id
"closer"
>>> my.id = 'further'
"further"

让我们看看经过所有这些修改后的标签是什么样子的:

>>> my
<p id="further" align="right" style="border: 1px solid red; font-weight: bold;">

玩转形态

如前所述,JavaScript 非常适合客户端输入验证,可以节省到服务器的几次往返。让我们练习一下表单操作,用一个流行页面上的表单玩一会儿。

Fun with Forms

选择所有输入字段:

>>> var inputs = document.getElementsByTagName('input');
>>> inputs.length;
4

打印出 inputs[0], inputs[1]等等,可以看到第一个输入的是隐藏字段,第二个是搜索查询,第三个是谷歌搜索按钮,第四个——是我感觉幸运按钮。

访问搜索框:

>>> inputs[1].name;
"q"

更改搜索查询,通过设置包含在 value属性中的文本:

>>> inputs[1].value = 'my query';
"my query"

现在让我们玩得开心点。将按钮中的幸运改为棘手:

>>> inputs[3].value = inputs[3].value.replace(/Lu/, 'Tri');
"I'm Feeling Tricky"

Fun with Forms

现在让我们实现棘手的部分,并使该按钮显示和隐藏一秒钟。我们可以用一个简单的函数做到这一点。姑且称之为 toggle()。每次调用该函数时,它都会检查 CSS 属性 visibility的值,如果“隐藏”则设置为“可见”,反之亦然。

function toggle(){
var st = document.getElementsByTagName('input')[3].style;
st.visibility = (st.visibility === 'hidden') ? 'visible': 'hidden';
}

让我们设置一个时间间隔,每秒调用一次,而不是手动调用函数。

>>> var myint = setInterval(toggle, 1000);

结果呢?按钮开始闪烁(使点击变得更加棘手)。当你厌倦了追逐它,只要删除超时间隔。

>>> clearInterval(myint)

创建新节点

为了创建新节点,可以使用 createElement()createTextNode()方法。一旦你有了新的节点,你可以用 appendChild()将它们添加到 DOM 树中。

创建新的 p元素并设置其 innerHTML:

>>> var myp = document.createElement('p');
>>> myp.innerHTML = 'yet another';
"yet another"

新元素自动获取所有默认属性,如 style,可以修改:

>>> myp.style
CSSStyleDeclaration length=0
>>> myp.style.border = '2px dotted blue'
"2px dotted blue"

使用 appendChild()可以将新节点添加到 DOM 树中。在 document.body节点上调用该方法意味着在最后一个子节点之后再创建一个子节点:

>>> document.body.appendChild(myp)
<p style="border: 2px dotted blue;">

下面是添加新节点前后页面的图示:

Creating New Nodes

纯 DOM 方法

使用 innerHTML是让事情更快完成的一种方式;纯 DOM 方式应该是:

  1. 1.创建包含“又一个”文本的新文本节点

  2. 2.创建新段落节点

  3. 3.将文本节点作为子节点追加到段落中

  4. 4.将段落作为子段落附加到正文中

通过这种方式,您可以创建任意数量的文本节点和元素,并按照您喜欢的方式嵌套它们。假设您想在正文的末尾添加以下 HTML:

<p>one more paragraph<strong>bold</strong></p>

将它表示为层次结构类似于:

P element
text node with value "one more paragraph"
STRONG element
text node with value "bold"

让我们看看实现这一点的代码:

// create P
var myp = document.createElement('p');
// create text node and append to P
var myt = document.createTextNode('one more paragraph')
myp.appendChild(myt);
// create STRONG and append another text node to it
var str = document.createElement('strong');
str.appendChild(document.createTextNode('bold'));
// append STRONG to P
myp.appendChild(str);
// append P to BODY
document.body.appendChild(myp);

克隆体()

创建节点的另一种方法是复制(或克隆)现有节点。方法 cloneNode()这样做,并接受一个布尔参数(true =所有子节点的深度复制, false =浅复制,只有这个节点)。让我们测试一下这个方法。

获取对要克隆的元素的引用:

>>> var el = document.getElementsByTagName('p')[1];

现在 el指的是页面上的第二段,看起来是这样的:

<p><em>second</em> paragraph</p>

让我们创建一个 el的浅克隆,并将其附加到body:

>>> document.body.appendChild(el.cloneNode(false))

你在页面上看不到区别,因为浅拷贝只拷贝了 P 节点,没有任何子节点。这意味着段落内部的文本(它是文本节点子节点)没有被克隆。上面的行相当于:

>>> document.body.appendChild(document.createElement('p'));

现在,如果您创建一个深度副本,从 P 开始的整个 DOM 子树将被复制,这包括文本节点和 EM 元素。

>>> document.body.appendChild(el.cloneNode(true))

如果需要,您也可以仅复制 EM:

>>> document.body.appendChild(el.firstChild.cloneNode(true))
<em>

或者只有值为“秒”的文本节点:

>>> document.body.appendChild(el.firstChild. firstChild.cloneNode(false))
"second"

插入之前 ()

使用 appendChild(),只能在所选元素的末尾添加新的子元素。为了更好地控制准确的位置,有 insertBefore()。这与 appendChild()相同,但接受一个额外的参数,指定在哪个元素之前插入新节点。例如,下面的代码将在 body: 的末尾插入一个文本节点

>>> document.body.appendChild(document.createTextNode('boo!'));

这将创建相同的文本节点,并将其添加为 body:的第一个子节点

document.body.insertBefore(
document.createTextNode('boo!'),
document.body.firstChild
);

移除节点

要从 DOM 树中移除节点,可以使用 removeChild()方法。再次,让我们使用这个 BODY:

<body>
<p class="opener">first paragraph</p>
<p><em>second</em> paragraph</p>
<p id="closer">final</p>
<!-- and that's about it -->
</body>

以下是删除第二段的方法:

>>> var myp = document.getElementsByTagName('p')[1];
>>> var removed = document.body.removeChild(myp);

如果您希望以后使用已移除的节点,该方法将返回该节点。尽管元素已经不在树中,您仍然可以使用所有的 DOM 方法:

>>> removed
<p>
>>> removed.firstChild
<em>

还有 replaceChild()方法,移除一个节点并放入另一个节点。现在,删除上面显示的节点后,树看起来像:

<body>
<p class="opener">first paragraph</p>
<p id="closer">final</p>
<!-- and that's about it -->
</body>

现在第二段是 ID 为“closer”的:

>>> var p = document.getElementsByTagName('p')[1];
>>> p
<p id="closer">

让我们用 removed变量中的一段来替换这段:

>>> var replaced = document.body.replaceChild(removed, p);

就像 removeChild(), replaceChild()返回对现在不在树中的节点的引用一样:

>>> replaced
<p id="closer">

现在身体看起来像:

<body>
<p class="opener">first paragraph</p>
<p><em>second</em> paragraph</p>
<!-- and that's about it -->
</body>

清除所有子树内容的一个快速方法是将 innerHTML设置为一个空字符串。这将删除 BODY 的所有子对象:

>>> document.body.innerHTML = '';
""

测试:

>>> document.body.firstChild
null

使用 innerHTML移除很容易,但是 DOM 唯一的方法是遍历所有子节点并逐个移除。这里有一个小函数,从给定的开始节点移除所有节点:

function removeAll(n) {
while (n.firstChild) {
n.removeChild(n.firstChild);
}
}

如果你想删除所有 BODY 子代,并留下一个空的页面<body></body>:

>>> removeAll(document.body);

纯 HTML DOM 对象

正如您已经知道的,文档对象模型适用于 XML 和 HTML 文档。上面关于遍历树,然后添加、移除或修改节点的知识适用于任何 XML 文档。然而,有一些纯 HTML 的对象和属性。

document.body就是这样一个纯 HTML 对象。在 HTML 文档中有一个<body>标签是很常见的,而且经常被访问,所以有一个比同等的 document.getElementsByTagName('body')[0]更友好的对象是合理的。

document.body是一个对象的例子,该对象实际上是从历史前的 DOM Level 0 继承而来的,并被移到了 DOM 规范的 HTML 扩展中。还有其他类似 document.body的对象。对于他们中的一些人来说,没有等同的核心 DOM 对于其他人来说,有一个等效的,但不管怎样,DOM0 的原始版本是为了简单和遗留的目的而移植的。让我们来看看这些对象。

访问文档的基本方式

与 DOM 不同,DOM 允许您访问任何元素(甚至是注释和空白),最初 JavaScript 只能有限地访问 HTML 文档的元素。这主要是通过一些集合完成的:

  • document.images -这是页面上所有图片的集合。这与核心 DOM 等效物 document.getElementsByTagName('img')相同

  • document.applets—这个和 document.getElementsByTagName ('applets')一样

  • document.links

  • document.anchors

  • document.forms

document.links包含页面上所有<a href="http://...></a>标签的列表,表示具有 href属性的 A 标签。 document.anchors包含所有带有 name属性( <aname="..."></a>)的链接。

最广泛使用的集合之一是 document.forms,它包含一个<form>标签列表。这将允许您访问页面上的第一个表单:

>>> document.forms[0]

这与以下情况相同:

>>> document.getElementsByTagName('forms')[0]

表单集合包含输入字段和按钮,可通过 elements属性访问。以下是如何访问页面上第一个表单的第一个输入:

>>> document.forms[0].elements[0]

一旦可以访问元素,就可以将标签的属性作为对象属性进行访问。想象第一种形式的第一个字段是这样的:

<input name="search" id="search" type="text" size="50" maxlength="255" value="Enter email..." />

您可以使用类似以下的方法来更改字段中的文本( value属性的值):

>>> document.forms[0].elements[0].value = 'me@example.org'
"me@example.org"

如果要动态禁用该字段:

>>> document.forms[0].elements[0].disabled = true;

当表单或表单元素具有 name属性时,您也可以通过名称来访问它们:

>>> document.forms[0].elements['search']; // array notation
>>> document.forms[0].elements.search; // object property

document.write()

方法 document.write()允许你在加载页面的同时将 HTML 插入页面。可以有这样的东西:

<p>It is now <script>document.write("<em>" + new Date() + "</em>");</script></p>

这与您在 HTML 文档的源中直接输入日期是一样的:

<p>It is now <em>Sat Feb 23 2008 17:48:04 GMT-0800 (Pacific Standard Time)</em></p>

还有与 document.write()相同的 document.writeln(),但是它在末尾增加了一个新行“\n”,所以这些是等价的:

>>> document.write('boo!\n');
>>> document.writeln('boo!');

请注意,您只能在页面加载时使用 document.write(),如果您在页面加载后尝试,它将替换整个页面的内容。

你很少需要 document.write(),如果你认为你需要,尝试另一种方法。DOM Level 1 提供的修改页面内容的方法是首选的,而且更加灵活。

cookie、标题、推荐人、域

您将在本节中看到的 document的四个附加属性也从 DOM 级别 0 移植到了 DOM 级别 1 的 HTML 扩展。与前面的不同,对于这些属性,没有核心 DOM 等价物。

document.cookie是包含字符串的属性。该字符串是服务器和客户端之间交换的 cookies 的内容。服务器向浏览器发送页面时,可能会包含 Set-Cookie HTTP 头。当客户端向服务器发送请求时,它会将带有 Cookie头的 cookie 信息发送回来。使用 document.cookie可以更改浏览器发送给服务器的 cookies。例如,访问cnn.com并在控制台中键入 document.cookie会给你类似的感觉:

>>> document.cookie

“CNNid = Ga50a0c6f-14404-1198821758-6;SelectedEdition = wwws_sess= %20s_dslv%..

document.title允许您更改浏览器窗口中显示的页面标题。比如在cnn.com上,你可以做到:

>>> document.title = 'My title'
"My title"

结果会是这样的:

Cookies, Title, Referrer, Domain

请注意,这不会改变<title>标签的值,只会改变浏览器窗口中的显示,因此不等同于 document.getElementsByTagName('title')[0]

document.referrer告诉你之前访问过的页面的网址。这与浏览器请求页面时在 Referer HTTP 头中发送的值相同。(注意 Referer在 HTTP 头中拼错了,但在 JavaScript 的 document.referrer中是正确的)。如果你已经通过搜索雅虎访问了美国有线电视新闻网的网页!首先,您可以看到类似这样的内容:

>>> document.referrer
"http://search.yahoo.com/search?p=cnn&ei=UTF-8&fr=moz2"

document.domain让你访问当前加载页面的域名。当需要执行所谓的域放松时,这很有用。想象你的页面是www.yahoo.com,你有一个框架或 iframe 托管在music.yahoo.com上。这是两个独立的域,因此浏览器的安全限制不允许页面与 iframe 通信。要解决这个问题,你可以将两个页面上的 document.domain设置为yahoo.com,这样他们就可以互相“交谈”了。

请注意,您只能将域设置为不太特定的域;例如,您可以将www.yahoo.com更改为yahoo.com,但不能将yahoo.com更改为www.yahoo.com或任何其他非雅虎域。

>>> document.domain
"www.yahoo.com"
>>> document.domain = 'yahoo.com'
"yahoo.com"
>>> document.domain = 'www.yahoo.com'
Illegal document.domain value" code: "1009
>>> document.domain = 'www.example.org'
Illegal document.domain value" code: "1009

在本章前面,您看到了 window.location对象。嗯,同样的功能也可以作为 document.location:使用

>>> window.location === document.location
true

事件

想象你正在听一个广播节目,他们宣布:“大事件!巨大!外星人已经登陆地球了!”你可能会想“是啊,管他呢”,其他一些听众可能会想“他们是和平而来的”,还有一些人会想“我们都会死!”。类似地,浏览器广播事件,如果您的代码决定“收听”并收听发生的事件,它会通知您。一些示例事件包括:

  • 用户点击一个按钮

  • 用户在表单域中键入一个字符

  • 页面加载完毕

您可以将一个 JavaScript 函数(称为事件监听器或*事件处理程序)*附加到一个特定的事件上,一旦事件发生,浏览器就会执行您的函数。让我们看看这是如何做到的。

内嵌 HTML 属性

给标签添加特定属性是最懒的方式,例如:

<div onclick="alert('Ouch!')">click</div>

在这种情况下,当用户点击<div>时,点击事件触发,并且执行包含在 onclick属性中的 JavaScript 代码字符串。没有监听点击事件的显式函数,但在幕后仍会创建一个函数,它包含您指定为 onclick属性值的代码。

元素属性

另一种在点击事件触发时执行一些代码的方法是给 DOM 节点元素的 onclick属性分配一个函数。例如:

<div id="my-div">click</div>
<script type="text/javascript">
var myelement = document.getElementById('my-div');
myelement.onclick = function() {
alert('Ouch!');
alert('And double ouch!');
}
</script>

这种方式实际上更好,因为它可以帮助你保持你的<div>没有任何 JavaScript 代码。请始终记住,HTML 代表内容,JavaScript 代表行为,CSS 代表格式,您应该尽可能将这三者分开。

这种方法的缺点是,您只能为事件附加一个函数,就好像广播节目只有一个侦听器一样。的确,在同一个功能中可以发生很多事情,但这并不总是方便的,好像所有的收音机听众都在同一个房间里。

DOM 事件监听器

处理浏览器事件的最佳方式是使用 DOM Level 2 中概述的事件侦听器方法,在该方法中,您可以让许多函数侦听事件。当事件触发时,所有的功能都被执行。所有的听众不需要互相了解,可以独立工作。他们可以在不影响其他听众的情况下随时进出。

让我们使用与上一节相同的简单标记(可在http://www.phpied.com/files/jsoop/ch7.html中使用)。我们有一块标记:

<p id="closer">final</p>

您的 JavaScript 代码可以使用 addEventListener()方法为点击事件分配监听器:

>>> var mypara = document.getElementById('my-div');
>>> mypara.addEventListener('click', function() {alert('Boo!')}, false);
>>> mypara.addEventListener('click', console.log, false);

如您所见, addEventListeners()是在节点对象上调用的方法,接受事件类型作为其第一个参数,接受函数指针作为其第二个参数。可以使用 function(){alert('Boo!')}等匿名函数,也可以使用 console.log等现有函数。当事件发生时,您指定的侦听器函数将被调用,一个参数将被传递给它们。该参数是一个事件对象。如果您运行上面的代码并单击最后一段,您可以看到事件对象被记录到 Firebug 控制台。

DOM Event Listenerseventselement properties

单击事件对象可以查看其属性。

DOM Event Listenerseventselement properties

捕捉和冒泡

在上面对 addEventListener()的调用中,有第三个参数 false。让我们看看这是干什么用的。

假设您在无序列表中有一个链接,如下所示:

<body>
<ul>
<li><a href="http://phpied.com">my blog</a></li>
</ul>
</body>

当你点击链接时,你实际上也是在点击列表项<li>、列表<ul><body>以及最终整个文档。点击一个链接也可以看作是点击文档,因为事件传播。传播事件的过程可以通过两种方式实现:

  • 事件捕获—单击首先发生在文档上,然后传播到正文、列表、列表项,最后传播到链接。

  • 事件冒泡—点击链接,然后冒泡到文档。

DOM 级别 2 事件规范建议事件分三个阶段传播:捕获、瞄准和冒泡。这意味着事件从文档传播到链接(目标),然后再回到文档。事件对象具有 eventPhase属性,该属性反映当前阶段。

Capturing and Bubbling

历史上,IE 和网景(独立工作,没有标准可循)实现了完全相反的东西。IE 只实现冒泡,网景只捕捉。在 DOM 规范之后,火狐、Opera 和 Safari 实现了这三个阶段,但 IE 只保持了冒泡。

与事件传播相关的实际意义是什么?

  • addEventListener()的第三个参数指定是否应该使用捕捉。为了让你的代码更容易跨浏览器移植,最好总是将这个参数设置为 false,并且代码只使用冒泡。

  • 您可以在侦听器中停止事件的传播,这样它就不会冒泡,也不会到达文档。为此,您可以调用事件对象的 stopPropagation()方法(下一节有一个例子)。

  • 也可以使用事件委托。如果一个<div>中有十个按钮,你总是可以附加十个事件监听器,每个按钮一个。但是更聪明的做法是只在包装上附加一个监听器<div>,当事件发生时,检查哪个按钮是点击的目标。

完全公平地说,IE 中有一种使用事件捕捉的方式(使用 setCapture()releaseCapture()方法),但只针对鼠标事件。不支持捕获任何其他事件(例如击键事件)。

停止传播

让我们看一个如何阻止事件冒泡的例子。回到测试文档,我们有:

<p id="closer">final</p>

让我们定义一个处理段落点击的函数:

>>> function paraHandler(){alert('clicked paragraph');}

现在让我们将这个函数作为 click 事件的监听器:

>>> var para = document.getElementById('closer');
>>> para.addEventListener('click', paraHandler, false);

让我们也将侦听器附加到正文、文档和浏览器窗口上的 click 事件:

>>> document.body.addEventListener('click', function(){alert ('clicked body')}, false);
>>> document.addEventListener('click', function(){alert ('clicked doc')}, false);
>>> window.addEventListener('click', function(){alert ('clicked window')}, false);

请注意,DOM 规范没有说明窗口上的任何事件。为什么会这样,因为 DOM 处理的是文档,而不是浏览器。IE 不会将点击事件传播到窗口,但火狐会。

现在,如果您点击该段落,您将看到四条提示:

  • 点击段落

  • 点击正文

  • 点击单据

  • 点击窗口

这说明了同一个单击事件如何从目标一直传播到窗口。

addEventLister()的反义词是 removeEventListener(),它接受完全相同的参数。让我们去掉附在段落上的监听器。

>>> para.removeEventListener('click', paraHandler, false);

如果您现在尝试,您将只在正文、文档和窗口上看到单击事件的提醒,而不会在段落上看到。

现在让我们停止事件的传播。作为监听器添加的函数接收事件对象作为参数,您可以调用该事件对象的 stopPropagation()方法:

function paraHandler(e){
alert('clicked paragraph');
e.stopPropagation();
}

添加修改后的侦听器:

>>> para.addEventListener('click', paraHandler, false);

现在,如果您单击该段落,您将只看到一个警报,因为事件不会冒泡到正文、文档或窗口。

请注意,您不能移除使用匿名函数的侦听器。如果您移除一个监听器,您必须传递一个指针到您之前附加的同一个函数。但是两个匿名函数仍然是内存中某个地方的两个独立的函数对象,即使它们有完全相同的主体。这样做是行不通的:

document.body.removeEventListener('click',
eventspropogation, stoppingfunction(){
alert('clicked body')
},
false); // does NOT remove the handler

防止违约行为

某些浏览器事件具有预定义的行为。例如,单击链接会加载另一个页面。您可以将侦听器附加到链接上的单击,也可以禁用默认行为。为此,可以在事件对象上调用方法 preventDefault()

让我们看看你如何通过询问**来激怒你的访问者。你确定要关注这个链接吗?**每次他们点击一个链接。如果用户点击取消(导致 confirm()返回 false,则调用 preventDefault()方法:

// all links
var all_links = document.getElementsByTagName('a');
for (var i = 0; i < all_links.length; i++) { // loop all links
all_links[i].addEventListener(
'click', // event type
function(e){ // handler
if (!confirm('Are you sure you want to follow this link?')){
e.preventDefault();
}
},
false); // don't use capturing
}

请注意,并非所有事件都允许您阻止默认行为。大多数都有,但是如果你想确定,可以检查事件对象的 cancellable属性。

跨浏览器事件监听器

正如您已经知道的,大多数现代浏览器几乎完全实现了 DOM Level 1 规范。然而,事件直到 DOM 2 才被标准化。因此,与火狐、Opera 和 Safari 相比,IE 实现这一功能的方式有很大的不同。

查看一个导致被点击元素(目标元素)的 nodeName被写入控制台的示例:

document.addEventListener('click', function(e){
console.log(e.target.nodeName);
}, false);

现在让我们来看看 IE 有何不同:

  • 在 IE 中没有 addEventListener()方法,尽管自从 IE 第 5 版以来就有了一个等价的 attachEvent()。对于早期版本,您唯一的选择是直接访问属性(如 onclick)。

  • 使用 attachEvent()click事件变为 onclick

  • 如果你用传统的方式听事件(例如,通过给 onclick属性设置一个函数值),当回调函数被调用时,它不会得到一个作为参数传递的事件对象。但是不管你如何附加监听器,在 IE 中总有一个全局对象 window.event指向事件。

  • 在 IE 中,事件对象没有一个 target属性告诉你事件触发的元素,但是它有一个等价的属性叫做 srcElement

  • 如前所述,事件捕获并不适用于所有事件,因此应该只使用冒泡。

  • 没有 stopPropagation()方法,但是可以将仅限 IE 的 cancelBubble属性设置为 true

  • 没有 preventDefault()方法,但是可以将仅限 IE 的 returnValue属性设置为 false

  • 为了停止收听某个事件,你需要 detachEvent(),而不是 IE 中的 removeEventListener()

这里是上面代码的修订版本,可以跨浏览器工作:

function callback(evt) {
// prep work
evt = evt || window.event;
var target = (typeof evt.target !== 'undefined') ? evt.target : evt.srcElement;
// actual callback work
console.log(target.nodeName);
}
// start listening for click events
if (document.addEventListener){ // FF
document.addEventListener('click', callback, false);
} else if (document.attachEvent){ // IE
document.attachEvent('onclick', callback);
} else {
document.onclick = callback;
}

事件类型

现在您知道如何处理跨浏览器事件了。但是上面所有的例子都只使用了点击事件。外面还发生了什么事?正如你可能猜到的,不同的浏览器提供不同的事件。有一个跨浏览器事件的子集和一些特定于浏览器的事件。有关事件的完整列表,您应该查阅浏览器的文档,但这里有一些跨浏览器事件的选择:

  • 鼠标事件

    • mouseup,mousedown,click(顺序是 mousedown-up-click),dblclick

    • mouseover(鼠标在元素上)、mouseout(鼠标在元素上但离开它)、mousemove

  • 键盘事件

    • 向下键、按下键、向上键(按此顺序出现)
  • 加载/窗口事件

    • 加载(图像或页面及其所有组件都已加载完毕)、卸载(用户离开页面)、卸载前(脚本可以为用户提供停止卸载的选项)

    • 中止(用户停止加载火狐中的页面或 IE 中的图像)、错误(火狐和 IE 中的 JavaScript 错误,也是在 IE 中无法加载图像时)

    • 调整大小(调整浏览器窗口大小)、滚动(页面滚动)、上下文菜单(出现右键菜单)

  • 表单事件

    • 聚焦(输入表单域),模糊(离开表单域)

    • 更改(在值更改后保留字段),选择(在文本字段中选择文本)

    • 重置,提交

事件的讨论到此结束。关于创建自己的事件实用程序来处理跨浏览器事件的小挑战,请参考本章末尾的练习部分。

XMLHttpRequest

XMLHttpRequest()是一个对象(一个构造函数),允许你从 JavaScript 发送 HTTP 请求。历史上,XMLHttpRequest(或简称 XHR)是在 IE 中引入的,最初是作为 ActiveX 对象实现的。从 IE7 开始,它是一个本机浏览器对象,就像它在火狐、Safari 和 Opera 中一样。这个对象跨浏览器的常见实现产生了所谓的 AJAX 应用,在这里不再需要每次需要新内容时都刷新整个页面。使用 JavaScript,您可以向服务器发出 HTTP 请求,获得响应并只更新页面的一部分。通过这种方式,您可以构建响应速度更快、类似桌面的网页。

AJAX 代表异步 JavaScript 和 XML。

  • 异步因为在发送一个 HTTP 请求之后,你的代码不需要等待响应,但是它可以做其他事情,并且在响应到达时被通知(通过一个事件)。

  • JavaScript—很明显,我们用 JavaScript 创建 XHR 对象。

  • 因为最初开发人员对 XML 文档进行 HTTP 请求,并使用其中包含的数据来更新页面。然而,这不再是一种常见的做法,因为您可以以纯文本、更方便的 JSON 格式或者简单地作为准备插入页面的 HTML 来请求数据。

使用 XMLHttpRequest 实际上有两个步骤:

  • 发送请求——这包括创建一个 XMLHttpRequest 对象和附加一个事件监听器

  • 处理响应—您的事件侦听器会收到响应已经到达的通知,您的代码会忙于处理响应

发送请求

为了创建一个对象,您只需使用这个(我们稍后会处理浏览器不一致的地方):

var xhr = new XMLHttpRequest();

接下来的事情是将一个事件监听器附加到由对象触发的 readystatechange事件上:

xhr.onreadystatechange = myCallback;

然后需要调用 open()方法,如下所示:

xhr.open('GET', 'somefile.txt', true);

第一个参数指定 HTTP 请求的类型(GET、 POST, HEAD等)。 GETPOST是最常见的。当您不需要随请求发送太多数据时,使用 GET,否则使用 POST。第二个参数是您请求的网址。在这个例子中,是文本文件 somefile.txt与页面位于同一个目录。最后一个参数是一个布尔值,指定请求是异步的(true)还是非异步的(false)。

最后一步是实际启动请求。

xhr.send('');

方法 send()接受您想要随请求发送的任何数据。对于 GET请求,这是一个空字符串,因为数据在网址中。对于 POST请求,它是一个形式为 key=value&key2=value2的查询字符串。

此时,请求被发送,您的代码(和用户)可以继续执行其他任务。当服务器返回响应时,将调用回调函数 myCallback

处理响应

我们已经为 readystatechange事件安排了一名听众。什么是就绪状态,它是如何变化的?

XHR 对象有一个属性叫做 readyState。每次改变, readystatechange事件就会触发。 readyState房产的可能价值如下:

  • 0—未初始化

  • 1—装载

  • 2—已加载

  • 3-交互式

  • 4—完成

readyState得到 4 的值时,表示响应回来,准备处理。在 myCallback中,确定 readyState4 后,另一个要检查的是 HTTP 请求的状态码。例如,您可能请求了一个不存在的网址,并获得了一个 **404(找不到文件)**状态代码。有趣的代码是 200 (OK) 代码,所以 myCallback应该检查这个值。XHR 对象的 status属性中有状态代码。

一旦 xhr.readyState4xhr.status200 ,您就可以使用 xhr.responseText属性访问请求的网址的内容。让我们看看如何实现 myCallback来简单地 alert()请求的网址的内容。

function myCallback() {
if (xhr.readyState < 4) {
return; // not ready yet
}
if (xhr.status !== 200) {
alert('Error!'); // the HTTP status code is not OK
return;
}
// all is fine, do the work
alert(xhr.responseText);
}

一旦你收到了你请求的新内容,你可以把它添加到页面上,或者用它来做一些计算,或者用于你认为合适的任何其他目的。

总的来说,这个两步过程(发送请求、处理响应)是整个 XHR/AJAX 功能的核心。现在你已经知道了基础知识,你可以继续构建下一个 Gmail 或者下一个雅虎了!地图。哦,是的,让我们看看一个小的浏览器不一致。

在 7 版之前的 IE 中创建 XMLHttpRequest 对象

在版本 7 之前的 Internet Explorer 中,XMLHttpRequest 对象是一个 ActiveX 对象,因此创建 XHR 实例有点不同。大概是:

var xhr = new ActiveXObject('MSXML2.XMLHTTP.3.0');

MSXML2。XMLHTTP.3.0 是您想要创建的对象的标识符。XMLHttpRequest 对象有几个版本,如果您的页面访问者没有安装最新的版本,您可以在放弃之前尝试两个旧版本。

对于完全跨浏览器的解决方案,您应该首先测试用户的浏览器是否支持 XMLHttpRequest 作为本机对象,如果不支持,请尝试 IE 方式。因此,创建 XHR 实例的整个过程可能如下所示:

var ids = ['MSXML2.XMLHTTP.3.0',
'MSXML2.XMLHTTP',
'Microsoft.XMLHTTP'];
var xhr;
if (typeof window.XMLHttpRequest === 'function') {
xhr = new XMLHttpRequest();
} else {
for (var i = 0; i < ids.length; i++) {
try {
xhr = new ActiveXObject(ids[i]);
break;
} catch (e){}
}
}

这是在干什么?数组 ids包含一个要尝试的 ActiveX 程序标识列表。变量 xhr将指向新的 XHR 对象。代码首先检查 windows.XMLHttpRequest是否是有效的函数。如果是,这意味着浏览器原生支持 XMLHttpRequest()(因此浏览器是 Firefox、Safari、Opera 或 IE7(或更高版本)之一)。如果不是,代码将循环通过 ids试图创建一个对象。 catch(e)悄悄捕捉故障,循环继续。一旦一个 xhr对象被创建,我们 break就退出循环。

如您所见,这是相当多的代码,因此最好将其抽象成一个函数。实际上,本章末尾的练习之一提示您创建自己的 AJAX 实用程序。

A 代表异步

现在您知道如何创建一个 XHR 对象,给它一个网址并处理对请求的响应。异步发送两个请求会发生什么?如果对第二个请求的响应先于第一个请求呢?

在上面的示例中,XHR 对象是全局的, myCallback依赖于该全局对象的存在来访问其 readyState, statusresponseText属性。另一种防止依赖全局变量的方法是用闭包包装回调。让我们看看如何:

var xhr = new XMLHttpRequest();
xhr.onreadystatechange = (function(myxhr){
return function(){myCallback(myxhr);}
})(xhr);
xhr.open('GET', 'somefile.txt', true);
xhr.send('');

在这种情况下 myCallback()将接收 XHR 对象作为参数,并且不打算在全局空间中寻找它。这也意味着在收到响应时,原始的 xhr可能已经被重新用于第二个请求,甚至被销毁。闭包将继续指向原始对象。

X 代表 XML

尽管如今 JSON(在下一章中讨论)作为数据传输格式比 XML 更受欢迎,但 XML 仍然是一种选择。除了 responseText属性,XHR 对象还有另一个属性叫做 responseXML。如果您发送一个对 XML 文档的 HTTP 请求, responseXML将指向一个 XML DOM 文档对象。要处理这个文档,您可以使用本章前面讨论的所有核心 DOM 方法,例如 getElementsByTagName(), getElementById()等等。

一个例子

让我们用一个例子来总结不同的 XHR 话题。您可以访问位于http://www.phpied.com/files/jsoop/xhr.html的页面,亲自研究该示例。

主页面 xhr.html是一个简单的静态页面,除了三个<div>s什么都没有。

<div id="text">Text will be here</div>
<div id="html">HTML will be here</div>
<div id="xml">XML will be here</div>

使用 Firebug 控制台,您可以编写代码来请求三个文件,并将它们各自的内容加载到每个<div>中。

要加载的三个文件是:

  • content.txt —包含文本“我是文本文件”的简单文本文件

  • content.html —包含一些 HTML 代码的文件:

"I am <strong>formatted</strong> <em>HTML</em>"
  • content.xml —一个 XML 文件,包含:
<?xml version="1.0" ?>
<root>
I'm XML data.
</root>

所有文件都存储在与 xhr.html相同的目录中。请注意,出于安全原因,您只能使用 XMLHttpRequest 来请求同一域中的文件。

首先,让我们创建一个函数来抽象请求/响应部分:

function request(url, callback) {
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = (function(myxhr){
return function(){
callback(myxhr);
}
})(xhr);
xhr.open('GET', url, true);
xhr.send('');
}

这个函数接受一个请求的网址和一个一旦响应到达就调用的回调函数。我们可以调用函数三次,每个文件一次,如下所示:

request(
'http://www.phpied.com/files/jsoop/content.txt',
function(o){
document.getElementById('text').innerHTML = o.responseText;
}
);
request(
'http://www.phpied.com/files/jsoop/content.html',
function(o){
document.getElementById('html').innerHTML = o.responseText;
}
);
request(
'http://www.phpied.com/files/jsoop/content.xml',
function(o){
document.getElementById('xml').innerHTML =
o.responseXML.getElementsByTagName('root')[0] .firstChild.nodeValue;
}
);

回调函数是内联定义的。前两个非常相似。他们只是用请求文件的内容替换相应<div>的 HTML。第三个有点不同,因为它处理的是 XML 文档。首先,我们以 o.responseXML的形式访问 XML DOM 对象。然后,使用 getElementsByTagName(),我们得到所有<root>标签的列表(只有一个)。<root>firstChild是一个文本节点,使用 nodeValue我们得到包含在其中的文本(“我是 XML 数据”)。然后我们再次用新内容替换<div id="xml">的 HTML。结果显示在下图中:

An Example

在处理 XML 文档时,也可以使用 o.responseXML.documentElement来获取<root>元素,而不是 o.responseXML.getElementsByTagName('root')[0]。记住 documentElement给了你一个 XML 文档的根节点。HTML 文档中的根总是<html>标签。

总结

这一章我们已经讲了很多。您已经学习了一些跨浏览器 BOM ( 浏览器对象模型)对象:

  • 全局 window对象的属性,如 navigator, location, history, frames, screen

  • setInterval() setTimeout(); alert(), confirm() prompt(); moveTo/By() resizeTo/By()等方法

然后,您了解了 DOM ( 文档对象模型)作为一种将 HTML(或 XML)文档表示为树结构的方式,其中每个标签或文本都是树上的一个节点。您学会了如何:

  • 访问节点:

    • 使用父/子关系属性父节点, childNodes, firstChild, lastChild, nextSibling, previousSibling

    • 使用 getElementsById(), getElementsByTagName(), getElementsByName()

  • 修改节点:

    • 使用 innerHTMLinnerText/textContent

    • 使用 nodeValuesetAttribute()或仅使用属性作为对象属性

  • removeChild()replaceChild()移除节点

  • 并用 appendChild(), cloneNode(), insertBefore()添加新的

我们还查看了一些移植到 DOM Level 1 的 DOM0(预标准化)属性:

  • 收藏: document.forms, images, links, anchors, applets。不鼓励使用这些,因为 DOM1 有更灵活的方法 getElementsByTagName()

  • document.body这是进入<body>的便捷方式。

  • document.title, cookie, referrer, domain

接下来,您了解了浏览器如何广播您可以收听的事件。以跨浏览器的方式做到这一点并不简单,但这是可能的。事件冒泡,所以你知道可以使用事件委托来更全局地收听事件。您还可以停止事件的传播,并干扰默认的浏览器行为。

最后,您了解了 XMLHttpRequest对象,该对象允许您构建响应性网页:

  • 向服务器发出获取数据的 HTTP 请求

  • 处理响应以更新页面的部分内容

练习

在前几章中,练习的解决方案可以在本章的正文中找到。这一次,一些练习可能需要你在这本书之外做更多的阅读(或实验)。

  1. 1.月初(beginning of month 的缩写)

    作为一个 BOM 练习,试着编写一些错误的、突兀的、对用户不友好的,总而言之,非常 Web 1.0 的东西:摇晃的浏览器窗口。尝试实现像地震一样移动窗口的代码。你所需要的只是一个move*()功能,一个或多个对setInterval()的调用,也许还有一个对setTimeout()的调用来停止整件事。或者打开一个 200x200 的弹出窗口,然后慢慢地逐渐调整到 400x400 怎么样?或者这里有一个更简单的方法:在状态栏(window.status)中打印当前日期/时间,并像时钟一样每秒更新一次。请注意,对于这些练习,您需要在浏览器中允许一些默认情况下通常被禁用的功能,因为人们已经厌倦了这种只会恶化用户体验的“效果”(在火狐中,转到工具|选项|内容|启用 JavaScript |高级)。

  2. 2.数字正射影像图

    2.1.以不同的方式执行walkDOM()。也让它接受一个回调函数,而不是硬编码console.log()

    2.2.用innerHTML移除内容很容易(document.body.innerHTML = ''),但并不总是最好的。问题是,如果有任何事件侦听器,附加到被移除的元素,它们不会在 IE 中被移除,导致浏览器泄漏内存,因为它存储了对不存在的东西的引用。实现一个通用函数,删除 DOM 节点,但首先删除任何事件侦听器。您可以遍历节点的属性,并检查该值是否为函数。如果是,很可能是像onclick这样的属性。在从树中移除元素之前,您需要将其设置为null

    2.3.创建一个名为include()的函数,根据需要包含外部脚本。这意味着动态创建一个新的<script>标签并设置其src属性。使用以下方法进行测试:

>>> include('somescript.js');
  • 2.4.使用 2.3 中的函数。,消费一个雅虎!用 JavaScript 搜索服务。文件在这里:http://developer.yahoo.com/search/web/V1/webSearch.html。在构造请求的 URL 时,需要设置 output=json 和 callback=console.log,这样服务调用的结果(一个 JavaScript 对象)就会打印在控制台中。将console.log替换为您选择的功能,以创建更有趣的内容。
  1. 3.事件

    3.1.创建一个名为myevent的事件实用程序(对象),它有以下跨浏览器工作的方法:

    • addListener(element, event_name, callback)—其中 element也可以是元素的数组

    • removeListener(element, event_name, callback)

    • getEvent(event) —只是为了检查旧版本 IE 的 window.event

    • getTarget(event)

    • stopPropagation(event)

    • preventDefault(event)

用法示例:

function myCallback(e) {
e = myevent.getEvent(e);
alert(myevent.getTarget(e).href);
myevent.stopPropagation(e);
myevent.preventDefault(e);
}
myevent.addListener(document.links, 'click', myCallback);

示例代码的结果应该是,文档中的所有链接都不会导致任何结果,而只会提醒 href属性。

  • 3.2.创建一个绝对定位的<div>,比如 x=100px,y=100px。编写代码,以便能够使用箭头键或键 J (左)、 K (右)、 M (下)、 I (上)在页面中移动 div。从 3.1 中重用您自己的事件实用程序。
  1. 4.XMLHttpRequest

    4.1.创建自己的名为ajax的 XHR 实用程序(对象)。示例用途:

function myCallback(xhr) {
alert(xhr.responseText);
}
ajax.request('somefile.txt', 'get', myCallback);
ajax.request('script.php', 'post', myCallback, 'first=John&last=Smith');
  • 4.2.谷歌搜索。使用 Firebug,您可以“插入”JavaScript,就像它是页面的一部分一样。这将允许您使用 XHR 请求 google.com 的页面。因此,访问google.com并编写代码,该代码将允许您在进行搜索时不加载第二页,而是在不刷新页面的情况下加载搜索表单下的结果。重用您自己的事件实用程序(来自练习 3.1。)和您自己的 AJAX 实用程序(从 4.1 开始。).请遵循以下步骤:

    • 将事件侦听器附加到表单的提交事件,并防止默认行为导致表单不被提交;

    • 创建一个 XHR 对象,并在网址http://www.google.com/search?q=myquery请求页面,其中我的查询是您在搜索栏中键入的任何内容;

    • 在 XHR 的回调中,将带有 id="content"的新<div>追加到<body>中,将其 innerHTML设置到 XHR 的 responseText中。

这样你应该可以在搜索栏中输入,按进入即可获得搜索结果,无需加载新页面。您应该可以重复这个过程任意多次,不同的是您只创建一次内容 div,然后只更新它的 HTML。