JavaScript学习笔记(10):BOM

window对象

在“基本引用类型”章节提到了ES中的Global对象,而在不同的ES实现中,Global对象的存在形式可能是不一样的,在浏览器中的Global对象即window对象,来作为浏览器窗口的JavaScript接口。

window对象被复用为ECMAScript的Global对象,所以浏览器环境中通过var声明的所有全局变量和函数都会变成window对象的属性和方法。

窗口与视口

窗口关系

一个页面可能会通过window.open()来打开多个子窗口,这样就会存在多个window对象。

在这些window对象中有以下属性:

  • top:始终指向最上层的窗口,即浏览器本身的窗口。
  • parent:当前窗口的父窗口。如果窗口是最上层窗口,那么其父窗口为浏览器窗口,即window.top,也即它本身。
  • name:窗口的名称。通过window.open()打开的窗口才会有。
  • self:指向当前window本身。

窗口位置

主要有两个属性:

  • screenLeft属性:用于表示窗口相对于屏幕左侧的位置,返回值的单位是CSS像素。
  • screenTop属性:用于表示窗口相对于屏幕顶部的位置,返回值的单位是CSS像素。

以及两个方法来移动浏览器窗口:

  • moveTo()方法:绝对移动,接收要移动到的新位置的绝对坐标x和y两个参数。
  • moveBy()方法:相对移动,接收相对当前位置在两个方向上移动的像素数。

依浏览器而定,这两个方法可能会被部分或全部禁用。并且它们一般无法操作顶级窗口,而只能用来操作通过window.open()打开的自定义窗口。

下方显示你的浏览器的窗口位置(数值仅为window.screenLeftwindow.screenTop的值):

你的浏览器窗口距离左侧: px
你的浏览器窗口距离顶部: px

窗口像素比

window.devicePixelRatio表示物理像素与逻辑像素之间的缩放系数,这就是像素比。

CSS像素是Web开发中使用的统一像素单位,即CSS中的px。这个像素是逻辑像素,而非物理像素

物理像素就是你的屏幕上的一个光点,无数个光点组成了你的屏幕,你的屏幕拥有的光点的数量也就是你的屏幕分辨率。

那么现在问题来了:

考虑电脑和手机,大森的电脑屏幕是15.6英寸,1920×1080的分辨率,也就是屏幕上组成图像的小光点有1920×1080这么多,而大森的手机屏幕尺寸才6.7英寸,远小于电脑屏幕,但它的分辨率却有3216×1440,也就是说大森的手机小小的屏幕的像素密度要远远大于电脑的屏幕,小小的屏幕里充满了多多的像素。

这导致了手机的一个物理像素远小于电脑的一个物理像素,那么一个3px粗的线条,如果使用物理像素来绘制,那么在电脑上这条线的粗细看起来要远远粗于手机上显示的,因此引入了逻辑像素的概念:即使用若干个物理像素来拼成一个逻辑像素

举个例子,假如手机上的像素很小,16个像素拼起来才有电脑的一个像素大,那么就把手机的16个像素拼成一个像素来用,这样使得手机和电脑上的显示大小看起来差不多,这就是逻辑像素的作用。CSS像素就是一种逻辑像素。

那么window.devicePixelRatio所表示的像素比就可以认为是CSS像素相对物理像素的缩放比例,它的值为1就代表着你的设备的1个物理像素被用来当作一个逻辑像素了,而我测试的我的Chrome浏览器的该值为1.25,即逻辑像素是物理像素的1.25倍,而搜狗浏览器的该值为1。
同时这个像素比的值是会随着屏幕分辨率以及用户缩放页面而改变的

下方显示你目前所用浏览器的像素比:

你的浏览器的 window.devicePixelRatio 值为:

窗口尺寸

窗口大小涉及到四个属性:

  • outerWidth和outerHeight属性:不管是在最外层window上使用,还是在窗格<frame>中使用,都返回浏览器窗口的大小。
  • innerWidth和innerHeight属性:返回当前页面视口的大小。

窗口的innerWidth和innerHeight属性返回页面视口的大小,窗口就是整个浏览器的窗口,包含了浏览器的工具栏等内容,而视口就是页面文档所占的区域(包括滚动条所占的区域)。

