张啸


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


JS(8) Window对象

Window对象是客户端JavaScript程序的全局对象,本文介绍Window对象的属性和方法,这些属性定义了许多不同的API。

本文主要介绍以下方面:

  • setTimeout()setInterval()

  • location属性,以及如何获取当前显示文档的url和载入新的文档

  • history属性,以及如何在历史记录中向前和向后移动

  • navigator属性,以及如何获取浏览器厂商和版本信息,如何使用screen属性查询窗口尺寸

  • alert()promt()confirm()showModalDialog()

  • onerror处理方法,以及如何在未捕获的JavaScript异常发生时调用

  • HTML元素的IDname作为Window对象的属性来使用

  • 如何打开和关闭浏览器窗口,以及如何编写可以在多个窗口和嵌套窗体中工作的JavaScript代码


一、计时器

setTimeout()setInterval()可以用来注册在指定时间之后单次或重复调用的函数。

Window对象的setTimeout()方法用来实现一个函数在指定的毫秒数之后运行。setTimeout()返回一个值,这个值可以传递给clearTimeout()用于取消这个函数的执行。

setInterval()setTimeout()一样,只不过这个函数会在指定毫秒数的间隔里重复调用。

由于历史原因,setTimeout()setInterval()的第一个参数可以作为字符串传入。如果这么做,那这个字符串会在指定的超时时间或间隔之后进行求值(相当于执行eval())。除前两个参数之外,H5规范还允许setTimeout()setInterval()传入额外的参数,并在调用函数时把这些参数传递过去。

如果以0毫秒的超时时间来调用setTimeout(),那么指定的函数不会立刻执行。相反,会把它放到队列中,等到前面处于等待状态的事件处理程序全部执行完成后,再“立刻”调用它。


二、浏览器定位和导航

Window对象的location属性引用的是Location对象,它表示该窗口中当前显示的文档的url,并定义了方法来使窗口载入新的文档。

Document对象的location属性也引用到Location对象

1
window.location == document.location;           // 总是返回true

Document对象也有一个url属性,是文档首次载入后保存该文档的url的静态字符串。如果定位到文档中的片段标识符,Location对象会做相应的更新,而document.url属性则不会改变。

1. 解析url

Window对象的location属性引用的是Location对象,它表示该窗口中当前显示的文档的url。Location对象的href属性是一个字符串,后者包含url的完整文本。Location对象的toString()方法返回href属性的值,因此会隐式调用toString()的情况下,可以使用location代替location.href

这个对象的其他属性——protocolhosthostnameportpathnamesearch,分别表示url的各个部分。他们成为“url分解”属性。

Location对象的hashsearch属性比较有趣。如果有的话,hash属性返回url中的“片段标识符”部分。search属性也类似,它返回的是问号之后的url,这部分通常是某种类型的查询字符串。一般来说,这部分内容是用来参数化url并在其中嵌入参数的。

2. 载入新的文档

Location对象的assign()方法可以使窗口载入并显示指定的url中的文档。replace()方法也类似,但它在载入新文档之前会从浏览历史中把当前文档删除。如果脚本无条件地载入一个新文档,replace()方法可能是比assign()方法更好的选择。否则,“后退”按钮会把浏览器带回到原始文档,而相同的脚本则会再次载入新文档。如果检测到用户的浏览器不支持某些特性来显示功能齐全的版本,可以用location.replace()来载入静态的HTML版本

1
2
3
4
5
// 如果浏览器不支持XMLHttpRequest对象
// 则将其重定向到一个不需要Ajax的静态页面
if (!XMLHttpRequest) {
location.replace('staticpage.html');
}

注意,在这个例子中传入replace()的是一个相对url。相对url是相对于当前页面所在的目录来解析的,就像将它们用于一个超链接中。

除了assign()replace()方法,Location对象还定义了reload()方法,后者可以让浏览器重新载入当前文档。

使浏览器跳转到新页面的一种更传统的方法是直接把新的url赋值给location属性

1
location = "http://www.orilly.com";

还可以吧相对url赋给location,它们会相对当前url进行解析

1
location = "page2.html";

