# BOM
BOM ( Browser Object Model ) 是指浏览器对象模型, BOM 提供了与网页内容无关的,可以与浏览器窗口进行互动的对象结构。
# window
对象
window 是 BOM 的核心对象, 表示浏览器的一个实例. 在浏览器中,window 对象有双重角色,它既是通过 JavaScript 访问浏览器窗口的一个接口,又是 ECMAScript 规定的 Global 对象。
在标签浏览器(比如 Firefox)中,每个标签具有自己的 window 对象, 同一个窗口的标签之间不会共享一个 window 对象. 有一些方法,如 window.resizeTo 和 window.resizeBy 之类的方法会作用于整个窗口而不是 window 对象所属的那个标签。一般而言,如果无法恰当地作用于标签,则会将其作用于窗口。
# 窗口关系
window.top
对象始终指向最上层(最外层)窗口。window.parent
对象则始终指向当前窗口的父窗口。如果当前窗口是最上层窗口,则parent
等于top
window.self
对象,始终会指向window
window.top === window; // true
window.parent === window.top; // true
window.self === window; // true
# 窗口位置 & 像素比
在 Firefox 中 screenX
和 screenY
属性分别用于表示窗口相对于屏幕左边和上边的距离。
IE、Safari、Opera 和 Chrome 也都提供了 screenLeft
和 screenTop
属性来表示相同的位置信息。
跨浏览器取得距离值:
var leftPos =
typeof window.screenLeft == "number" ? window.screenLeft : window.screenX;
var topPos =
typeof window.screenTop == "number" ? window.screenTop : window.screenY;
可以使用 moveTo()
和 moveBy()
方法移动窗口 ( 依浏览器而定,以上方法可能会被部分或全部禁用 ) 这两个方法都接收两个参数:
moveTo()
接收要移动到的新位置的绝对坐标x
和y
。moveBy()
则接收相对当前位置在两个方向上移动的像素数。
// 把窗口移动到左上角
window.moveTo(0, 0);
// 把窗口向下移动100像素
window.moveBy(0, 100);
// 把窗口移动到坐标位置(200, 300)
window.moveTo(200, 300);
// 把窗口向左移动50像素
window.moveBy(-50, 0);
『 CSS 像素 』是 Web 开发中使用的统一像素单位,是一个逻辑单位,大小固定为 1/96 英寸。这样在不同设备上,1 个 CSS 像素的大小都是一样的。
- 🌰 低分辨率平板设备上 12 CSS 像素的文字,应该与高清 4K 屏幕下 12 CSS 像素的文字具有相同大小。
但是不同设备具有不同的物理像素数量,即屏幕实际的分辨率。所以需要把物理像素数转换为转换为 CSS 像素数。不同像素密度的屏幕下就会有不同的「 缩放系数 」
物理像素与 CSS 像素之间的转换比率由 window.devicePixelRatio
属性提供。
- 🌰 如果
window.devicePixelRatio
的值是3
,则 12 CSS 像素的文字,实际上就会用 36 像素的物理像素来显示。
# 窗口大小
提供了 4 个属性确定一个窗口的大小:
innerWidth
: 浏览器视口( viewport )宽度, 包括垂直滚动条innerHeight
: 浏览器窗口的视口( viewport )高度,包括水平滚动条。outerWidth
: 整个浏览器窗口的宽度。outerHeight
: 整个浏览器窗口的高度。
可以使用 resizeTo()
和 resizeBy()
方法调整窗口大小 ( 可能被浏览器禁用 )
resizeTo()
接收新的宽度和高度值。resizeBy()
接收宽度和高度各要缩放多少。
// 缩放到100×100
window.resizeTo(100, 100);
// 缩放到200×150
window.resizeBy(100, 50);
// 缩放到300×300
window.resizeTo(300, 300);
# 视口位置
浏览器窗口尺寸通常无法满足完整显示整个页面,为此用户可以通过滚动在有限的视口中查看文档。
window.scrollX
页面水平方向滚动的像素值。等同于window.pageXoffset
。window.scrollY
页面垂直方向滚动的像素值。等同于window.pageYoffset
。
可以使用 scroll()
、scrollTo()
和 scrollBy()
方法滚动页面。这 3 个方法都接收表示相对视口距离的 x
和 y
坐标,这两个参数在前两个方法中表示要滚动到的坐标,在最后一个方法中表示滚动的距离。
// 相对于当前视口向下滚动100像素
window.scrollBy(0, 100);
// 相对于当前视口向右滚动40像素
window.scrollBy(40, 0);
// 滚动到页面左上角
window.scroll(0, 0);
// 滚动到距离屏幕左边及顶边各100像素的位置
window.scrollTo(100, 100);
# 导航 & 打开新窗口
window.open()
方法可以用于导航到指定 URL,也可以用于打开新浏览器窗口。接收 4 个参数:
- 目标 URL。
- 目标窗口名,如果是一个已经存在的窗口或 Frame 的名字,则会在对应的窗口或 Frame 中打开 URL。否则就会打开一个新窗口并将其命名为指定值。除此之外,也可以是一个特殊的窗口名。
- 特性字符串,用于指定新窗口的配置。是一个逗号分隔的设置字符串,用于指定新窗口包含的特性。
- 如果打开的不是新窗口,则忽略第三个参数。
- 如果没有传第三个参数,则新窗口会带有所有默认的浏览器特性。
- 布尔值,表示新窗口在浏览器历史记录中,是否替代当前加载页面。
第二个参数「 目标窗口名 」的取值可以是一些特殊的窗口名:
_self
: 当前页面。_blank
: 新窗口打开。_parent
: 当前窗口的父窗口。如果没有parent
窗口,此选项与_self
相同。_top
: 最外层窗口,如果当前窗口就是最上层,此选项与_self
相同。
第三个参数「 特性字符串 」,用于指定新窗口的配置。下表列出了一些选项:
window.open(
"http://www.wrox.com/",
"wroxWindow",
"height=400,width=400,top=10,left=10,resizable=yes"
);
- 这行代码会打开一个可缩放的新窗口,大小为 400 像素 ×400 像素,位于离屏幕左边及顶边各 10 像素的位置。
window.open()
方法返回一个对新建窗口的引用。与普通 window
对象没有区别。
可以使用 close()
方法关闭新打开的窗口。
新创建窗口的 window
对象有一个属性 opener
,指向打开它的窗口。
let wroxWin = window.open(
"http://www.wrox.com/",
"wroxWindow",
"height=400,width=400,top=10,left=10,resizable=yes"
);
wroxWin.close();
alert(wroxWin.opener === window); // true
# 弹窗屏蔽程序
如果浏览器内置的弹窗屏蔽程序阻止了弹窗,那么 window.open()
很可能会返回 null
。此时,只要检查这个方法的返回值就可以知道弹窗是否被屏蔽了。
除此之外,弹窗也可能被浏览器扩展或其他程序屏蔽,此时 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!");
}
# 定时器
setTimeout()
指定在一段时间后执行某些代码。setInterval()
指定每隔一段时间执行某些代码。
setTimeout()
方法接收两个参数:
- 要执行的代码
- 等待的时间(毫秒)告诉 JavaScript 引擎在指定的毫秒数过后把任务添加到宏任务队列。
调用 setTimeout()
时,会返回一个表示该超时排期任务的数值 ID。调用 clearTimeout()
方法并传入超时 ID 可以取消等待中的排期任务。
- 只要是在指定时间到达之前调用
clearTimeout()
,就可以取消超时任务。在任务执行后再调用clearTimeout()
没有效果。
// 设置超时任务
let timeoutId = setTimeout(() => alert("Hello world!"), 1000);
// 取消超时任务
clearTimeout(timeoutId);
setInterval()
与 setTimeout()
的使用方法类似。但要注意。第二个参数,指的是每隔指定时间,就会向队列再添加一次任务。不管上一个任务是否执行完毕,以及执行要花多长时间。
setInterval()
方法也会返回一个循环定时任务 ID,可以调用 clearInterval()
并传入定时 ID,来取消循环定时。如果一直不管它,那么定时任务会一直执行,直到关闭页面。
let num = 0,
intervalId = null;
let max = 10;
let incrementNumber = function() {
num++;
// 如果达到最大值,则取消所有未执行的任务
if (num == max) {
clearInterval(intervalId);
alert("Done");
}
};
intervalId = setInterval(incrementNumber, 500);
👆 上面这个模式也可以使用 setTimeout()
来实现:
let num = 0;
let max = 10;
let incrementNumber = function() {
num++;
// 如果还没有达到最大值,再设置一个超时任务
if (num < max) {
setTimeout(incrementNumber, 500);
} else {
alert("Done");
}
};
setTimeout(incrementNumber, 500);
上面这种写法,不需要手动取消循环定时任务。而且可以保证上一次任务执行完毕之后,才会开始执行下一次任务。
# 系统对话框
使用 alert()
、confirm()
和 promt()
方法,可以让浏览器调用系统对话框向用户显示消息。
- 它们的外观由操作系统或者浏览器决定,无法使用 CSS 设置。
- 都是同步的模态对话框,即在它们显示的时候,代码会停止执行。
通过 alert()
显示「 警示框 」,只有一个 OK 按钮。通常用于向用户显示一些他们无法控制的消息,比如报错。
- 方法只接收一个参数。传入字符串会显示在弹框中。如果不是一个原始字符串,则会调用这个值的
toString()
方法将其转换为字符串。
通过调用 confirm()
来显示「 确认框 」。有两个按钮:Cancel 和 OK。用户通过单击不同的按钮表明希望接下来执行什么操作。
- 方法的返回值,
true
表示单击了 OK 按钮,false
表示单击了 Cancel 按钮,或者单击 X 图标关闭了确认框。
if (confirm("Are you sure?")) {
alert("I'm so glad you're sure!");
} else {
alert("I'm sorry to hear you're not sure.");
}
通过调用 prompt()
方法显示「 提示框 」。用途是提示用户输入消息。除了 OK 和 Cancel 按钮,提示框还会显示一个文本框,让用户输入内容。
- 方法接收两个参数:要显示给用户的文本,和文本框的默认值。
- 如果用户单击了 OK 按钮,则
prompt()
会返回文本框中的值。如果用户单击了 Cancel 按钮,或者对话框被关闭,则prompt()
会返回null
。
let result = prompt("What is your name? ", "");
if (result !== null) {
alert("Welcome, " + result);
}
# location
对象
location
是最有用的 BOM 对象之一,提供了当前窗口中加载文档的信息,以及通常的导航功能。
它既是 window
的属性,也是 document
的属性。也就是说,window.location
和 document.location
指向同一个对象。
# 常用属性
location 对象上保存着当前窗口所加载的文档的 URL 地址信息:
假设浏览器当前加载的 URL 是 http://foouser:barpassword@www.wrox.com:80/WileyCDA/?q=javascript#contents
属性 | 值 | 说明 |
---|---|---|
location.hash | "#contents" | URL 散列值,如果没有则为空字符串 |
location.host | "www.wrox.com:80" | 服务器名及端口号 |
location.hostname | "www.wrox.com" | 服务器名 |
location.href | "http://www.wrox.com:80/WileyCDA/?q=javascript#contents" | 当前加载页面的完整 URL。location 的 toString() 方法返回这个值 |
location.pathname | "/WileyCDA/" | URL 中的路径和(或)文件名 |
location.port | "80" | 请求的端口。如果 URL 中没有端口,则返回空字符串 |
location.protocol | "http:" | 页面使用的协议。通常是 "http:" 或 "https:" |
location.search | "?q=javascript" | URL 的查询字符串。这个字符串以问号开头 |
location.username | "foouser" | 域名前指定的用户名 |
location.password | "barpassword" | 域名前指定的密码 |
location.origin | "http://www.wrox.com" | URL 的源地址。只读 |
# 解析 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("="))) {
// 因为查询字符串通常是被编码后的格式
// 所以需要使用 decodeURIComponent() 解码
let name = decodeURIComponent(item[0]),
value = decodeURIComponent(item[1]);
if (name.length) {
args[name] = value;
}
}
return args;
};
# 操作地址
通过给 location.assign()
方法传入一个 URL 字符串,可以加载给定 URL 的内容资源到当前窗口。
执行下面这行代码,会立即导航到 URL 指定的页面上。同时在浏览器历史记录中增加一条记录。
location.assign("https://www.baidu.com");
如果给 location.href
或 window.location
设置一个 URL,也会有同样的效果。
location.assign("https://www.baidu.com");
// 等价于
window.location = "https://www.baidu.com";
location.href = "https://www.baidu.com";
修改 location
对象的属性也会修改当前加载的页面。
- 除了
hash
之外,只要修改location
的一个属性,就会导致页面重新加载新 URL。 - 修改
hash
的值会在浏览器历史中增加一条新记录。可以前进后退。
// 假设当前 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;
使用 replace()
方法可以让当前窗口重新加载到指定的 URL,且不会增加历史记录。也就是说,用户不能 "后退" 到前一个页面了。
location.replace("https://www.baidu.com/");
reload()
能重新加载当前显示的页面。
- 如果不传参数,页面会以最有效的方式重新加载。会尽可能从缓存中加载页面。
- 如果想强制从服务器重新加载,则将
true
作为参数传入。
location.reload(); // 重新加载,可能是从缓存加载
location.reload(true); // 重新加载,从服务器加载
# screen
对象
window.screen
对象提供显示器相关信息。
属性 | 说明 |
---|---|
availHeight | 屏幕像素高度减去系统组件高度(只读) |
availLeft | 没有被系统组件占用的屏幕的最左侧像素(只读) |
availTop | 没有被系统组件占用的屏幕的最顶端像素(只读) |
availWidth | 屏幕像素宽度减去系统组件宽度(只读) |
colorDepth | 表示屏幕颜色的位数;多数系统是 32(只读) |
height | 屏幕像素高度 |
left | 当前屏幕左边的像素距离 |
pixelDepth | 屏幕的位深(只读) |
top | 当前屏幕顶端的像素距离 |
width | 屏幕像素宽度 |
orientation | 返回 Screen Orientation API 中屏幕的朝向 |
# history
对象
window.history
对象表示当前窗口使用以来用户的导航历史记录。于安全考虑,这个对象不会暴露用户访问过的 URL,但是可以控制页面的前进和后退。
go()
方法可以在用户历史记录中沿任何方向导航。只接收一个参数:
- 可以是整数,表示前进或后退多少步。
- 可以是字符串,浏览器会导航到历史中包含该字符串的第一个位置。可能前进,也可能是后退。
# 前进 & 后退
// 后退一页
history.go(-1);
// 前进一页
history.go(1);
// 前进两页
history.go(2);
// 导航到最近的 wrox.com 页面
history.go("wrox.com");
// 导航到最近的 nczonline.net 页面
history.go("nczonline.net");
back()
和 forward()
方法可以直接控制前进一步和后退一步。
// 后退一页
history.back();
// 前进一页
history.forward();
length
属性反映了历史记录的数量。对于窗口中加载的第一个页面,history.length
等于 1
。
if (history.length == 1) {
// 这是用户窗口中的第一个页面
}
# 历史状态管理
用户每次点击都会触发页面刷新的时代早已过去,单页面应用里的很多操作不会产生新的历史记录,而用户却习惯于用「 后退 」和「 前进 」来切换至自己上一步,或后一步操作。如何合理地进行历史记录管理,就变成了现代 Web 开发的一个难题。
hashchange
事件会在页面 URL 的 hash
变化时被触发,开发者可以在此时执行某些操作。具体细节我们在「 事件 」那一章再讲。
HTML5 为 history 对象增加了方便的状态管理特性。使用 history.pushState()
方法可以向当前浏览器会话的历史堆栈添加一个新纪录,并且该记录与一个状态对象关联。
方法接收 3 个参数:
state
对象:与新历史记录相关联,可以是任何可以序列化的对象。应该包含正确初始化页面所必需的状态信息。- 新状态的标题:当前大多数浏览器都忽略此参数,尽管将来可能会使用它。可以传一个空字符串,也可以传一个短标题。
- 相对 URL( 可选的 ):浏览器不会加载此 URL。 如果未指定此参数,则将其设置为文档的当前 URL。
⚠️ 注意,要确保通过 pushState()
设置的每个 URL 都对应着服务器上一个真实的物理 URL。否则,单击「 刷新 」按钮会导致 404 错误。
let stateObject = { foo: "bar" };
history.pushState(stateObject, "My title", "baz.html");
当 pushState() 执行后,一个新的历史记录被创建。如果用户执行「 后退 」操作。就会触发 window
对象上的 popstate
事件。
事件处理函数接收一个 event
事件对象作为参数,事件对象有一个 state
属性,其中包含对应历史记录的 state
对象。如果历史记录并不是通过 pushState()
方法添加的,则 event.state
值为 null
。
基于这个状态,你可以把页面重置为对应的状态。
window.addEventListener("popstate", (event) => {
let state = event.state;
if (state) {
processState(state); // 根据该状态,处理页面。
}
});
具体操作展示:
window.addEventListener("popstate", (event) => {
console.log(event.state);
});
history.pushState({ number: 1 }, "state_1");
history.pushState({ number: 2 }, "state_2");
history.pushState({ number: 3 }, "state_3");
上述代码执行完毕后,三条历史记录被添加进浏览器。页面此时的状态为 { number: 3 }
。也就是说,用户最多可以「 后退 」三次:
- 第 1 次后退,打印状态
{number: 2}
- 第 2 次后退,打印状态
{number: 1}
- 第 3 次后退,打印状态
null
也就是最开始的状态。
可以使用 history.state
获取当前的状态对象。
console.log(history.state); // { number: 1 }
可以使用 replaceState()
来更新当前记录的状态。更新状态不会创建新历史记录,只会覆盖当前状态。方法接收与 pushState()
同样的前两个参数。
- 继续拿上面 👆 的代码举例,假设我现在的页面状态为
{number: 2}
。然后我执行下面 👇 这条语句。
history.replaceState({ number: 222 }, "state_2");
- 之后,我点击「 前进 」按钮,页面状态变为
{number: 3}
,然后我再点击「 后退 」,此时页面状态变为{number: 222}