BOM的核心是window对象,表示浏览器的实例。
window对象在浏览器中有两重身份,一个是ECMAScript中的Global对象,另一个就是浏览器窗口的JavaScript接口。
这意味着网页中定义的所有对象、变量和函数都以window作为其Global对象,都可以访问其上定义的parseInt()等全局方法。
注意 因为window对象的属性在全局作用域中有效,所以很多浏览器API及相关构造函数都以window对象属性的形式暴露出来。
另外,由于实现不同,某些window对象的属性在不同浏览器间可能差异很大。

12.1.1 Global作用域

因为window对象被复用为ECMAScript的Global对象,所以通过var声明的所有全局变量和函数都会变成window对象的属性和方法。
如果使用let或const替代var,则不会把变量添加给全局对象

  1. var age = 29;
  2. var sayAge = () => console.log(this.age);
  3. console.log(window.age); // 29
  4. sayAge(); // 29
  5. window.sayAge(); // 29
  6. let age1 = 29;
  7. const sayAge1 = () => console.log(this.age1);
  8. console.log(window.age1); // undefined
  9. sayAge1(); // undefined
  10. window.sayAge1(); // Uncaught TypeError: window.sayAge1 is not a function

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

  1. // 这会导致抛出错误,因为oldVal没有声明
  2. var newVal = oldVal; // Uncaught ReferenceError: oldVal is not defined
  3. // 这不会抛出错误,因为这里是属性查询。newVal会被设置为undefined
  4. var newVal = window.oldVal;

JavaScript中有很多对象都暴露在全局作用域中,比如location和navigator(本章后面都会讨论),因而它们也是window对象的属性。

12.1.2 窗口关系

top对象始终指向最上层(最外层)窗口,即浏览器窗口本身。
parent对象则始终指向当前窗口的父窗口。
如果当前窗口是最上层窗口,则parent等于top(都等于window)。
最上层的window如果不是通过window.open()打开的,那么其name属性就不会包含值,本章后面会讨论。
self对象,它是终极window属性,始终会指向window。
实际上,self和window就是同一个对象。之所以还要暴露self,就是为了和top、parent保持一致。
这些属性都是window对象的属性,因此访问window.parent、window.top和window.self都可以。
这意味着可以把访问多个窗口的window对象串联起来,比如window.parent.parent。
12.1.3 窗口位置与像素比
window对象的位置可以通过不同的属性和方法来确定。
现代浏览器提供了screenLeft和screenTop属性,表示窗口相对于屏幕左侧和顶部的位置,返回值的单位是CSS像素。
可以使用moveTo()和moveBy()方法移动窗口。
这两个方法都接收两个参数
moveTo()接收要移动到的新位置的绝对坐标x和y;
moveBy()则接收相对当前位置在两个方向上移动的像素数。
依浏览器而定,以上方法可能会被部分或全部禁用。

像素比

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表示物理像素与逻辑像素之间的缩放系数。

12.1.4 窗口大小

现代浏览器支持4个属性:innerWidth、innerHeight、outerWidth和outerHeight。
outerWidth和outerHeight返回浏览器窗口自身的大小(不管是在最外层window上使用,还是在窗格中使用)。
innerWidth和innerHeight返回浏览器窗口中页面视口的大小(不包含浏览器边框和工具栏)。document.documentElement.clientWidth和document.documentElement.clientHeight返回页面视口的宽度和高度。
浏览器窗口自身的精确尺寸不好确定,但可以确定页面视口的大小
如下所示:

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

