Skip to main content

[JS] BOM

摘记<Javascript高级程序设计> -- 马特 2020版 第十二章

BOM

虽然 ECMAScript 把浏览器对象模型(BOM,Browser Object Model)描述为 JavaScript 的核心,但实际上 BOM 是使用 JavaScript 开发 Web 应用程序的核心。BOM 提供了与网页无关的浏览器功能对象. 多年来,BOM 是在缺乏规范的背景下发展起来的,因此既充满乐趣又问题多多。毕竟,浏览器开发商都按照自己的意愿来为它添砖加瓦。最终,浏览器实现之间共通的部分成为了事实标准,为 Web 开发提供了浏览器间互操作的基础。HTML5 规范中有一部分涵盖了 BOM 的主要内容,因为 W3C 希望将JavaScript 在浏览器中最基础的部分标准化。

window 对象

BOM 的核心是 window 对象,表示浏览器的实例。window 对象在浏览器中有两重身份,

  • 一个是ECMAScript 中的 Global 对象
  • 另一个就是浏览器窗口的 JavaScript 接口

这意味着网页中定义的所有对象、变量和函数都以 window 作为其 Global 对象,都可以访问其上定义的全局方法。

Global 作用域

因为 window 对象被复用为 ECMAScript 的 Global 对象,所以通过 var 声明的所有全局变量和函数都会变成 window 对象的属性和方法。比如:

var age = 29;
var sayAge = () => alert(this.age);
alert(window.age); // 29
sayAge(); // 29
window.sayAge(); // 29

这里,变量 age 和函数 sayAge()被定义在全局作用域中,它们自动成为了 window 对象的成员。因此,变量 age 可以通过 window.age 来访问,而函数 sayAge()也可以通过 window.sayAge()来访问。因为 sayAge()存在于全局作用域,this.age 映射到 window.age,所以就可以显示正确的结果了。

如果在这里使用 let 或 const 替代 var,则不会把变量添加给全局对象:

let age = 29;
const sayAge = () => alert(this.age);
alert(window.age); // undefined
sayAge(); // undefined
window.sayAge(); // TypeError: window.sayAge is not a function

另外,访问未声明的变量会抛出错误,但是可以在 window 对象上查询是否存在可能未声明的变量。比如:

// 这会导致抛出错误,因为 oldValue 没有声明
var newValue = oldValue;
// 这不会抛出错误,因为这里是属性查询
// newValue 会被设置为 undefined
var newValue = window.oldValue;

窗口关系

top 对象始终指向最上层(最外层)窗口,即浏览器窗口本身。而 parent 对象则始终指向当前窗口的父窗口。如果当前窗口是最上层窗口,则 parent 等于 top(都等于 window)。最上层的 window如果不是通过 window.open()打开的,那么其 name 属性就不会包含值

还有一个 self 对象,它是终极 window 属性,始终会指向 window。实际上,self 和 window 就是同一个对象。之所以还要暴露 self,就是为了和 top、parent 保持一致。

这些属性都是 window 对象的属性,因此访问 window.parentwindow.topwindow.self都可以。这意味着可以把访问多个窗口的 window 对象串联起来,比如 window.parent.parent。

窗口位置

window 对象的位置可以通过不同的属性和方法来确定。现代浏览器提供了 screenLeft 和screenTop 属性,用于表示窗口相对于屏幕左侧和顶部的位置 ,返回值的单位是 CSS 像素。

像素比

CSS 像素是 Web 开发中使用的统一像素单位。这个单位的背后其实是一个角度:0.0213°。如果屏幕距离人眼是一臂长,则以这个角度计算的 CSS 像素大小约为 1/96 英寸。这样定义像素大小是为了在不同设备上统一标准。

比如,低分辨率平板设备上 12 像素(CSS 像素)的文字应该与高清 4K 屏幕下12 像素(CSS 像素)的文字具有相同大小。这就带来了一个问题,不同像素密度的屏幕下就会有不同的缩放系数,以便把物理像素(屏幕实际的分辨率)转换为 CSS 像素(浏览器报告的虚拟分辨率)

