红宝书之第十二章:BOM

321 阅读12分钟

首先看看BOM的定义:

浏览器对象模型,BOM是Browser Object Model 的简写方法;

window 对象

BOM的核心是window对象,表示浏览器的实例。window对象在浏览器中有两重身份,一个是ECMAScript中的Global对象,另一个就是浏览器窗口的JavaScript接口。这意味着网页中定义的所有对象、变量和函数都以window作为其Global对象,都可以访问其上定义的parseInt()等全局方法。

Global 作用域

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

如果在这里使用 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;

JavaScript 中有很多对象都暴露在全局作用域中,比如 location 和 navigator, 因而它们也是 window 对象的属性。

窗口关系
  • top 对象始终指向最上层(最外层)窗口,即浏览器窗口本身。
  • parent 对象则始终指向当前窗口的父窗口。
  • self 对象,它是终极 window 属性

如果当前窗口是最上层窗口,则 parent 等于 top

这些属性都是 window 对象的属性,因此访问 window.parent、window.top 和 window.self 都可以。

窗口位置与像素比

现代浏览器提供了 screenLeft 和 screenTop 属性,用于表示窗口相对于屏幕左侧和顶部的位置 ,返回值的单位是 CSS 像素

可以使用 moveTo()和 moveBy()方法移动窗口。这两个方法都接收两个参数,其中 moveTo()接 收要移动到的新位置的绝对坐标 x 和 y;而moveBy()则接收相对当前位置在两个方向上移动的像素数。

依浏览器而定,以上方法可能会被部分或全部禁用。

==像素比==

CSS 像素是 Web 开发中使用的统一像素单位

比如,低分辨率平板设备上 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 表示物理像素与逻辑像素之间的缩放系数

窗口大小

获取浏览器窗口大小方法:

// innerWidth和 innerHeight 返回浏览器窗口中页面视口的大小(不包含浏览器边框和工具栏)。
let pageWidth = window.innerWidth,
  pageHeight = window.innerHeight;
if (typeof pageWidth != "number") {
  if (document.compatMode == "CSS1Compat") {
  // document.documentElement.clientWidth 和 document.documentElement.clientHeight返回页面视口的宽度和高度
    pageWidth = document.documentElement.clientWidth;
    pageHeight = document.documentElement.clientHeight;
  } else {
    pageWidth = document.body.clientWidth;
    pageHeight = document.body.clientHeight;
  }
}

可以使用resizeTo()和resizeBy()方法调整窗口大小。这两个方法都接收两个参数,resizeTo() 接收新的宽度和高度值,而 resizeBy()接收宽度和高度各要缩放多少。

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

视口位置

度量文档相对于视口滚动距离的属性有两对,返回相等的值:window.pageXoffset/window. scrollX 和 window.pageYoffset/window.scrollY。

可以使用 scroll()、scrollTo()和 scrollBy()方法滚动页面。这 3 个方法都接收表示相对视口距离的 x 和 y 坐标,这两个参数在前两个方法中表示要滚动到的坐标,在最后一个方法中表示滚动的距离。

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

// 正常滚动 
window.scrollTo({ 
 left: 100, 
 top: 100, 
 behavior: 'auto' 
}); 
// 平滑滚动
window.scrollTo({ 
 left: 100, 
 top: 100, 
 behavior: 'smooth' 
});
导航与打开新窗口

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

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 对象有一个属性 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

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

安全限制

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

弹窗屏蔽程序

浏览器内置的弹窗屏蔽程序阻止了弹窗,那么 window.open()很可能会返回 null,也有可能抛出错误;

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!");
}
定时器

JavaScript 在浏览器中是单线程执行的,但允许使用定时器指定在某个时间之后或每隔一段时间就执行相应的代码。

  • setTimeout()用于指定在一定时间后执行某些代码,
  • setInterval()用于指定每隔一段时间执行某些代码。

调用 setTimeout()时,会返回一个表示该超时排期的数值ID,可用于取消该任务

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

setInterval()与 setTimeout()的使用方法类似

下面是一个常见的例子:

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);

使用 setTimeout()时,不一定要记录超时ID,因为它会在条件满足时自动停止一般来说,最好不要使用 setInterval()。

系统对话框

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

JavaScript 还可以显示另外两种对话框:find()和print()。这两种对话框都是异步显示的,即控 制权会立即返回给脚本。

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

因为这两种对话框是异步的,所以浏览器的对话框计数器不会涉及它们,而且用户选择禁用对话框对它们也没有影响

location 对象

location 是最有用的 BOM 对象之一,提供了当前窗口中加载文档的信息,以及通常的导航功能。

它既是 window 的属性,也是 document 的属性。也就是说,window.location 和 document.location 指向同一个对象

属性说明
location.hash"#contents"URL 散列值(井号后跟零或多个字符),如果没有则为空字符串
location.host"www.wrox.com:80"服务器名及端口号
location.hostname"www.wrox.com"服务器名
location.href"www.wrox.com:80/WileyCDA/?q…"当前加载页面的完整 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"www.wrox.com"URL 的源地址。只读
查询字符串

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;
};

// 假设查询字符串为?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"]

更多用法看下面这个文章:URLSearchParams初体验

操作地址

可以通过修改 location 对象修改浏览器的地址

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

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

如果不希望增加历史记录,可以使用 replace()方法。这个方法接收一个 URL 参数,但重新加载后不会增加历史记录

 location.replace("http://www.wrox.com/"),

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

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

navigator 对象

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

检测插件
// 插件检测,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"));

旧版本 IE 中的插件检测

// 在旧版本 IE 中检测插件
function hasIEPlugin(name) {
  try {
    new ActiveXObject(name);
    return true;
  } catch (ex) {
    return false;
  }
}
// 检测 Flash
alert(hasIEPlugin("ShockwaveFlash.ShockwaveFlash"));
// 检测 QuickTime
alert(hasIEPlugin("QuickTime.QuickTime"));
注册处理程序(了解)

registerProtocolHandler()方法,必须传入 3 个参数:

  • 要处理的协议(如"mailto"或"ftp")
  • 处理该协议的 URL
  • 以及应用名称

要把一个 Web 应用程序注册为默认邮件客户端,可以这样做:

navigator.registerProtocolHandler("mailto", 
 "http://www.somemailclient.com?cmd=%s", 
 "Some Mail Client");

screen 对象(了解)

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

history 对象

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

导航

go()方法可以在用户历史记录中沿任何方向导航,可以前进也可以后退。

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

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

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

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

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

history 对象通常被用于创建“后退”和“前进”按钮,以及确定页面是不是用户历史记录中的第 一条记录。

历史状态管理
  • hashchange 会在页面 URL 的散列变化时被触发,开发者可以在此时执行某些操作。
  • history.pushState()方法。这个方法接收 3 个参数:一个 state 对象、一个新状态的标题和一个(可选的)相对 URL。
let stateObject = {foo:"bar"}; 
history.pushState(stateObject, "My title", "baz.html");

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

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

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

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

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