纯粹的片段标识符是相对url的一种类型,它不会让浏览器载入新文档,但只会使它滚动到文档的某个位置。#top标识符是个特殊的例子:如果文档中没有元素的ID是“top”,它会让浏览器跳到文档开始处。

1
2
// 跳转到文档的顶部
location = '#top';

Location对象的URL分解属性是可写的,对它们重新赋值会改变url的位置,并且导致浏览器载入一个新的文档(如果改变的是hash属性,则在当前文档中进行跳转)

1
location.search = '?page=' + (pagenum + 1);

三、浏览历史

History对象用来把窗口的浏览历史用文档和文档状态列表的形式表示。History对象的length属性表示浏览历史列表中的元素数量,但出于安全的因素,脚本不能访问已保存的url。(如果允许,则任意脚本都可以窥探我们的浏览历史)。

如果窗口包含多个子窗口(比如<iframe>元素),子窗口的浏览历史会按时间顺序穿插在主窗口的历史中。这意味着在主窗口调用history.back()可能会导致其中一个子窗口往回跳转到前一个显示的文档,但主窗口保留当前状态不变。


四、浏览器和屏幕信息

脚本有时候需要获取和它们所在的Web浏览器或浏览器所在的桌面相关的信息。本节所介绍Window对象的navigatorscreen属性。它们分别引用的是Navigator和Screen对象,而这些对象提供的信息允许脚本来根据环境定制自己的行为。

1. Navigator对象

Window对象的navigator属性引用的是包含浏览器厂商和版本信息的Navigator对象。Navigator对象的命名是为了纪念Netscape之后Navigator浏览器,不过所有其他的浏览器也支持它。

过去,Navigator对象通常被脚本用来确定它们是在IE中还是在Netscape中运行。这种浏览器嗅探方法有问题,因为它要求随着新浏览器和现有浏览器的新版本的引入而不断地调整。如今,有一种更好地功能测试方法,只需要测试所需要的功能,而不是假设特定的浏览器版本及其功能。

然而,浏览器嗅探有时候仍然有价值。这样的一种情况是,当需要解决存在于某个特定的浏览器的特定版本中的特殊的bug时。Navigator对象有4个属性用于提供关于运行中的浏览器的版本信息,并且可以使用这些属性进行浏览器嗅探。

  • appName

    Web浏览器的全称。

  • appVersion

    此属性通常以数字开始,并跟着包含浏览器厂商和版本信息的详细字符串。

  • userAgent

    浏览器在它的USER-AGENT HTTP头部中发送的字符串。这个属性通常包含appVersion中的所有信息,并且常常也可能包含其他的细节。

  • platform

    在其上运行浏览器的操作系统(并且可能是硬件)的字符串。

下文示例中展示了如何使用正则表达式从navigator.userAgent中抽取浏览器名称和版本号的方法(摘自jQuery)

1
2
3
4
5
6
7
8
9
10
var browser = (function() {
var s = navigator.userAgent.toLowerCase();
var match = /(webkit)[ \/]([\w.]+)/.exec(s) ||
/(opera)(?:.*version)?[ \/]([\w.]+)/.exec(s) ||
/(msie)([\w.]+)/.exec(s) ||
!/compatible/.test(s) && /(mozilla)(?:.*? rv:([\w.]+))?/.exec(s) ||
[];

return { name: match[1] || '', version: match[2] || '0' }
}());

除了浏览器厂商和版本信息的属性之外,Navigator对象还包含一些杂项的属性和方法。以下是一些标准化的属性以及广泛应用但未标准化的属性:

  • onLine

    navigator.onLine属性表示浏览器当前是否连接到网络。

  • geolocation

    Geolocation对象定义用于确定用户地理位置信息的接口。

  • javaEnabled()

    一个非标准的方法,当浏览器可以运行Java小程序时返回true。

  • cookieEnabled()

    一个非标准的方法,如果浏览器可以保存永久的cookie时,返回true。当cookie配置为“视具体情况而定”时可能会返回不正确的值。

2. Screen对象