先将pageWidth和pageHeight的值分别设置为window.innerWidth和window. innerHeight。
然后,检查pageWidth是不是一个数值,如果不是则通过document.compatMode来检查页面是否处于标准模式。
如果是,则使用document.documentElement.clientWidth和document.documentElement.clientHeight;
否则,就使用document.body.clientWidth和document.body.clientHeight。
在移动设备上,window.innerWidth和window.innerHeight返回视口的大小,也就是屏幕上页面可视区域的大小。Mobile Internet Explorer支持这些属性,但在document.documentElement. clientWidth和document.documentElement.clientHeight中提供了相同的信息。在放大或缩小页面时,这些值也会相应变化。
在其他移动浏览器中,document.documentElement.clientWidth和document.documentElement. clientHeight返回的布局视口的大小,即渲染页面的实际大小。布局视口是相对于可见视口的概念,可见视口只能显示整个页面的一小部分。Mobile Internet Explorer把布局视口的信息保存在document.body.clientWidth和document.body.clientHeight中。在放大或缩小页面时,这些值也会相应变化。
因为桌面浏览器的差异,所以需要先确定用户是不是在使用移动设备,然后再决定使用哪个属性。
可以使用resizeTo()和resizeBy()方法调整窗口大小。
这两个方法都接收两个参数,resizeTo()接收新的宽度和高度值,而resizeBy()接收宽度和高度各要缩放多少。
下面看个例子:

  1. window.resizeTo(100, 100); // 缩放到100×100
  2. window.resizeBy(100, 50); // 缩放到200×150
  3. window.resizeTo(300, 300); // 缩放到300×300

与移动窗口的方法一样,缩放窗口的方法可能会被浏览器禁用,而且在某些浏览器中默认是禁用的。
同样,缩放窗口的方法只能应用到最上层的window对象。

12.1.5 视口位置

用户通过滚动在有限的视口中查看文档。度量文档相对于视口滚动距离的属性有两对,返回相等的值:window.pageXoffset/window. scrollX和window.pageYoffset/window.scrollY。
可以使用scroll()、scrollTo()和scrollBy()方法滚动页面。
这3个方法都接收表示相对视口距离的x和y坐标,这两个参数在前两个方法中表示要滚动到的坐标,在最后一个方法中表示滚动的距离。

  1. window.scrollBy(0, 100); // 相对于当前视口向下滚动100像素
  2. window.scrollBy(40, 0); // 相对于当前视口向右滚动40像素
  3. window.scrollTo(0, 0); // 滚动到页面左上角
  4. window.scrollTo(100, 100); // 滚动到距离屏幕左边及顶边各100像素的位置

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

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

12.1.6 导航与打开新窗口

window.open()方法用于导航到指定URL,也可用于打开新浏览器窗口。
这个方法接收4个参数:要加载的URL、目标窗口、特性字符串和表示新窗口在浏览器历史记录中是否替代当前加载页面的布尔值。
通常,调用这个方法时只传前3个参数,最后一个参数只有在不打开新窗口时才会使用。
如果window.open()的第二个参数是一个已经存在的窗口或窗格(frame)的名字,则会在对应的窗口或窗格中打开URL。
下面是一个例子:

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

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

1.弹出窗口

如果window.open()的第二个参数不是已有窗口,则会打开一个新窗口或标签页。
第三个参数,即特性字符串,用于指定新窗口的配置。
如果没有传第三个参数,则新窗口(或标签页)会带有所有默认的浏览器特性(工具栏、地址栏、状态栏等都是默认配置)。
如果打开的不是新窗口,则忽略第三个参数。
特性字符串是一个逗号分隔的设置字符串,用于指定新窗口包含的特性。
下表列出了一些选项:
image.png
这些设置需要以逗号分隔的名值对形式出现,其中名值对以等号连接。(特性字符串中不能包含空格。)
来看下面的例子:

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

这行代码会打开一个可缩放的新窗口,大小为400像素×400像素,位于离屏幕左边及顶边各10像素的位置。window.open()方法返回一个对新建窗口的引用。
这个对象与普通window对象没有区别,只是为控制新窗口提供了方便。
例如,某些浏览器默认不允许缩放或移动主窗口,但可能允许缩放或移动通过window.open()创建的窗口。
跟使用任何window对象一样,可以使用这个对象操纵新打开的窗口。

  1. let wroxWin = window.open('http://www.wrox.com/',
  2. 'wroxWindow',
  3. 'height=400, width=400, top=10, left=10, resizable=yes');
  4. // 缩放
  5. wroxWin.resizeTo(500, 500);
  6. // 移动
  7. wroxWin.moveTo(100, 100);

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

  1. wroxWin.close();