还有两个方法可以用来调整窗口尺寸:

  • resizeTo():接收新的宽度和高度值,单位是像素,正值。
  • resizeBy():接收宽度和高度各要缩放多少,单位是像素,可正可负。

同样它们可能被浏览器禁用。

视口尺寸

由于浏览器的原因,可能无法得到浏览器窗口的具体位置和尺寸,但页面视口的尺寸是可以精确得到的,涉及到document对象的以下属性:

  • document.compatMode:可能的值有"BackCompat"表示混杂模式,"CSS1Compat"表示标准模式。
  • document.documentElement.clientWidth:标准模式下,返回页面视口的宽度。
  • document.documentElement.clientHeight:标准模式下,返回页面视口的高度。
  • document.body.clientWidth:混杂模式下,返回页面视口的宽度。
  • document.body.clientHeight:混杂模式下,返回页面视口的高度。

标准模式和混杂模式实际上是CSS模式,主要的区别是盒模型。标准模式的盒模型为标准盒模型,width和height尺寸只包含内容区域,而混杂模式的盒模型为奇异盒模型,其width和height尺寸包含border。

window.innerWidthwindow.innerHeight如果有值的话,将会和document.documentElement.clientWidth以及document.documentElement.clientHeight相等,它们都代表视口尺寸。

因此可通过以下方式来获取浏览器视口尺寸:

1
2
3
4
5
6
7
8
9
10
11
let pageWidth = window.innerWidth,
pageHeight = window.innerHeight;
if (typeof pageWidth != "number") {
if (document.compatMode == "CSS1Compat") {
pageWidth = document.documentElement.clientWidth;
pageHeight = document.documentElement.clientHeight;
} else {
pageWidth = document.body.clientWidth;
pageHeight = document.body.clientHeight;
}
}

或者使用更简单的做法:

1
2
3
4
5
6
var pageWidth = window.innerWidth ||
document.documentElement.clientWidth ||
document.body.clientWidth;
var pageHeight = window.innerHeight ||
document.documentElement.clientHeight ||
document.body.clientHeight;

移动设备上同样有这些值,但代表的内容可能不太一样。

移动浏览器都是运行在标准模式的,因此在移动设备上不必使用document.body.clientWidthdocument.body.clientHeight

对于Mobile Internet Explorer浏览器,window.innerWidthwindow.innerHeight返回视口的大小和document.documentElement.clientWidthdocument.documentElement.clientHeight返回的一样,都是可见视口的尺寸,布局视口则通过document.body.clientWidthdocument.body.clientHeight获取。

对于其他移动浏览器,window.innerWidthwindow.innerHeight代表可见视口尺寸,而document.documentElement.clientWidthdocument.documentElement.clientHeight代表布局视口的大小。

布局视口大小:整个页面文档的大小。

可见视口大小:是布局视口的一部分,是可见的那一部分。

同时,当浏览器页面被用户缩放时,这些值也会随之改变,但要注意,当页面被放大时,值反而会变小,当页面被缩小时,值会变大。原因是因为:窗口宽度是一定的,那么视口所占的物理像素是一定的,放大页面会使window.devicePixelRatio变大,意味着使用更多的物理像素来表示一个CSS像素,那么同样数量的物理像素能表示的CSS像素就少了,而视口尺寸又是基于CSS像素统计的,因此放大页面导致视口中的CSS像素变少了,视口尺寸就变小了。

视口位置

页面一般比视口大,因此需要通过在页面上滚动视口来查看完整的文档,因此需要调整视口位置。这涉及的属性和方法有:

  • window.pageXoffset / window.scrollX:视口距页面左侧的位置。
  • window.pageYoffset / window.scrollY:视口距页面顶部的位置。
  • scroll()方法:绝对滚动,接受两个参数分别为x、y坐标,表示到滚动到的位置,单位为像素。
  • scrollTo()方法:同上。
  • scrollBy()方法:相对滚动,相对当前位置的像素偏移,可正可负。

window.scrollXwindow.scrollYwindow.pageXoffsetwindow.pageYoffset中可能有其中一组没有值,建议优先使用window.scrollXwindow.scrollY

