BOM
window 对象
BOM 的核心是 window 对象,表示浏览器的实例。
window 对象在浏览器中有两重身份,一个是ECMAScript 中的 Global 对象,另一个就是浏览器窗口的 JavaScript 接口。
Global 作用域
window 对象被复用为 ECMAScript 的 Global 对象。
如果使用 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 对象上查询是否存在可能未声明的变量。
记住,JavaScript 中有很多对象都暴露在全局作用域中,比如 location 和 navigator(本章后面都会讨论),因而它们也是 window 对象的属性。
窗口关系
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。
窗口位置与像素比
现代浏览器提供了 screenLeft 和screenTop 属性,用于表示窗口相对于屏幕左侧和顶部的位置 ,返回值的单位是 CSS 像素。
像素比
义像素大小是为了在
不同设备上统一标准。比如,低分辨率平板设备上 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 表示物理像素与逻辑像素之间的缩放系数。
//我的电脑
console.log(window.devicePixelRatio)
// 1.25
窗口大小
所有现代浏览器都支持 4 个属性:innerWidth、innerHeight、outerWidth 和 outerHeight。(innerHeight不包含浏览器边框和工具栏)。
document.documentElement.clientWidth 和 document.documentElement.clientHeight返回页面视口的宽度和高度。
浏览器窗口自身的精确尺寸不好确定,但可以确定页面视口的大小,如下所示:
let pageWidth = window.innerWidth,
pageHeight = window.innerHeight;
//这里我的电脑是处于标准模式的
if (typeof pageWidth != "number") { //检查 pageWidth 是不是一个数值
if (document.compatMode == "CSS1Compat"){ //如果不是则通过 document.compatMode来检查页面是否处于标准模式。
pageWidth = document.documentElement.clientWidth;
pageHeight = document.documentElement.clientHeight;
} else {
pageWidth = document.body.clientWidth;
pageHeight = document.body.clientHeight;
}
}
视口位置
用户可以通过滚动在有限的视口中查看文档。度量文档相对于视口滚动距离的属性有两对,返回相等的值:window.pageXoffset/window. scrollX 和 window.pageYoffset/window.scrollY。
导航与打开新窗口
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。
- 弹出窗口
如果 window.open()的第二个参数不是已有窗口,则会打开一个新窗口或标签页。第三个参数,即特性字符串,用于指定新窗口的配置。如果没有传第三个参数,则新窗口(或标签页)会带有所有默认的浏览器特性(工具栏、地址栏、状态栏等都是默认配置)。如果打开的不是新窗口,则忽略第三个参数。
特性字符串是一个逗号分隔的设置字符串,用于指定新窗口包含的特性。下表列出了一些选项。
设 置 | 值 | 说 明 |
---|---|---|
fullscreen | "yes"或"no" | 表示新窗口是否最大化。仅限 IE 支持 |
height | 数值 | 新窗口高度。这个值不能小于 100 |
left | 数值 | 新窗口的 x 轴坐标。这个值不能是负值 |
location | "yes"或"no" | 表示是否显示地址栏。不同浏览器的默认值也不一样。在设置为"no"时,地址栏可能隐藏或禁用(取决于浏览器) |
Menubar | "yes"或"no" | 表示是否显示菜单栏。默认为"no" |
resizable | "yes"或"no" | 表示是否可以拖动改变新窗口大小。默认为"no" |
scrollbars | "yes"或"no" | 表示是否可以在内容过长时滚动。默认为"no" |
status | "yes"或"no" | 表示是否显示状态栏。不同浏览器的默认值也不一样 |
toolbar | "yes"或"no" | 表示是否显示工具栏。默认为"no" |
top | 数值 | 新窗口的 y 轴坐标。这个值不能是负值 |
width | 数值 | 新窗口的宽度。这个值不能小于 100 |
window.open()方法返回一个对新建窗口的引用。这个对象与普通 window 对象没有区别,只是为控制新窗口提供了方便。例如,某些浏览器默认不允许缩放或移动主窗口,但可能允许缩放或移动通过window.open()创建的窗口。
//谷歌浏览器本身主窗口的缩放,移动默认禁止
//打开新窗口wroxWindow,打开百度链接,该窗口大小为 400 像素×400 像素,位于离屏幕左边及顶边各 10 像素的位置,允许改变大小
let wroxWin = window.open("http://www.baidu.com/",
"wroxWindow",
"height=400,width=400,top=10,left=10,resizable=yes");
//新创建窗口的 window 对象有一个属性 opener,指向打开它的窗口。
alert(wroxWin.opener === window); // true
// 缩放 在主窗口控制wroxWin的缩放
wroxWin.resizeTo(500, 500);
// 移动 在主窗口控制wroxWin的移动
wroxWin.moveTo(100, 100);
//关闭窗口
wroxWin.close();
window.close()只能用于 window.open()创建的弹出窗口。虽然不可能不经用户确认就关闭主窗口,但弹出窗口可以调用 top.close()来关闭自己。关闭窗口以后,窗口的引用虽然还在,但只能用于检查其 closed 属性了:
wroxWin.close();
alert(wroxWin.closed); // true
虽然新建窗口中有指向打开它的窗口的指针(wroxWin.opener),但反之则不然。窗口不会跟踪记录自己打开的新窗口,因此开发者需要自己记录。
在某些浏览器中,每个标签页会运行在独立的进程中。如果一个标签页打开了另一个,而 window对象需要跟另一个标签页通信,那么标签便不能运行在独立的进程中。在这些浏览器中,可以将新打开的标签页的 opener 属性设置为 null,表示新打开的标签页可以运行在独立的进程中。比如:
//表示新打开的标签页不需要与打开它的标签页通信,因此可以在独立进程中运行。这个连接一旦切断,就无法恢复了。
wroxWin.opener = null;
-
安全限制
早期很多在线广告会把弹出窗口伪装成系统对话框,诱导用户点击。因为长得像系统对话框,所以用户很难分清这些弹窗的来源。后期限制不允许隐藏状态栏,地址栏等措施。 -
弹窗屏蔽程序
所有现代浏览器都内置了屏蔽弹窗的程序,因此大多数意料之外的弹窗都会被屏蔽。如果浏览器内置的弹窗屏蔽程序阻止了弹窗,那么 window.open()很可
能会返回 null,此时,只要检查这个方法的返回值就可以知道弹窗是否被屏蔽了。
此外,在浏览器扩展或其他程序屏蔽弹窗时,window.open()通常会抛出错误。除了检测 window.open()的返回值,还要把它用 try/catch 包装起来://无论弹窗是用什么方法屏蔽的,以上代码都可以准确判断调用 window.open()的弹窗是否被屏蔽了。 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()时,会返回一个表示该超时排期的数值 ID。这个超时 ID 是被排期执行代码的唯一标识符,可用于取消该任务。要取消等待中的排期任务,可以调用 clearTimeout()方法并传入超时 ID。
setInterval()的关键点:,第二个参数,也就是间隔时间,指的是向队列添加新任务之前等待的时间。调用 setInterval()的时间为 12点,间隔时间为 3000 毫秒。这意味着 12点过3秒时,浏览器会把任务添加到执行队列。浏览器不关心这个任务什么时候执行或者执行要花多长时间。因此,到了 12点过6秒,它会再向队列中添加一个任务。由此可看出,执行时间短、非阻塞的回调函数比较适合 setInterval()。
同样:setInterval()方法也会返回一个循环定时 ID,可以用于在未来某个时间点上取消循环定时。系统对话框
警告框(alert)通常用于向用户显示一些他们无法控制的消息,比如报错。用户唯一的选择就是在看到警告框之后把它关闭。
确认框(confirm),确认框跟警告框类似,都会向用户显示消息。但不同之处在于,确认框有两个按钮:“Cancel”(取消)和“OK”(确定)。用户通过单击不同的按钮表明希望接下来执行什么操作。if (confirm("Are you sure?")) { //点击了“OK”(确定)之后。返回值为true alert("I'm so glad you're sure!"); } else { //点击了"Cancel”(取消)之后。返回值为false alert("I'm sorry to hear you're not sure."); }
提示框(prompt),提示框的用途是提示用户输入消息。除了 OK 和 Cancel 按钮,提示框还会显示一个文本框,让用户输入内容。prompt()方法接收两个参数:要显示给用户的文本,以及文本框的默认值(可以是空字符串)。
let result = prompt("What is your name? ", ""); //单击了 Cancel 按钮,或者对话框被关闭,则 prompt()会返回 null。 if (result !== null) { //点击了“OK”(确定)之后。返回值为输入框的值 alert("Welcome, " + result); }
很多浏览器针对这些系统对话框添加了特殊功能。
JavaScript 还可以显示另外两种对话框:find()和 print()。这两种对话框都是异步显示的,即控制权会立即返回给脚本。location 对象
location 是最有用的 BOM 对象之一,提供了当前窗口中加载文档的信息,以及通常的导航功能。这个对象独特的地方在于,它既是 window 的属性,也是 document 的属性。也就是说,window.location 和 document.location 指向同一个对象。
假设浏览器当前加载的 URL 是 http://foouser:barpassword@www.wrox.com:80/WileyCDA/?q=javascript#contents ,location 对象的内容如下表所示
属 性 | 值 | 说 明 |
---|---|---|
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 的源地址。只读 |
查询字符串
虽然location.search 返回了从问号开始直到 URL 末尾的所有内容,但没有办法逐个访问每个查询参数。
location.search 返回了从问号开始直到 URL 末尾的所有内容,但没有办法逐个访问每个查询参数。下面的函数解析了查询字符串,并返回一个以每个查询参数为属性的对象:
let getQueryStringArgs = function() { //location.search返回的: ?参数1=xxx&参数2=xxx(问号及其之后的参数数据)
// 取得没有开头问号的查询字符串
let qs = (location.search.length > 0 ? location.search.substring(1) : ""), //如果有返回,把问号去掉
// 保存数据的对象qs,格式为:参数1=xxx&参数2=xxx
args = {};
// 把每个参数添加到 args 对象
for (let item of qs.split("&").map(kv => kv.split("="))) { //由&区分参数,split返回数组:[参数1=xxx,参数2=xxx]
let name = decodeURIComponent(item[0]), //map映射数组:[[参数1,xxx],[参数2,xxx]],然后遍历map后的数组
value = decodeURIComponent(item[1]); //name取参数1,value取xxx,如果参数1不是空,存入参数集合args
if (name.length) {
args[name] = value;
}
}
return args; //循环结束后返回args:{参数1:xxx,参数2:xxx}
}
decodeURIComponent() 函数可对 encodeURIComponent() 函数编码的 URI 进行解码。(这是因为查询字符串通常是被编码后的格式)
URLSearchParams
URLSearchParams 提供了一组标准 API 方法,通过它们可以检查和修改查询字符串。
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 的实例用作可迭代对象
操作地址
可以通过修改 location 对象修改浏览器的地址。首先,最常见的是使用 assign()方法并传入一个 URL。
如果给location.href 或 window.location 设置一个 URL,也会以同一个 URL 值调用 assign()方法。
修改 location 对象的属性也会修改当前加载的页面。(除了 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); //replace原来的地址替换,不能后退
</script>
</body>
</html>
最后一个修改地址的方法是 reload(),它能重新加载当前显示的页面。调用 reload()而不传参数,页面会以最有效的方式重新加载。也就是说,如果页面自上次请求以来没有修改过,浏览器可能会从缓存中加载页面。如果想强制从服务器重新加载,可以像下面这样给 reload()传个 true:
location.reload(); // 重新加载,可能是从缓存加载
location.reload(true); // 重新加载,从服务器加载
脚本中位于 reload()调用之后的代码可能执行也可能不执行,这取决于网络延迟和系统资源等因素。为此,最好把 reload()作为最后一行代码。
navigator对象
只要浏览器启用 JavaScript,navigator 对象就一定存在。但是与其他 BOM 对象一样,每个浏览器都支持自己的属性。
console.log(navigator)
appCodeName: "Mozilla" //即使在非 Mozilla 浏览器中也会返回"Mozilla"
appName: "Netscape" //浏览器全名
appVersion: "5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Safari/537.36" //浏览器版本。通常与实际的浏览器版本不一致
bluetooth: Bluetooth {}
clipboard: Clipboard {}
connection: NetworkInformation {onchange: null, effectiveType: "4g", rtt: 100, downlink: 2.8, saveData: false}
cookieEnabled: true //返回布尔值,表示是否启用了 cookie
credentials: CredentialsContainer {}
deviceMemory: 8 //返回单位为 GB 的设备内存容量
doNotTrack: null //返回用户的“不跟踪”(do-not-track)设置
geolocation: Geolocation {}
hardwareConcurrency: 8 //返回设备的处理器核心数量
hid: HID {onconnect: null, ondisconnect: null}
keyboard: Keyboard {}
language: "zh-CN" //返回浏览器的主语言
languages: (2) ["zh-CN", "zh"] //返回浏览器偏好的语言数组
locks: LockManager {}
managed: NavigatorManagedData {onmanagedconfigurationchange: null}
maxTouchPoints: 0 //返回设备触摸屏支持的最大触点数
mediaCapabilities: MediaCapabilities {}
mediaDevices: MediaDevices {ondevicechange: null}
mediaSession: MediaSession {metadata: null, playbackState: "none"}
mimeTypes: MimeTypeArray {0: MimeType, 1: MimeType, 2: MimeType, 3: MimeType, application/pdf: MimeType, application/x-google-chrome-pdf: MimeType, application/x-nacl: MimeType, application/x-pnacl: MimeType, length: 4}
onLine: true //返回布尔值,表示浏览器是否联网
permissions: Permissions {}
platform: "Win32" //返回浏览器运行的系统平台
plugins: PluginArray {0: Plugin, 1: Plugin, 2: Plugin, Chrome PDF Plugin: Plugin, Chrome PDF Viewer: Plugin, Native Client: Plugin, length: 3} //返回浏览器安装的插件数组。在 IE 中,这个数组包含页面中所有<embed>元素
presentation: Presentation {defaultRequest: null, receiver: null}
product: "Gecko" //返回产品名称(通常是"Gecko")
productSub: "20030107" //返回产品的额外信息(通常是 Gecko 的版本)
scheduling: Scheduling {}
serial: Serial {onconnect: null, ondisconnect: null}
serviceWorker: ServiceWorkerContainer {controller: null, ready: Promise, oncontrollerchange: null, onmessage: null, onmessageerror: null} //返回用来与 ServiceWorker 实例交互的ServiceWorkerContainer
storage: StorageManager {}
usb: USB {onconnect: null, ondisconnect: null}
userActivation: UserActivation {hasBeenActive: true, isActive: true}
userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Safari/537.36" //返回浏览器的用户代理字符串
userAgentData: NavigatorUAData {brands: Array(3), mobile: false}
vendor: "Google Inc." //返回浏览器的厂商名称
vendorSub: "" //返回浏览器厂商的更多信息
wakeLock: WakeLock {}
webdriver: false //返回浏览器当前是否被自动化程序控制
webkitPersistentStorage: DeprecatedStorageQuota {}
webkitTemporaryStorage: DeprecatedStorageQuota {}
xr: XRSystem {ondevicechange: null}
[[Prototype]]: Navigator
navigator 对象的属性通常用于确定浏览器的类型。
检测插件
检测浏览器是否安装了某个插件是开发中常见的需求。除 IE10 及更低版本外的浏览器,都可以通过 plugins 数组来确定。
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"));
console.log(navigator.plugins)
//PluginArray {0: Plugin, 1: Plugin, 2: Plugin, Chrome PDF Plugin: Plugin, Chrome PDF Viewer: Plugin, Native Client: Plugin, length: 3}
//plugins 数组包含如下属性name,description,filename,length
旧版本 IE 中的插件检测
因为这些浏览器不支持 Netscape 式的插件。在这些 IE中检测插件要使用专有的 ActiveXObject,并尝试实例化特定的插件。
注册处理程序
现代浏览器支持 navigator 上的(在 HTML5 中定义的)registerProtocolHandler()方法。这个方法可以把一个网站注册为处理某种特定类型信息应用程序。随着在线 RSS 阅读器和电子邮件客户端的流行,可以借助这个方法将 Web 应用程序注册为像桌面软件一样的默认应用程序。
screen 对象
这个对象中保存的纯粹是客户端能力信息,也就是浏览器窗口外面的客户端显示器的信息,比如像素宽度和像素高度。
console.log(screen)
availHeight: 824 //屏幕像素高度减去系统组件高度(只读)
availLeft: 0 //没有被系统组件占用的屏幕的最左侧像素(只读)
availTop: 0 //没有被系统组件占用的屏幕的最顶端像素(只读)
availWidth: 1536 //屏幕像素宽度减去系统组件宽度(只读)
colorDepth: 24 //表示屏幕颜色的位数;多数系统是 32(只读)
height: 864 //屏幕像素高度
orientation: ScreenOrientation {angle: 0, type: "landscape-primary", onchange: null}
pixelDepth: 24 //屏幕的位深(只读)
width: 1536 //屏幕像素宽度
[[Prototype]]: Screen
history 对象
history 对象表示当前窗口首次使用以来用户的导航历史记录。因为 history 是 window 的属性,所以每个 window 都有自己的 history 对象。出于安全考虑,这个对象不会暴露用户访问过的 URL,但可以通过它在不知道实际 URL 的情况下前进和后退。
导航
go()方法:在旧版本的一些浏览器中,go()方法的参数也可以是一个字符串,这种情况下浏览器会导航到历史中包含该字符串的第一个位置。最接近的位置可能涉及后退,也可能涉及前进。如果历史记录中没有匹配的项,则这个方法什么也不做(淘汰的方法,用数字正负比较好)
// 后退一页
history.go(-1);
// 前进一页
history.go(1);
// 前进两页
history.go(2);
go()有两个简写方法:back()和 forward()。顾名思义,这两个方法模拟了浏览器的后退按钮和前进按钮:
// 后退一页
history.back();
// 前进一页
history.forward();
history 对象还有一个 length 属性,表示历史记录中有多个条目。这个属性反映了历史记录的数量,包括可以前进和后退的页面。对于窗口或标签页中加载的第一个页面,history.length 等于 1。
历史状态管理
现代 Web 应用程序开发中最难的环节之一就是历史记录管理。为解决这个问题,首先出现的是 hashchange 事件
hashchange 会在页面 URL 的散列变化时被触发,开发者可以在此时执行某些操作。而状态管理API 则可以让开发者改变浏览器 URL 而不会加载新页面。
history.pushState()方法。这个方法接收 3 个参数:一个 state 对象、一个新状态的标题和一个(可选的)相对 URL。例如:
let stateObject = {foo:"bar"};
history.pushState(stateObject, "My title", "baz.html");
//第一个参数应该包含正确初始化页面状态所必需的信息。为防止滥用,这个状态的对象大小是有限制的,通常在 500KB~1MB 以内。
//第二个参数并未被当前实现所使用,因此既可以传一个空字符串也可以传一个短标题。
发现:只是“前进:history.forward()”,"“后退:history.back()”" 返回的是地址栏中的内容,浏览器页不会向服务器发送请求。要刷 / location.href / location.reload等重新请求,才会向服务器请求,采用刷新页面之后,即向服务器请求:‘https://www.baidu.com/baz.html’
因为 pushState()会创建新的历史记录,所以也会相应地启用“后退”按钮。此时单击“后退”按钮,就会触发 window 对象上的 popstate 事件。popstate 事件的事件对象有一个 state 属性,其中包含通过 pushState()第一个参数传入的 state 对象:
window.addEventListener("popstate", (event) => {
let state = event.state;
if (state) { // 第一个页面加载时状态是 null
processState(state);
}
});
基于这个状态,应该把页面重置为状态对象所表示的状态(因为浏览器不会自动为你做这些)。记住,页面初次加载时没有状态。因此点击“后退”按钮直到返回最初页面时,event.state 会为 null。
客户端检测
客户端检测一直是 Web 开发中饱受争议的话题,跨平台的浏览器尽管版本相同,但总会存在不同的问题。这些差异迫使 Web 开发者要么面向最大公约数而设计,要么(更常见地)使用各种方法来检测客户端,以克服或避免这些缺陷。
要检测当前的浏览器有很多方法,每一种都有各自的长处和不足。问题的关键在于知道客户端检测应该是解决问题的最后一个举措。任何时候,只要有更普适的方案可选,都应该毫不犹豫地选择。首先要设计最常用的方案,然后再考虑为特定的浏览器进行补救。
能力检测
一套简单的检测逻辑,测试浏览器是否支持某种特性。
if (object.propertyInQuestion) {
// 使用 object.propertyInQuestion
}
基本思路:检查最常用的方法实现,再检查备用的,所有备选的都不行,抛出错误。
安全能力检测
能力检查能检查对象成员是否存在,但不能确定它就是你想要的。
// 不要这样做!错误的能力检测,只能检测到能力是否存在
function isSortable(object) {
return !!object.sort; //要是sort仅仅只是object上的一个属性也会返回true,让你误以为有这种能力
}
let result = isSortable({ sort: true }); //true,这个true实际是object的属性,而不是他的方法
//所以:更好的方式是检测 sort 是不是函数
优化的isSortable,检测 sort 是不是函数
function isSortable(object) {
return typeof object.sort == "function";
}
let result = isSortable({ sort: true }); //result 为 false
进行能力检测时应该尽量使用 typeof 操作符,但光有它还不够。尤其是某些宿主对象并不保证对typeof 测试返回合理的值。
最有名的例子就是 Internet Explorer(IE)。在多数浏览器中,下面的代码都会在 document.createElement()存在时返回 true,但在ie中document.createElement()函数被实现为 COM 对象,typeof 返回"object"。IE9 对 DOM 方法会返回"function"。这样一来就很多不兼容。
基于能力检测进行浏览器分析
使用能力检测而非用户代理检测的优点在于,伪造用户代理字符串很简单,而伪造能够欺骗能力检测的浏览器特性却很难。
-
检测特性
可以按照能力将浏览器归类。如果你的应用程序需要使用特定的浏览器能力,那么最好集中检测所有能力,而不是等到用的时候再重复检测。// 检测浏览器是否支持 Netscape 式的插件 let hasNSPlugins = !!(navigator.plugins && navigator.plugins.length); // 检测浏览器是否具有 DOM Level 1 能力 let hasDOM1 = !!(document.getElementById && document.createElement && document.getElementsByTagName);
这个例子完成了两项检测:一项是确定浏览器是否支持 Netscape 式的插件,另一项是检测浏览器是否具有 DOM Level 1 能力。保存在变量中的布尔值可以用在后面的条件语句中,这样比重复检测省事多了。
-
检测浏览器
可以根据对浏览器特性的检测并与已知特性对比,确认用户使用的是什么浏览器。这样可以获得比用户代码嗅探(稍后讨论)更准确的结果。但未来的浏览器版本可能不适用于这套方案。
根据不同浏览器独有的行为推断出浏览器的身份。这里故意没有使用 navigator. userAgent 属性,后面会讨论它:
class BrowserDetector {
constructor() {
// 测试条件编译
// IE6~10 支持
this.isIE_Gte6Lte10 = /*@cc_on!@*/false;
// 测试 documentMode
// IE7~11 支持
this.isIE_Gte7Lte11 = !!document.documentMode;
// 测试 StyleMedia 构造函数
// Edge 20 及以上版本支持
this.isEdge_Gte20 = !!window.StyleMedia;
// 测试 Firefox 专有扩展安装 API
// 所有版本的 Firefox 都支持
this.isFirefox_Gte1 = typeof InstallTrigger !== 'undefined';
// 测试 chrome 对象及其 webstore 属性
// Opera 的某些版本有 window.chrome,但没有 window.chrome.webstore
// 所有版本的 Chrome 都支持
this.isChrome_Gte1 = !!window.chrome && !!window.chrome.webstore;
// Safari 早期版本会给构造函数的标签符追加"Constructor"字样,如:
// window.Element.toString(); // [object ElementConstructor]
// Safari 3~9.1 支持
this.isSafari_Gte3Lte9_1 = /constructor/i.test(window.Element);
// 推送通知 API 暴露在 window 对象上
// 使用默认参数值以避免对 undefined 调用 toString()
// Safari 7.1 及以上版本支持
this.isSafari_Gte7_1 =
(({pushNotification = {}} = {}) =>
pushNotification.toString() == '[object SafariRemoteNotification]'
)(window.safari);
// 测试 addons 属性
// Opera 20 及以上版本支持
this.isOpera_Gte20 = !!window.opr && !!window.opr.addons;
}
isIE() { return this.isIE_Gte6Lte10 || this.isIE_Gte7Lte11; }
isEdge() { return this.isEdge_Gte20 && !this.isIE(); }
isFirefox() { return this.isFirefox_Gte1; }
isChrome() { return this.isChrome_Gte1; }
isSafari() { return this.isSafari_Gte3Lte9_1 || this.isSafari_Gte7_1; }
isOpera() { return this.isOpera_Gte20; }
}
这个类暴露的通用浏览器检测方法使用了检测浏览器范围的能力测试。随着浏览器的变迁及发展,可以不断调整底层检测逻辑,但主要的 API 可以保持不变。
- 能力检测的局限
都是假设了该浏览器未来版本继续存在这个属性,并且其他浏览器不会支持,这样一来随着时代发展,检查的方法也会不断更新。用户代理检测
用户代理字符串包含在每个HTTP 请求的头部,在 JavaScript 中可以通过 navigator.userAgent 访问。在服务器端,常见的做法是根据接收到的用户代理字符串确定浏览器并执行相应操作。而在客户端,用户代理检测被认为是不可靠的,只应该在没有其他选项时再考虑。(我发送请求的时候,伪装userAgent这个请求头,那么你检测的时候误以为我是别的浏览器,自己做微信小程序的时候禁止修改userAgent这个属性,想必也是为了安全措施,防止伪装成别的啥的浏览器请求去爬虫或者啥的)
用户代理的历史
就是各个浏览器厂商为了让别人的检测兼容,不断伪装成别的,破环原本的命名规则,然后一步步演练。导致命名很不规范,一直变化。
浏览器分析
相比于能力检测,用户代理检测(window.navigator.userAgent返回的字符串值,所有浏览器都会提供这个值)还是有一定优势的。能力检测可以保证脚本不必理会浏览器而正常执行。现代浏览器用户代理字符串的过去、现在和未来格式都是有章可循的,我们能够利用它们准确识别浏览器。
- 伪造用户代理
只不过实现window.navigator 对象的浏览器(即所有现代浏览器)都会提供 userAgent 这个只读属性。因此,简单地给这个属性设置其他值不会有效。不过,通过简单的办法可以绕过这个限制。比如,有些浏览器提供伪私有的defineGetter方法,利用它可以篡改用户代理字符串:
console.log(window.navigator.userAgent);
// Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36
window.navigator.__defineGetter__('userAgent', () => 'foobar');
console.log(window.navigator.userAgent);
// foobar
即:与其劳心费力检测造假,不如更好地专注于浏览器识别。如果相信浏览器返回的用户代理字符串,那就可以用它来判断浏览器。如果怀疑脚本或浏览器可能篡改这个值,那最好还是使用能力检测。
- 分析浏览器
通过解析浏览器返回的用户代理字符串,可以极其准确地推断出下列相关的环境信息:浏览器 浏览器版本 浏览器渲染引擎 设备类型(桌面/移动) 设备生产商 设备型号 操作系统 操作系统版本,当然,新浏览器、新操作系统和新硬件设备随时可能出现,其中很多可能有着类似但并不相同的用户代理字符串。因此,用户代理解析程序需要与时俱进,频繁更新,以免落伍。自己手写的解析程序如果不及时更新或修订,很容易就过时了。(很不实用)软件与硬件检测
现代浏览器提供了一组与页面执行环境相关的信息,包括浏览器、操作系统、硬件和周边设备信息。这些属性可以通过暴露在 window.navigator 上的一组 API 获得。不过,这些 API 的跨浏览器支持还不够好,远未达到标准化的程度。即:强烈建议在使用这些 API 之前先检测它们是否存在,因为其中多数都不是强制性的,且很多浏览器没有支持。另外,本节介绍的特性有时候不一定可靠。
硬件
浏览器检测硬件的能力相当有限。不过,navigator 对象还是通过一些属性提供了基本信息。
- 处理器核心数
- 设备内存大小
- 最大触点数
总结:客户端检测是 JavaScript 中争议最多的话题之一。因为不同浏览器之间存在差异,所以经常需要根据浏览器的能力来编写不同的代码。主要是:能力检测 和 用户代理检测。
文章评论