新创建窗口的window对象有一个属性opener,指向打开它的窗口。
这个属性只在弹出窗口的最上层window对象(top)有定义,是指向调用window.open()打开它的窗口或窗格的指针。
把opener设置为null表示新打开的标签页不需要与打开它的标签页通信,因此可以在独立进程中运行。这个连接一旦切断,就无法恢复了。

2.安全限制

弹出窗口有段时间被在线广告用滥了。很多在线广告会把弹出窗口伪装成系统对话框,诱导用户点击。
因为长得像系统对话框,所以用户很难分清这些弹窗的来源。
为了让用户能够区分清楚,浏览器开始对弹窗施加限制。
IE的早期版本实现针对弹窗的多重安全限制,包括不允许创建弹窗或把弹窗移出屏幕之外,以及不允许隐藏状态栏等。
从IE7开始,地址栏也不能隐藏了,而且弹窗默认是不能移动或缩放的。
Firefox 1禁用了隐藏状态栏的功能,因此无论window.open()的特性字符串是什么,都不会隐藏弹窗的状态栏。Firefox3强制弹窗始终显示地址栏。
Opera只会在主窗口中打开新窗口,但不允许它们出现在系统对话框的位置。
此外,浏览器会在用户操作下才允许创建弹窗。在网页加载过程中调用window.open()没有效果,而且还可能导致向用户显示错误。弹窗通常可能在鼠标点击或按下键盘中某个键的情况下才能打开。
注:IE对打开本地网页的窗口再弹窗解除了某些限制。同样的代码如果来自服务器,则会施加弹窗限制。

3.弹窗屏蔽程序

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

  1. let wroxWin = window.open('http://www.wrox.com/',
  2. '_blank');
  3. if (wroxWin == null) {
  4. console.log('The popup was blocked!')
  5. }

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

  1. let blocked = false;
  2. try {
  3. let wroxWin = window.open('http://www.wrox.com/',
  4. '_blank');
  5. if (wroxWin == null) {
  6. blocked = true;
  7. }
  8. }catch(ex) {
  9. blocked = true;
  10. }
  11. if (blocked) {
  12. console.log('The popup was blocked!')
  13. }

12.1.7 定时器

JavaScript在浏览器中是单线程执行的,但允许使用定时器指定在某个时间之后或每隔一段时间就执行相应的代码。
setTimeout()用于指定在一定时间后执行某些代码
setInterval()用于指定每隔一段时间执行某些代码。
setTimeout()方法通常接收两个参数:要执行的代码和在执行回调函数前等待的时间(毫秒)。
第一个参数可以是包含JavaScript代码的字符串(类似于传给eval()的字符串)或者一个函数,
比如:

  1. // 在1秒后显示
  2. setTimeout(() => console.log('Hello World!'), 1000);

第二个参数是要等待的毫秒数,而不是要执行代码的确切时间。
JavaScript是单线程的,所以每次只能执行一段代码。
为了调度不同代码的执行,JavaScript维护了一个任务队列。
其中的任务会按照添加到队列的先后顺序执行。
setTimeout()的第二个参数只是告诉JavaScript引擎,在指定的毫秒数过后把任务添加到这个队列。
如果队列是空的,则会立即执行该代码。
如果队列不是空的,则代码必须等待前面的任务执行完才能执行。
调用setTimeout()时,会返回一个表示该超时排期的数值ID。这个超时ID是被排期执行代码的唯一标识符,可用于取消该任务。
要取消等待中的排期任务,可以调用clearTimeout()方法并传入超时ID
如下面的例子所示:

  1. // 设置超时任务
  2. let timeoutId = setTimeout(() => console.log('Hello World!'), 1000);
  3. // 取消超时任务
  4. clearTimeout(timeoutId);