这几个方法也都接收一个ScrollToOptions字典,除了提供偏移值,还可以通过behavior属性告诉浏览器是否平滑滚动,如:

1
2
3
4
5
6
7
8
9
10
11
12
// 正常滚动
window.scrollTo({
left: 100,
top: 100,
behavior: 'auto'
});
// 平滑滚动
window.scrollTo({
left: 100,
top: 100,
behavior: 'smooth'
});

跳转与打开

在窗口中打开链接

跳转与打开都通过window.open()方法来实现。该方法接收四个参数:要加载的URL、目标窗口、特性字符串和是否替代当前加载页面的布尔值。一般只用到前三个,第四个参数只在不打开新窗口或选项卡时才会用。

假如我如此使用:

1
window.open("https://sadose.github.io/","myFrame");

那么它就和

1
<a href="https://sadose.github.io/" target="myFrame"></a>

没有什么区别。即如果有一个窗口名字叫做"myFrame",那么就在它里面打开链接,否则就新建一个名叫"myFrame"的窗口来打开链接。窗口的名称使用window对象的name属性来保存。

当然你可能想到了a标签的target属性还有个常用值为"_blank",代表在新标签页打开链接,在window.open()方法中当然也可使用啦,实际上,_self_parent_top_blank都可以使用,分别代表在本窗口、父窗口、顶级窗口和新窗口打开链接,当然现在浏览器往往不新建窗口,而是新建标签页。

如果需要打开新窗口而非新选项卡,则需要指定第三个参数,即特性字符串,指定了新窗口的配置。主要有以下配置项:

多个配置项之间用逗号分隔即可,如以下代码:

1
2
3
window.open("https://sadose.github.io/",
"blogWindow",
"height=800, width=800, top=10, left=10, resizable=yes");

就在新窗口中打开了800×800大小的窗口,距离屏幕顶部和底部的距离都是10像素,并且窗口可调整大小。

操纵窗口

window.open()方法的返回值是新打开的窗口(或标签页),那么就可以用一个变量把它保存下来,从而在父窗口中操纵子窗口。

操纵窗口的方法主要就是window对象上的close()方法,以及移动窗口的moveTo()moveBy()方法,还有更改窗口尺寸的resizeTo()resizeBy()方法。

但要注意:close()方法只能关闭通过open()方法打开的窗口,并且关闭之后,父窗口并不知道子窗口被关闭了,因为它里面可能还保留着对这个子窗口的引用,这时通过该引用的closed属性(布尔值)可判断窗口是否已被关闭。

同时新创建窗口的window对象有一个属性opener,指向打开它的窗口,但反之不然,窗口不会跟踪记录自己打开的新窗口,因此开发者需要自己记录。

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

安全限制

由于曾经浏览器弹窗被广告滥用,因此很多浏览器对弹窗添加了限制,如需要用户进行允许操作才会弹出窗口,或者干脆使用了弹窗屏蔽。

如果弹窗失败,window.open()方法将返回null,可以通过它来判断弹窗是否成功。

定时器

定时器相关的函数是window对象上的两对方法:

  • setTimeout()方法和clearTimeout()方法。
  • setInterval()方法和clearInterval()方法。

setTimeout()方法接收两个必选参数:要执行的代码和在执行回调函数前等待的时间(毫秒)。

第一个参数可以是包含JavaScript代码的字符串(类似于传给eval()的字符串),也可以是一个函数。

如果第一个参数是一个函数时,在两个必选参数之后还可以有任意个可选参数,这些可选参数都将作为参数传递给第一个参数所指定的函数。

该函数的返回值是一个表示该超时排期的数值ID,这个ID是被排期任务的唯一标识符,可以调用clearTimeout()函数并传入超时ID来取消被排期的任务。

在任务未被加入队列之前调用clearTimeout()函数就可以取消超时任务,在任务加入队列后再调用clearTimeout()是没有效果的。

setInterval()函数与setTimeout()函数类似,只不过指定的任务会每隔指定时间就执行一次,其第二个参数是两次执行的间隔,直到取消循环定时或者页面卸载。