举个例子,手机屏幕的物理分辨率可能是 1920×1080,但因为其像素可能非常小,所以浏览器就需要将其分辨率降为较低的逻辑分辨率,比如 640×320。这个物理像素与 CSS 像素之间的转换比率由window.devicePixelRatio 属性提供。对于分辨率从 1920×1080 转换为 640×320 的设备,window.devicePixelRatio 的值就是 3。这样一来,12 像素(CSS 像素)的文字实际上就会用 36 像素的物理像素来显示。window.devicePixelRatio 实际上与每英寸像素数(DPI,dots per inch)是对应的。DPI 表示单位像素密度,而 window.devicePixelRatio 表示物理像素与逻辑像素之间的缩放系数。

窗口大小

在不同浏览器中确定浏览器窗口大小没有想象中那么容易。所有现代浏览器都支持 4 个属性:

  • innerWidth
  • innerHeight
  • outerWidth
  • outerHeight

innerWidth和 innerHeight 返回浏览器窗口中页面视口的大小(不包含浏览器边框和工具栏)。

outerWidth 和 outerHeight 返回浏览器窗口自身的大小(不管是在最外层 window 上使用,还是在窗格<frame>中使用)。

导航与打开新窗口

window.open()方法可以用于导航到指定 URL,也可以用于打开新浏览器窗口

这个方法接收 4个参数:

  • 要加载的 URL
  • 目标窗口
  • 特性字符串
  • 表示新窗口在浏览器历史记录中是否替代当前加载页面的布尔值

通常,调用这个方法时只传前 3 个参数,最后一个参数只有在不打开新窗口时才会使用。

如果 window.open()的第二个参数是一个已经存在的窗口或窗格(frame)的名字,则会在对应的窗口或窗格中打开 URL。下面是一个例子:

// 与<a href="http://www.wrox.com" target="topFrame"/>相同
window.open("http://www.wrox.com/", "topFrame");

执行这行代码的结果就如同用户点击了一个 href 属性为"http://www.wrox.com",target 属性为"topFrame"的链接。如果有一个窗口名叫"topFrame",则这个窗口就会打开这个 URL;否则就会打开一个新窗口并将其命名为"topFrame"。第二个参数也可以是一个特殊的窗口名,比如_self、_parent、_top 或_blank。

1. 弹出窗口

如果 window.open()的第二个参数不是已有窗口,则会打开一个新窗口或标签页。第三个参数即特性字符串,用于指定新窗口的配置。如果没有传第三个参数,则新窗口(或标签页)会带有所有默认的浏览器特性(工具栏、地址栏、状态栏等都是默认配置)。如果打开的不是新窗口,则忽略第三个参数。特性字符串是一个逗号分隔的设置字符串,用于指定新窗口包含的特性。下表列出了一些选项。

windowopen windowopen2

这些设置需要以逗号分隔的名值对形式出现,其中名值对以等号连接。(特性字符串中不能包含空格。)来看下面的例子:

window.open("http://www.wrox.com/",
"wroxWindow",
"height=400,width=400,top=10,left=10,resizable=yes");

这行代码会打开一个可缩放的新窗口,大小为 400 像素×400 像素,位于离屏幕左边及顶边各 10 像素的位置。

window.open()方法返回一个对新建窗口的引用。这个对象与普通 window 对象没有区别,只是为控制新窗口提供了方便。例如,某些浏览器默认不允许缩放或移动主窗口,但可能允许缩放或移动通过window.open()创建的窗口。跟使用任何 window 对象一样,可以使用这个对象操纵新打开的窗口。

let wroxWin = window.open("http://www.wrox.com/",
"wroxWindow",
"height=400,width=400,top=10,left=10,resizable=yes");

// 缩放
wroxWin.resizeTo(500, 500);
// 移动
wroxWin.moveTo(100, 100);

还可以使用 close()方法像这样关闭新打开的窗口:

wroxWin.close();

这个方法只能用于 window.open()创建的弹出窗口。虽然不可能不经用户确认就关闭主窗口,但弹出窗口可以调用 top.close()来关闭自己。关闭窗口以后,窗口的引用虽然还在,但只能用于检查其 closed 属性了:

wroxWin.close();
alert(wroxWin.closed); // true

新创建窗口的 window 对象有一个属性 opener,指向打开它的窗口。这个属性只在弹出窗口的最上层 window 对象(top)有定义,是指向调用 window.open()打开它的窗口或窗格的指针。例如:

let wroxWin = window.open("http://www.wrox.com/",
"wroxWindow",
"height=400,width=400,top=10,left=10,resizable=yes");
alert(wroxWin.opener === window); // true

虽然新建窗口中有指向打开它的窗口的指针,但反之则不然。窗口不会跟踪记录自己打开的新窗口,因此开发者需要自己记录。

在某些浏览器中,每个标签页会运行在独立的进程中。如果一个标签页打开了另一个,而 window对象需要跟另一个标签页通信,那么标签便不能运行在独立的进程中。在这些浏览器中,可以将新打开的标签页的 opener 属性设置为 null,表示新打开的标签页可以运行在独立的进程中。比如:

let wroxWin = window.open("http://www.wrox.com/",
"wroxWindow",
"height=400,width=400,top=10,left=10,resizable=yes");
wroxWin.opener = null;

把 opener 设置为 null 表示新打开的标签页不需要与打开它的标签页通信,因此可以在独立进程中运行。这个连接一旦切断,就无法恢复了。

2. 安全限制

弹出窗口有段时间被在线广告用滥了。很多在线广告会把弹出窗口伪装成系统对话框,诱导用户点击。因为长得像系统对话框,所以用户很难分清这些弹窗的来源。为了让用户能够区分清楚,浏览器开始对弹窗施加限制。

IE 的早期版本实现针对弹窗的多重安全限制,包括不允许创建弹窗或把弹窗移出屏幕之外,以及不允许隐藏状态栏等。从 IE7 开始,地址栏也不能隐藏了,而且弹窗默认是不能移动或缩放的。Firefox 1禁用了隐藏状态栏的功能,因此无论 window.open()的特性字符串是什么,都不会隐藏弹窗的状态栏。

此外,浏览器会在用户操作下才允许创建弹窗。在网页加载过程中调用 window.open()没有效果而且还可能导致向用户显示错误。弹窗通常可能在鼠标点击或按下键盘中某个键的情况下才能打开。

3. 弹窗屏蔽程序

所有现代浏览器都内置了屏蔽弹窗的程序,因此大多数意料之外的弹窗都会被屏蔽。在浏览器屏蔽弹窗时,可能会发生一些事。如果浏览器内置的弹窗屏蔽程序阻止了弹窗,那么 window.open()很可能会返回 null。此时,只要检查这个方法的返回值就可以知道弹窗是否被屏蔽了,比如:

let wroxWin = window.open("http://www.wrox.com", "_blank");
if (wroxWin == null){
alert("The popup was blocked!");
}

在浏览器扩展或其他程序屏蔽弹窗时,window.open()通常会抛出错误。因此要准确检测弹窗是否被屏蔽,除了检测 window.open()的返回值,还要把它用 try/catch 包装起来,像这样:

let blocked = false;
try {
let wroxWin = window.open("http://www.wrox.com", "_blank");
if (wroxWin == null){
blocked = true;
}
} catch (ex){
blocked = true;
}
if (blocked){
alert("The popup was blocked!");
}

无论弹窗是用什么方法屏蔽的,以上代码都可以准确判断调用 window.open()的弹窗是否被屏蔽了

定时器

setTimeout()方法通常接收两个参数:一个参数可以是包含 JavaScript 代码的字符串或者一个函数,第二个参数是要等待的毫秒数,而不是要执行代码的确切时间。该参数只是告诉 JavaScript 引擎在指定的毫秒数过后把任务添加到这个队列。如果队列是空的,则会立即执行该代码。如果队列不是空的,则代码必须等待前面的任务执行完才能执行。

调用 setTimeout()时,会返回一个表示该超时排期的数值 ID。这个超时 ID 是被排期执行代码的唯一标识符,可用于取消该任务。要取消等待中的排期任务,可以调用 clearTimeout()方法并传入超时 ID,如下面的例子所示:

// 设置超时任务
let timeoutId = setTimeout(() => alert("Hello world!"), 1000);
// 取消超时任务
clearTimeout(timeoutId);

只要是在指定时间到达之前调用 clearTimeout(),就可以取消超时任务。在任务执行后再调用clearTimeout()没有效果。