Window对象的screen属性引用的是Screen对象。它提供有关窗口显示的大小和可用的颜色数量信息。属性widthheight指定的是以像素为单位的窗口大小。属性availWidthavailHeight指定的是实际可用的显示大小,它们排除了像桌面任务栏这样的特性所占用的控件。属性colorDepth指定的是显示的BPPbits-per-pixel)值,典型的值有16、24和32.

window.screen属性和它引用的Screen对象都是非标准但广泛实现的。可以用Screen对象来确定Web应用是否运行在一个小屏幕的设备上。


五、对话框

Window对象提供了3个方法来向用户显示简单的对话框。alert()向用户显示一条信息并等待用户关闭对话框。confirm()也显示一条信息,要求用户单机“确定”或“取消”按钮,并返回一个布尔值。propty()同样也显示一条消息,等待用户输入字符串,并返回那个字符串。

1
2
3
4
5
6
do {
var name = promp('What is your name?');
var correct = confirm('You entered: ') + name + '\n'
+ 'Click Okay to proceed of Cancel to re-enter;'
} while (!correct)
alert('Hello, ' + name);

方法confirm()prompt()都会产生阻塞,也就是说,在用户关掉它们所显示的对话框之前,它们不会返回。这就意味着在弹出一个对话框前,代码就会停止运行。如果当前正在载入文档,也会停止载入,直到用户用要求的输入进行响应为止。在大多数的浏览器里,alert()也会产生阻塞,并等待用户关闭对话框,但并不总是这样。


六、错误处理

Window对象的onerror属性是一个事件处理程序,当未捕获的异常传播到调用栈上时就会调用它,并把错误消息输出到浏览器的JavaScript控制台上。如果给这个属性赋一个函数,那么只要这个窗口中发生了JavaScript错误,就会调用该函数,即它成了窗口的错误处理程序。

由于历史原因,Window对象的onerror事件处理函数的调用通过三个字符串参数,而不是通过通常传递的一个事件对象。

window.onerror的第一个参数是描述错误的一条消息。第二个参数是一个字符串,它存放引发错误的JavaScript代码所在的文档的url。第三个参数是文档中发生错误的行数。

除了这三个参数之外,onerror处理程序的返回值也很重要。如果onerror处理程序返回false,它通知浏览器事件处理程序已经处理了错误,不需要其他操作。换句话说,浏览器不应该显示它字迹的错误消息。遗憾的是,由于历史原因,Firefox里的错误处理程序必须返回true来表示它已经处理了错误。

onerror处理程序是早期JavaScript的遗物,那时语言核心不包含try/catch异常处理语句。现代代码很少使用它,但是在开发阶段,你可能要这样定义一个错误处理程序,当由错误发生时,来显式地通知你

1
2
3
4
5
6
7
8
window.onerror = function(msg, url, line) {
if (onerror.num++ < onerror.max) {
alert('ERROR: ' + msg + '\n' + url + ':' + line);
return true;
}
}
onerror.max = 3;
onerror.num = 0;

七、作为Window对象属性的文档元素

如果在HTML文档中用id属性来为元素命名,并且如果Window对象没有此名字的属性,Window对象会赋予一个属性,它的名字是id属性的值,而它们的值指向表示文档元素的HTMLElement对象。

在客户端JavaScript中,Window对象是以全局对象的形式存在于作用域链的最上层,这就意味着在HTML文档中使用的id属性会成为可以被脚本访问的全局变量。如果文档包含一个<button id="okay"/>元素,可以通过全局变量okay来引用此元素。

但是,有一个重要的警告:如果Window对象已经具有此名字的属性,这就不会发生。比如,id是“history”、“location”或“navigator”的元素,就不会以全局变量的形式出现,因为这些id已经占用了。同样,如果HTML文档包含一个id为“x”的元素,并且还在代码中声明并赋值给全局变量x,那么显式声明的变量会隐藏隐式的元素变量。如果脚本中的变量声明出现在命名元素之前,那这个变量的存在就会阻止元素获取它的window属性。而如果脚本的变量声明出现在命名元素之后,那么变量的显式赋值会覆盖该属性的隐式值。