它也会返回一个ID,可以通过调用clearInterval()函数来取消任务。

一般来说,最好不要使用setInterval()函数,而应该使用setTimeout()函数来实现功能,因为使用setInterval()函数无法保证两个任务之间的时间间隔,并且不容易管理其在合适停止,而使用setTimeout()函数可以解决这些问题,思路是在某个条件满足时使用setTimeout()函数设置下一周期的任务执行,在条件不满足时就不再设置下一周期的函数了,因此避免了意外地忘记停止循环任务,并且可以保证两个任务之间的时间间隔。

如对于一个变量num,使其每0.5秒递增1,直到它达到10,可以这样写:

1
2
3
4
5
6
7
8
9
10
11
12
13
let num = 0;
const id = setInterval(() => {
num++;
if (num >= 10) clearInterval(id);
}, 500);
而使用setTimeout()函数代替setInterval()函数,可以这样写:
let num = 0;
(function increace() {
setTimeout(() => {
num++;
if (num < 10) increace();
}, 500);
})();

系统对话框

使用alert()confirm()prompt()方法,可以让浏览器调用系统对话框向用户显示消息。这些对话框与浏览器中显示的网页无关,而且也不包含HTML。它们的外观由操作系统或者浏览器决定,无法使用CSS设置。此外,这些对话框都是同步的模态对话框,即在它们显示的时候,代码会停止执行,在它们消失以后,代码才会恢复执行。

  • alert()函数:接收一个字符串作为参数,由浏览器弹窗提示用户,如果参数不是字符串,则会调用其toString()方法转为字符串。
  • confirm()函数:显示一个确认框,和alert()函数类似,但区别是它会显示“确认”和“取消”两个按钮,其返回值表示了用户的选择,true为确认,false为取消,点击了关闭按钮则默认认为点击了取消。
  • prompt()方法:显示提示框,用途是提示用户输入消息。提示框会显示一个确认按钮、一个取消按钮和一个文本框,让用户输入内容。它接收两个参数:要显示给用户的文本,以及文本框的默认值(可以是空字符串)。用户点击了确认按钮,函数会返回用户输入的值,点击了取消或关闭按钮则会返回null。

浏览器可能会允许用户选择屏蔽后续的弹框,因此这些弹框可能会失败。

JavaScript还可以显示另外两种对话框:find()print()。这两种对话框都是异步显示的,即控制权会立即返回给脚本。用户在浏览器菜单上选择“查找”和“打印”时显示的就是这两种对话框。通过在window对象上调用find()print()可以显示它们:

1
2
window.print();
window.find();

用户选择了禁用后续弹框不会影响到这两种对话框

location对象

location是最有用的BOM对象之一,提供了当前窗口中加载文档的信息,以及通常的导航功能。它独特的地方在于,它既是window的属性,也是document的属性,即window.locationdocument.location指向同一个对象。

属性

location对象不仅保存着当前加载文档的信息,也保存着把URL解析为离散片段后能够通过属性访问的信息

对于这样一个URL:

1
http://foouser:barpassword@www.wrox.com:80/WileyCDA/?q=javascript#contents

以下为location对象的各个属性分别对应的值:

地址操作

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

1
location.assign("https://sadose.github.io/");

这样即可将当前页面导航到新URL,同时在浏览器的历史记录中增加一条记录。

如果给location.hrefwindow.location设置一个URL,本质上也是调用了location.assign()方法。

修改location对象的属性也会修改当前加载的页面。其中,hash、search、hostname、pathname和port属性被设置为新值之后都会修改当前URL。只要修改location的除了hash之外的一个属性,就会导致页面重新加载新URL。

在以前面提到的方式修改URL之后,浏览器历史记录中就会增加相应的记录。当用户单击“后退”按钮时,就会导航到前一个页面。如果不希望增加历史记录,可以使用location.replace()方法。这个方法接收一个URL参数,但重新加载后不会增加历史记录。调用location.replace()之后,用户不能回到前一页。
最后一个修改地址的方法是location.reload(),它能重新加载当前显示的页面,即刷新操作,它不需要参数。但是注意,刷新之后,后面的脚本可能不会被执行,因此location.reload()应当成为一个过程的最后一行代码。