info

注意 所有超时执行的代码(函数)都会在全局作用域中的一个匿名函数中运行,因此函 数中的 this 值在非严格模式下始终指向 window,而在严格模式下是 undefined。如果 给 setTimeout()提供了一个箭头函数,那么 this 会保留为定义它时所在的词汇作用域。

setInterval()与 setTimeout()的使用方法类似,只不过指定的任务会每隔指定时间就执行一次,直到取消循环定时或者页面卸载。

info

注意 这里的关键点是,第二个参数,也就是间隔时间,指的是向队列添加新任务之前等 待的时间。比如,调用 setInterval()的时间为 01:00:00,间隔时间为 3000 毫秒。这意 味着 01:00:03 时,浏览器会把任务添加到执行队列。浏览器不关心这个任务什么时候执行 或者执行要花多长时间。因此,到了 01:00:06,它会再向队列中添加一个任务。由此可看 出,执行时间短、非阻塞的回调函数比较适合 setInterval()。

setInterval()方法也会返回一个循环定时 ID,可以用于在未来某个时间点上取消循环定时。相对于 setTimeout()而言,取消定时的能力对 setInterval()更加重要。毕竟,如果一直不管它,那么定时任务会一直执行到页面卸载。下面是一个常见的例子:

let num = 0, intervalId = null;
let max = 10;
let incrementNumber = function() {
num++;
// 如果达到最大值,则取消所有未执行的任务
if (num == max) {
clearInterval(intervalId);
alert("Done");
}
}
intervalId = setInterval(incrementNumber, 500);

在这个例子中,变量 num 会每半秒递增一次,直至达到最大限制值。此时循环定时会被取消。这个模式也可以使用 setTimeout()来实现,比如:

let num = 0;
let max = 10;
let incrementNumber = function() {
num++;
// 如果还没有达到最大值,再设置一个超时任务
if (num < max) {
setTimeout(incrementNumber, 500);
} else {
alert("Done");
}
}

注意在使用 setTimeout()时,不一定要记录超时 ID,因为它会在条件满足时自动停止,否则会自动设置另一个超时任务。这个模式是设置循环任务的推荐做法。setIntervale()在实践中很少会在生产环境下使用,因为一个任务结束和下一个任务开始之间的时间间隔是无法保证的,有些循环定时任务可能会因此而被跳过。而像前面这个例子中一样使用 setTimeout()则能确保不会出现这种情况。一般来说,最好不要使用 setInterval()。


location 对象

location 是最有用的 BOM 对象之一,提供了当前窗口中加载文档的信息,以及通常的导航功能。这个对象独特的地方在于,它既是 window 的属性,也是 document 的属性。也就是说,window.location 和 document.location 指向同一个对象。location 对象不仅保存着当前加载文档的信息,也保存着把 URL 解析为离散片段后能够通过属性访问的信息。这些解析后的属性在下表中有详细说明(location 前缀是必需的)。

假设浏览器当前加载的 URL 是 http://foouser:barpassword@www.wrox.com:80/WileyCDA/?q=javascript#contents,location 对象的内容如下表所示。

location

查询字符串

location 的多数信息都可以通过上面的属性获取。但是 URL 中的查询字符串并不容易使用。虽然location.search 返回了从问号开始直到 URL 末尾的所有内容,但没有办法逐个访问每个查询参数。下面的函数解析了查询字符串,并返回一个以每个查询参数为属性的对象:

let getQueryStringArgs = function() {
// 取得没有开头问号的查询字符串
let qs = (location.search.length > 0 ? location.search.substring(1) : ""),
// 保存数据的对象
args = {};

// 把每个参数添加到 args 对象
for (let item of qs.split("&").map(kv => kv.split("="))) {
let name = decodeURIComponent(item[0]),
value = decodeURIComponent(item[1]);
if (name.length) {
args[name] = value;
}
}
return args;
}

这个函数首先删除了查询字符串开头的问号,当然前提是 location.search 必须有内容。解析后的参数将被保存到 args 对象,这个对象以字面量形式创建。接着,先把查询字符串按照&分割成数组,每个元素的形式为 name=value。for 循环迭代这个数组,将每一个元素按照=分割成数组,这个数组第一项是参数名,第二项是参数值。参数名和参数值在使用 decodeURIComponent()解码后(这是因为查询字符串通常是被编码后的格式)分别保存在 name 和 value 变量中。最后,name 作为属性而 value作为该属性的值被添加到 args 对象。这个函数可以像下面这样使用:

// 假设查询字符串为?q=javascript&num=10
let args = getQueryStringArgs();
alert(args["q"]); // "javascript"
alert(args["num"]); // "10"

现在,查询字符串中的每个参数都是返回对象的一个属性,这样使用起来就方便了。

URLSearchParams

URLSearchParams 提供了一组标准 API 方法,通过它们可以检查和修改查询字符串。给URLSearchParams 构造函数传入一个查询字符串,就可以创建一个实例。这个实例上暴露了 get()、set()和 delete()等方法,可以对查询字符串执行相应操作。下面来看一个例子:

let qs = "?q=javascript&num=10";
let searchParams = new URLSearchParams(qs);
alert(searchParams.toString()); // " q=javascript&num=10"
searchParams.has("num"); // true
searchParams.get("num"); // 10
searchParams.set("page", "3");
alert(searchParams.toString()); // " q=javascript&num=10&page=3"
searchParams.delete("q");
alert(searchParams.toString()); // " num=10&page=3"

大多数支持 URLSearchParams 的浏览器也支持将 URLSearchParams 的实例用作可迭代对象:

let qs = "?q=javascript&num=10";
let searchParams = new URLSearchParams(qs);
for (let param of searchParams) {
console.log(param);
}
// ["q", "javascript"]
// ["num", "10"]

操作地址

可以通过修改 location 对象修改浏览器的地址。首先,最常见的是使用 assign()方法并传入一个 URL,如下所示:

location.assign("http://www.wrox.com");

这行代码会立即启动导航到新 URL 的操作,同时在浏览器历史记录中增加一条记录。如果给location.href 或 window.location 设置一个 URL,也会以同一个 URL 值调用 assign()方法。比如,下面两行代码都会执行与显式调用 assign()一样的操作:

window.location = "http://www.wrox.com";
location.href = "http://www.wrox.com";

在这 3 种修改浏览器地址的方法中,设置 location.href 是最常见的。

修改 location 对象的属性也会修改当前加载的页面。其中,hash、search、hostname、pathname和 port 属性被设置为新值之后都会修改当前 URL,如下面的例子所示:

// 假设当前 URL 为 http://www.wrox.com/WileyCDA/
// 把 URL 修改为 http://www.wrox.com/WileyCDA/#section1
location.hash = "#section1";
// 把 URL 修改为 http://www.wrox.com/WileyCDA/?q=javascript
location.search = "?q=javascript";
// 把 URL 修改为 http://www.somewhere.com/WileyCDA/
location.hostname = "www.somewhere.com";
// 把 URL 修改为 http://www.somewhere.com/mydir/
location.pathname = "mydir";
// 把 URL 修改为 http://www.somewhere.com:8080/WileyCDA/
location.port = 8080;

除了 hash 之外,只要修改 location 的一个属性,就会导致页面重新加载新 URL。

在以前面提到的方式修改 URL 之后,浏览器历史记录中就会增加相应的记录。当用户单击“后退”按钮时,就会导航到前一个页面。如果不希望增加历史记录,可以使用 replace()方法。这个方法接收一个 URL 参数,但重新加载后不会增加历史记录。调用 replace()之后,用户不能回到前一页。比如下面的例子:

<!DOCTYPE html>
<html>
<head>
<title>You won't be able to get back here</title>
</head>
<body>
<p>Enjoy this page for a second, because you won't be coming back here.</p>
<script>
setTimeout(() => location.replace("http://www.wrox.com/"), 1000);
</script>
</body>
</html>

浏览器加载这个页面 1 秒之后会重定向到 www.wrox.com。此时,“后退”按钮是禁用状态,即不能返回这个示例页面,除非手动输入完整的 URL。

最后一个修改地址的方法是 reload(),它能重新加载当前显示的页面。调用 reload()而不传参数,页面会以最有效的方式重新加载。也就是说,如果页面自上次请求以来没有修改过,浏览器可能会从缓存中加载页面。如果想强制从服务器重新加载,可以像下面这样给 reload()传个 true:

location.reload(); // 重新加载,可能是从缓存加载
location.reload(true); // 重新加载,从服务器加载

脚本中位于 reload()调用之后的代码可能执行也可能不执行,这取决于网络延迟和系统资源等因素。为此,最好把 reload()作为最后一行代码。


navigator 是由 Netscape Navigator 2 最早引入浏览器的,现在已经成为客户端标识浏览器的标准。只要浏览器启用 JavaScript,navigator 对象就一定存在。但是与其他 BOM 对象一样,每个浏览器都支持自己的属性。

navigator 对象实现了NavigatorID 、 NavigatorLanguage 、 NavigatorOnLine 、NavigatorContentUtils 、 NavigatorStorage 、 NavigatorStorageUtils 、 NavigatorConcurrentHardware、NavigatorPlugins 和 NavigatorUserMedia 接口定义的属性和方法。下表列出了这些接口定义的属性和方法:

navigator navigator2

navigator 对象的属性通常用于确定浏览器的类型。

检测插件

检测浏览器是否安装了某个插件是开发中常见的需求。除 IE10 及更低版本外的浏览器,都可以通过 plugins 数组来确定。这个数组中的每一项都包含如下属性。

  • name:插件名称。
  • description:插件介绍。
  • filename:插件的文件名。
  • length:由当前插件处理的 MIME 类型数量。

通常,name 属性包含识别插件所需的必要信息,尽管不是特别准确。检测插件就是遍历浏览器中可用的插件,并逐个比较插件的名称,如下所示:

// 插件检测,IE10 及更低版本无效
let hasPlugin = function(name) {
name = name.toLowerCase();
for (let plugin of window.navigator.plugins){
if (plugin.name.toLowerCase().indexOf(name) > -1){
return true;
}
}
return false;
}
// 检测 Flash
alert(hasPlugin("Flash"));
// 检测 QuickTime
alert(hasPlugin("QuickTime"));

screen 对象

window 的另一个属性 screen 对象,是为数不多的几个在编程中很少用的 JavaScript 对象。这个对象中保存的纯粹是客户端能力信息,也就是浏览器窗口外面的客户端显示器的信息,比如像素宽度和像素高度。每个浏览器都会在 screen 对象上暴露不同的属性。下表总结了这些属性。

screen


history 对象

history 对象表示当前窗口首次使用以来用户的导航历史记录。因为 history 是 window 的属性,所以每个 window 都有自己的 history 对象。出于安全考虑,这个对象不会暴露用户访问过的 URL,但可以通过它在不知道实际 URL 的情况下前进和后退。

导航

go()方法可以在用户历史记录中沿任何方向导航,可以前进也可以后退。这个方法只接收一个参数,这个参数可以是一个整数,表示前进或后退多少步。负值表示在历史记录中后退(类似点击浏览器的“后退”按钮),而正值表示在历史记录中前进(类似点击浏览器的“前进”按钮)。下面来看几个例子

// 后退一页
history.go(-1);
// 前进一页
history.go(1);
// 前进两页
history.go(2);

在旧版本的一些浏览器中,go()方法的参数也可以是一个字符串,这种情况下浏览器会导航到历史中包含该字符串的第一个位置。最接近的位置可能涉及后退,也可能涉及前进。如果历史记录中没有匹配的项,则这个方法什么也不做,如下所示:

// 导航到最近的 wrox.com 页面
history.go("wrox.com");
// 导航到最近的 nczonline.net 页面
history.go("nczonline.net");

go()有两个简写方法:back()和 forward()。顾名思义,这两个方法模拟了浏览器的后退按钮和前进按钮:

// 后退一页
history.back();
// 前进一页
history.forward();

history 对象还有一个 length 属性,表示历史记录中有多个条目。这个属性反映了历史记录的数量,包括可以前进和后退的页面。对于窗口或标签页中加载的第一个页面,history.length 等于 1。通过以下方法测试这个值,可以确定用户浏览器的起点是不是你的页面:

if (history.length == 1){
// 这是用户窗口中的第一个页面
}
tip