元素ID作为全局变量的隐式应用是Web浏览器演化过程中遗留的怪癖。它主要是出于与已有Web页面后向兼容性的考虑。但这里并不推荐使用这种做法——浏览器厂商可以在任何时候为Window对象定义新属性,而这些新属性都会破坏使用了此属性名的隐式定义的代码。

document.getElementById()来显示查找元素,如果给它一个更简单的名字,这种用法会变得更加简便。

1
2
3
4
var $ = function(id) {
return document.getElementById(id);
}
ui.prompt = $('prompt');

假设ID并没有被Window对象使用的话,那么任何由id属性的HTML元素都会成为全局变量的值。以下HTML元素如果由name属性的话,也会这样表现:

<a> <applet> <area> <embed> <form> <frame> <frameset> <iframe> <img> <object>

id元素在文档中必须是唯一的:两个元素不能有相同的id。但是,这对name属性无效。如果上面的元素有多于一个有相同的name属性(或者一个元素有name属性,而另一个元素具有相同值的id属性),具有该名称的隐式全局变量会引用一个类数组对象,这个类数组对象的元素是所有命名的元素。

nameid属性的<iframe>元素是个特殊的例子,为它们隐式创建的变量不会引用表示自身的Element对象,而是引用表示<iframe>元素创建的嵌套浏览器窗体的Window对象。


八、多窗口和窗体

一个Web浏览器窗口可能在桌面上包含多个标签页。每一个标签页哦都市独立的“浏览上下文”(browsing context),每一个上下文都有独立的Window对象,而且相互之间互不干扰。每个标签页中运行的脚本通常并不知道其他标签页的存在,更不用说和其他标签页的Window对象进行交互操作或者操作其他文档内容了。如果Web浏览器不支持多标签页,或者把标签页关掉了,可能在某一时刻桌面上会有很多打开的Web浏览器窗口。而使用标签页,每个桌面窗口中的Window对象都是独立的,也就是说彼此就是完全独立的,和其他桌面窗口没有任何联系。

但是窗口并不总是和其他窗口完全没关系。一个窗口或标签页中的脚本可以打开新的窗口或标签页,当一个脚本这样做时,这样多个窗口或窗口与另一个窗口的文档之间就可以互操作。

1. 打开和关闭窗口

使用Window对象的open()方法可以打开一个新的浏览器窗口(或标签页,这通常和浏览器的配置选项有关)。Window.open()载入指定的url到新的或已存在的窗口中,并返回代表那个窗口的Window对象。

它包含4个可选参数:

  • 第一个参数是要在新窗口中显示文档的url。如果这个参数省略了,那么会使用空白页面的url about:blank

  • 第二个参数是新打开的窗口的名字。如果指定的是一个已经存在的窗口的名字,会直接使用已存在的窗口。否则,会打开新的窗口,并将这个指定的名字赋值给它。如果省略次参数,会使用指定的名字“_blank”开打一个新的、未命名的窗口。

    窗口的名字是非常重要的,因为它允许open()方法引用已存在的窗口,并同时可以作为<a><form>元素上HTML target属性的值,用来表示引用的文档(或表单提交结果)应该显示在命名的窗口中。这个target属性的值可以设置为“_blank”、“_parent”或“_top”,从而使引用的文档显示在新的空白窗口、父窗口/窗体或顶层窗口中。

  • 第三个参数是一个以逗号分隔的列表,包含大小和各种属性,用以表明新窗口是如何打开的。如果省略这个参数,那么新窗口就会用一个默认的大小,而且带有一整组标准的UI组件,即菜单栏、状态栏、工具栏等。在标签式浏览器中,会创建一个新的标签。这个参数是非标准的,H5规范也主张浏览器忽略它。注意:当指定第三个参数时,所有没有显式指定的功能都会忽略。出于各种安全原因,浏览器包含对可能指定的功能的限制。

1
var w = window.open('smallwin.html', 'smallwin', 'width=400,height=350,status=yes,resizable:yes');
  • 第四个参数只在第二个参数命名是一个存在的窗口时才有用。它是一个布尔值,声明了由第一个参数指定的url是应用替换掉窗口浏览历史的当前条目(true),还是应该在窗口浏览历史中创建一个新的条目(false),后者是默认的设置。