只要是在指定时间到达之前调用clearTimeout(),就可以取消超时任务。
在任务执行后再调用clearTimeout()没有效果。
注:所有超时执行的代码(函数)都会在全局作用域中的一个匿名函数中运行,因此函数中的this值在非严格模式下始终指向window,而在严格模式下是undefined。如果给setTimeout()提供了一个箭头函数,那么this会保留为定义它时所在的词汇作用域。setInterval()与setTimeout()的使用方法类似,只不过指定的任务会每隔指定时间就执行一次,直到取消循环定时或者页面卸载。
setInterval()同样可以接收两个参数:要执行的代码(字符串或函数),以及把下一次执行定时代码的任务添加到队列要等待的时间(毫秒)
注:这里的关键点是,第二个参数,也就是间隔时间,指的是向队列添加新任务之前等待的时间。
比如,调用setInterval()的时间为01:00:00,间隔时间为3000毫秒。这意味着01:00:03时,浏览器会把任务添加到执行队列。浏览器不关心这个任务什么时候执行或者执行要花多长时间。因此,到了01:00:06,它会再向队列中添加一个任务。
由此可看出,执行时间短、非阻塞的回调函数比较适合setInterval()。
setInterval()方法也会返回一个循环定时ID,可以用于在未来某个时间点上取消循环定时。
要取消循环定时,可以调用clearInterval()并传入定时ID。
相对于setTimeout()而言,取消定时的能力对setInterval()更加重要。
毕竟,如果一直不管它,那么定时任务会一直执行到页面卸载。
下面是一个常见的例子:

  1. let num = 0, intervalId = null;
  2. let max = 10;
  3. let incrementNumber = function() {
  4. num ++ ;
  5. // 如果达到最大值,则取消所有未执行的任务
  6. if (num == max) {
  7. clearInterval(intervalId);
  8. console.log('Done');
  9. }
  10. }
  11. intervalId = setInterval(incrementNumber, 500);

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

  1. let num = 0;
  2. let max = 10;
  3. let incrementNumber = function() {
  4. num ++ ;
  5. // 如果还没有达到最大值,再设置一个超时任务
  6. if (num < max) {
  7. setTimeout(incrementNumber, 500);
  8. } else {
  9. console.log('Done');
  10. }
  11. }
  12. setTimeout(incrementNumber, 500);

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

12.1.8 系统对话框

使用alert()、confirm()和prompt()方法,可以让浏览器调用系统对话框向用户显示消息。
这些对话框与浏览器中显示的网页无关,而且也不包含HTML。
它们的外观由操作系统或者浏览器决定,无法使用CSS设置。
此外,这些对话框都是同步的模态对话框,即在它们显示的时候,代码会停止执行,在它们消失以后,代码才会恢复执行。
alert()方法接收一个要显示给用户的字符串。
与console.log可以接收任意数量的参数且能一次性打印这些参数不同,alert()只接收一个参数。
调用alert()时,传入的字符串会显示在一个系统对话框中。
对话框只有一个“OK”(确定)按钮。
如果传给alert()的参数不是一个原始字符串,则会调用这个值的toString()方法将其转换为字符串。
警告框(alert)通常用于向用户显示一些他们无法控制的消息,比如报错。
用户唯一的选择就是在看到警告框之后把它关闭。
第二种对话框叫确认框,通过调用confirm()来显示。
确认框跟警告框类似,都会向用户显示消息。
但不同之处在于,确认框有两个按钮:“Cancel”(取消)和“OK”(确定)。
用户通过单击不同的按钮表明希望接下来执行什么操作。
要知道用户单击了OK按钮还是Cancel按钮,可以判断confirm()方法的返回值:true表示单击了OK按钮,false表示单击了Cancel按钮或者通过单击某一角上的X图标关闭了确认框。
最后一种对话框是提示框,通过调用prompt()方法来显示。
提示框的用途是提示用户输入消息。除了OK和Cancel按钮,提示框还会显示一个文本框,让用户输入内容。prompt()方法接收两个参数:要显示给用户的文本,以及文本框的默认值(可以是空字符串)。
如果用户单击了OK按钮,则prompt()会返回文本框中的值。如果用户单击了Cancel按钮,或者对话框被关闭,则prompt()会返回null。

  1. let result = prompt('What is your name?');
  2. if (result !== null) {
  3. console.log('Welcome,' + result);
  4. }

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

  1. window.print(); // 显示打印对话框
  2. window.find(); // 显示查找对话框

这两个方法不会返回任何有关用户在对话框中执行了什么操作的信息,因此很难加以利用。
此外,因为这两种对话框是异步的,所以浏览器的对话框计数器不会涉及它们,而且用户选择禁用对话框对它们也没有影响。