navigator对象

只要浏览器启用JavaScript, navigator对象就一定存在。但是与其他BOM对象一样,每个浏览器都支持自己的属性。navigator对象的属性通常用于确定浏览器的类型。

navigator对象的方法和属性非常多,因此只主要学习其两个主要用途:检测插件和注册处理程序。

检测插件

通过window.navigator.plugins可以访问到浏览器安装的插件的数组,注意,插件数组是一个类数组对象,并且浏览器插件不是浏览器安装的扩展程序。可以看到下图为我的Chrome浏览器中安装的插件:

每一个插件都有name属性,可以通过它来判断某个插件是否存在。可以通过以下函数,传入插件名称,来判断插件是否存在:

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

这种方式不能用于IE10及更低版本的浏览器。

更低版本的浏览器的插件检测已不常用,不再介绍。

注册处理程序

navigator对象的registerProtocolHandler()方法可以把一个网站注册为处理某种特定类型信息应用程序。

要使用registerProtocolHandler()方法,必须传入3个参数:要处理的协议(如"mailto""ftp")、处理该协议的URL,以及应用名称。

这个API也不是很常使用,更多可参考MDN

history对象

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

导航

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

go()还有有两个简写方法:back()forward(),这两个方法模拟了浏览器的后退按钮和前进按钮,即后退一页和前进一页。

如:

1
2
3
4
5
6
7
8
9
10
// 后退一页
history.go(-1);
// 前进一页
history.go(1);
// 前进两页
history.go(2);
// 后退一页
history.back();
// 前进一页
history.forward();

history对象还有一个length属性,表示历史记录中有多个条目。这个属性反映了历史记录的数量,包括可以前进和后退的页面。

对于窗口或标签页中加载的第一个页面,history.length等于1。

历史状态管理

hashchange事件

hashchange事件会在页面URL的散列变化时被触发,开发者可以在此时执行某些操作。

而状态管理API则可以让开发者改变浏览器URL而不会加载新页面。

pushState方法

可以使用history.pushState()方法来改变页面URL地址。

这个方法接收3个参数:一个state对象、一个新状态的标题和一个(可选的)相对URL。例如:

1
2
let stateObject = { description: "这是一个描述" };
history.pushState(stateObject, "Title", "baz.html");

pushState()方法执行后,状态信息就会被推到历史记录中,浏览器地址栏也会改变以反映新的相对URL。

除了这些变化之外,不会发生其他变化,即URL改变但浏览器不会向服务器发送请求,因此该方法常用于前端路由。

第二个参数是一个短标题描述,但目前没什么用处。

第一个参数应该包含正确初始化页面状态所必需的信息对象。为防止滥用,这个状态的对象大小是有限制的,通常在500KB~1MB以内。

popstate事件

因为pushState()会创建新的历史记录,所以也会相应地启用“后退”按钮。此时单击“后退”按钮,就会触发window对象上的popstate事件。

popstate事件的事件对象有一个state属性,它其实就是调用pushState()方法时第一个参数传入的state对象:

1
2
3
4
5
6
window.addEventListener("popstate", (event) => {
let state = event.state;
if (state) {
processState(state);
}
});

基于这个状态,我们应该手动把页面重置为状态对象所表示的状态,浏览器不会自动为你做这些。

此外,页面初次加载时没有状态,因此点击“后退”按钮直到返回最初页面时,event.state会为null。

replaceState方法

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

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

注意

使用HTML5状态管理时,要确保通过pushState()创建的每个“假”URL背后都对应着服务器上一个真实的物理URL,或者前端能够处理这些路由信息,否则单击“刷新”按钮会导致404错误。

因此所有单页应用程序(SPA)框架都必须通过服务器或客户端的配置来解决这个问题。

screen对象

这个对象在编程中很少使用,其中保存的纯粹是客户端能力信息,也就是浏览器窗口外面的客户端显示器的信息,比如像素宽度和像素高度。每个浏览器都会在screen对象上暴露不同的属性:




* 你好,我是大森。如果文章内容帮到了你,你可通过下方付款二维码支持作者 *