就像方法open()打开一个新窗口一样,方法close()将关闭一个窗口。如果已经创建了Window对象,可以使用如下的代码将它关掉。

1
win.close();

运行在那个窗口中的JavaScript代码则可以使用下面的代码关闭

1
window.close();

注意:要显式地使用标识符window,这样可以避免混淆Window对象的close()方法和Document对象的close()方法——如果正在从事件处理程序调用close(),这很重要。

即使一个窗口关闭了,代表它的Window对象仍然存在。已关闭的窗口会有个值为true的closed属性,它的document会是null,它的方法通常也不会再工作。

2. 窗体之间的关系

Window对象的方法open()返回代表新创建的窗口的Window对象。而且这个新窗口具有opener属性,该属性可以打开它的原始窗口。这样,两个窗口就可以相互引用,彼此都可以读取对方的属性或是调用对方的方法。窗体也是这样的,窗口或窗体中运行的代码都可以通过下面介绍的属性引用到自己的窗口或窗体,以及嵌套的子窗体。

任何窗口中的JavaScript代码都可以将自己的窗口引用为window或self。窗体可以用parent属性引用包含它的窗口或窗体的Window对象

1
parent.history.back();

如果一个窗口是顶级窗口或标签,而不是窗体,那么其parent属性引用的就是这个窗口本身

1
2
// 只有顶级窗口才会返回true
parent == self;

如果一个窗体包含在另一个窗体中,而后者又包含在顶级窗口中,那么该窗体就可以使用parent.parent来引用顶级窗口。top属性是一个通用的快捷方式,无论一个窗体被嵌套了几层,它的top属性引用的都是指向包含它的顶级窗口。如果一个Window对象代表的是一个顶级窗口,那么它的top属性引用的就是窗口本身。对于那些顶级窗口的直接子窗体,top属性就等价于parent属性。

parent和top属性允许脚本引用它的窗体的祖先。有不止一种方法可以引用窗口或窗体的子孙窗体。窗体是通过<iframe>元素创建的。可以用获取其他元素的方法来获取一个表示<iframe>的元素对象。假定文档里有<iframe id="f1">。那么,表示该iframe的元素对象就是

1
var iframeElement = document.getElementById('f1');

<iframe>元素有contentWindow属性,引用该窗体的Window对象,所以此窗体的Window对象就是

1
var childFrame = document.getElementById('f1').contentWindow;

可以进行反向操作——从表示窗体的Window对象来获取该窗体的<iframe>元素——用Window对象的frameElement属性。表示顶级窗口的Window对象的frameElement属性为null,窗体中的Window对象的frameElement属性不是null

1
2
3
4
var elt = document.getElementById('f1');
var win = elt.contentWindow;
win.frameElement == elt; // true
window.frameElement == null; // true

尽管如此,通常不需要使用getElementById()方法和contentWindow属性来获取窗口中子窗体的引用。每个Window对象都有一个frames属性,它引用自身包含的窗口或窗体的子窗体。frames属性引用的是类数组对象,并可以通过数字或窗体名进行索引。要引用窗口的第一个子窗体,可以用frames[0]。要引用第二个子窗体的第三个子窗体,可以用frames[1].frames[2]。窗体里创建的代码可以用parent.frames[1]引用兄弟窗体。注意frames[]数组里的元素是Window对象,而不是<iframe>元素。

如果指定<iframe>元素的name或id属性,那么除了用数字进行索引之外,还可以用名字来进行索引。例如,名字为“f1”的帧应该用frames['f1']frames.f1

前面讲过,<iframe>以及其他元素的name和id都可以自动通过Window对象的属性来引用,而<iframe>元素和其他的元素有所不同:对于窗体来说,通过Window对象的属性引用的<iframe>是指窗体中的Window对象,而不是元素对象。也就是说,可以通过窗体的名字“f1”来代替frame.f1。实际上,H5规范指出frames属性是一个自引用(self-referential)的属性,就像window和self一样。而这个Window对象看起来像一个由窗体组成的数组。也就是说可以通过window[0]来获取第一个子窗体的引用,可以通过window.lengthlength查询窗体的编号。但是这里我们使用frames来代替window会比较清晰一些。