注意 如果页面 URL 发生变化,则会在历史记录中生成一个新条目。对于 2009 年以来发 布的主流浏览器,这包括改变 URL 的散列值(因此,把 location.hash 设置为一个新 值会在这些浏览器的历史记录中增加一条记录)。这个行为常被单页应用程序框架用来模 拟前进和后退,这样做是为了不会因导航而触发页面刷新。

历史状态管理

现代 Web 应用程序开发中最难的环节之一就是历史记录管理。用户每次点击都会触发页面刷新的时代早已过去,“后退”和“前进”按钮对用户来说就代表“帮我切换一个状态”的历史也就随之结束了。为解决这个问题,首先出现的是 hashchange 事件。HTML5 也为history 对象增加了方便的状态管理特性

hashchange 会在页面 URL 的散列变化时被触发,开发者可以在此时执行某些操作。而状态管理API 则可以让开发者改变浏览器 URL 而不会加载新页面。为此,可以使用 history.pushState()方法。这个方法接收 3 个参数:

  • 一个 state 对象
  • 一个新状态的标题
  • 一个(可选的)相对 URL。

例如:

let stateObject = {foo:"bar"};
history.pushState(stateObject, "My title", "baz.html");

pushState()方法执行后,状态信息就会被推到历史记录中,浏览器地址栏也会改变以反映新的相对 URL。除了这些变化之外,即使 location.href 返回的是地址栏中的内容,浏览器页不会向服务器发送请求。第二个参数并未被当前实现所使用,因此既可以传一个空字符串也可以传一个短标题。第一个参数应该包含正确初始化页面状态所必需的信息。为防止滥用,这个状态的对象大小是有限制的,通常在 500KB~1MB 以内。

因为 pushState()会创建新的历史记录,所以也会相应地启用“后退”按钮。此时单击“后退”按钮,就会触发 window 对象上的 popstate 事件。popstate 事件的事件对象有一个 state 属性,其中包含通过 pushState()第一个参数传入的 state 对象:

window.addEventListener("popstate", (event) => {
let state = event.state;
if (state) { // 第一个页面加载时状态是 null
processState(state);
}
});

基于这个状态,应该把页面重置为状态对象所表示的状态(因为浏览器不会自动为你做这些)。记住,页面初次加载时没有状态。因此点击“后退”按钮直到返回最初页面时,event.state 会为 null。

可以通过 history.state 获取当前的状态对象,也可以使用 replaceState()并传入与pushState()同样的前两个参数来更新状态。更新状态不会创建新历史记录,只会覆盖当前状态:

history.replaceState({newFoo: "newBar"}, "New title");

传给 pushState()和 replaceState()的 state 对象应该只包含可以被序列化的信息。因此DOM 元素之类并不适合放到状态对象里保存。

tip

注意 使用 HTML5 状态管理时,要确保通过 pushState()创建的每个“假”URL 背后 都对应着服务器上一个真实的物理 URL。否则,单击“刷新”按钮会导致 404 错误。所有 单页应用程序(SPA,Single Page Application)框架都必须通过服务器或客户端的某些配 置解决这个问题。

小结

浏览器对象模型(BOM,Browser Object Model)是以 window 对象为基础的,这个对象代表了浏览器窗口和页面可见的区域。window 对象也被复用为 ECMAScript 的 Global 对象,因此所有全局变量和函数都是它的属性,而且所有原生类型的构造函数和普通函数也都从一开始就存在于这个对象之上。本章讨论了 BOM 的以下内容。

  • 要引用其他 window 对象,可以使用几个不同的窗口指针。
  • 通过 location 对象可以以编程方式操纵浏览器的导航系统。通过设置这个对象上的属性,可以改变浏览器 URL 中的某一部分或全部。
  • 使用 replace()方法可以替换浏览器历史记录中当前显示的页面,并导航到新 URL。
  • navigator 对象提供关于浏览器的信息。提供的信息类型取决于浏览器,不过有些属性如userAgent 是所有浏览器都支持的。

BOM 中的另外两个对象也提供了一些功能。screen 对象中保存着客户端显示器的信息。这些信息通常用于评估浏览网站的设备信息。history 对象提供了操纵浏览器历史记录的能力,开发者可以确定历史记录中包含多少个条目,并以编程方式实现在历史记录中导航,而且也可以修改历史记录。