3. 交互窗口中的JavaScript

每个窗口和窗体都是它自身的JavaScript的执行上下文,以Window作为全局对象。但是如果一个窗口或窗体中的代码可以应用到其他窗口或窗体(并且同源策略没有阻止它),那么一个窗口或窗体的脚本就可以和其他窗口或窗体中的脚本进行交互。

设想一个Web页面里有两个<iframe>元素,分别叫“A”和“B”,并假设这些床体所包含的文档来自于相同的一个服务器,并且包含交互脚本。窗体A里的脚本定义了一个变量i

1
var i = 3;

这个变量只是全局对象的一个属性,也是Window对象的一个属性。窗体A中的代码可以用标识符i来引用变量,或者用window对象显式地引用这个变量

1
window.i;

由于窗体B中的脚本可以引用窗体A中的Window对象,因此它也可以引用那个Window对象的属性:

1
parent.A.i = 4;

同样的,我们可以在窗体B中定义一个函数,在窗体A中进行调用

1
2
3
parent.B.f();

var f = parent.B.f;

当采用这种方式在窗体或窗口间共享函数时,牢记词法作用域的规则非常重要。函数在定义它的作用域中执行,而不是调用它的作用域中执行。就这个例子来说,如果函数f引用了全局变量,那么将在窗体B的属性中查找这些变量,即使函数是由窗体A调用的。

要记住构造函数也是函数,所以当用构造函数和相关的额原型对象定义一个类时,那个类只在一个单独的窗口中定义。假设一个窗口包含窗体A和窗体B,并且包含Set类。

顶级窗口的脚本可以创建新的Set对象,类似这样

1
var s = new Set();

相反,每个床体中的代码必须显式地用父级窗口的属性类引用Set()构造函数

1
var s = new parent.Set();

另外,每个窗体中的代码还可以定义自己的变量来引用构造函数,这样就更方便了

1
2
var Set = top.Set();
var s = new Set();

和用户定义的类不同,内置的类(String、Date和RegExp等)都会在所有的窗口中自动预定义。但是要注意,每个窗口都有构造函数的一个独立副本和构造函数对应原型对象的一个独立副本。例如,每个窗口都有自己的String()构造函数和String.prototype对象的副本。因此,如果编写一个操作JavaScript字符串的新方法,并且通过把它赋值给当前窗口中的String.prototype对象而使它称为String类的一个方法,那么该窗口中的所有字符串就都可以使用这个新方法。但是,别的窗口中定义的字符串不能使用这个新方法。

事实上,每个Window都有自己的原型对象,这意味着instanceof操作符不能跨窗口工作。


九、 WindowProxy对象

我们已经讲过很多次,Window对象是客户端JavaScript的全局变量。但是从技术上来看,并不是这样的。Web浏览器每次向窗口或窗体中载入新的内容,它都会开始一个新的JavaScript执行上下文,包含一个新创建的全局对象。但是当多个窗口或窗体在使用时,有一个重要的概念,尽管窗体或窗口载入了新的文档,但是引用窗体或窗口的Window对象还仍然是一个有效的引用。

所以客户端JavaScript有两个重要的对象。客户端全局对象处于作用域链的顶级,并且是全局变量和函数定义的地方。事实上,全局对象会在窗口或窗体载入新内容时被替换。我们称为“Window对象”的对象实际上不是全局对象,而是全局对象的一个代理。每当查询或设置Window对象的属性时,就会在窗口或窗体的当前全局对象上查询或设置相同的属性。H5规范称这个代理对象为WindowProxy,但本文中我们继续使用名次Window对象。

由于它的代理行为,除了有更长的生命周期之外,代理对象表现得像真正的全局对象。如果可以比较两个对象,那么区分它们会很困难。但事实上,没有办法可以引用到真正地客户端全局对象。全局对象处于作用域链的顶端,但是windowselftopparent以及窗体的属性全部返回代理对象。window.open()方法也返回代理对象,甚至顶级函数里this关键字的值都是代理对象,而不是真正的全局对象。


参考文献

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