什么是 BOM?
BOM 是 browser object model 的缩写,中文意思是浏览器对象模型,提供了一系列操作浏览器的 API。与 ECMAScript 和 DOM 共同组成了 JavaScript。
在 Web 早期,BOM 没有标准规范,完全是由不同的浏览器厂商来实现的,所以不同的浏览器之间可能会存在差异。在 HTML5 规范中加入了 BOM 规范,弥补了这个问题。
BOM 能做什么?
打开新的标签页。
<button id="btn">打开百度首页</button>
<script>
const btn = document.getElementById("btn");
btn.addEventListener("click", () => window.open("https://www.baidu.com"));
</script>
关闭当前标签页。
<button id="btn">关闭页面</button>
<script>
const btn = document.getElementById('btn')
btn.addEventListener('click', () => window.close())
</script>
弹出新的浏览器窗口。
移动窗口。
缩放窗口。
控制用户浏览记录。
获取浏览器详细信息。
获取用户显示器分辨率详细信息。
其他更多功能。
BOM 包含什么?
JavaScript 最初是为 Web 浏览器创建的,但是后来它发展成了多用途和多平台的语言。任何能够提供 JavaScript 运行时的平台都可以称为 JavaScript 主机环境(host)。
目前主流的 JavaScript 平台有 Web 浏览器和可以运行服务端的 Node.js。
任何平台都需要有一个全局对象(Global)/根对象(Root)/宿主对象(Host),用于扩展除了 ECMAScript 语言自身的对象和函数外自己提供的对象和函数。
浏览器环境下的全局对象是 window,它扩展了 DOM 和 BOM。
window 除了具有全局对象含意外,还拥有另一个含意,是“浏览器窗口”。
它的组成如下:
从图中可以看到,BOM 主要包含了 Navigator、Screen、Location、Frames、History 和 XMLHttpRequest 6 个对象。下面逐个讲述它们的用途。
Navigator
Navigator 对象记录了访问者信息。
获取常用属性的示例代码如下:
<div id="info"></div>
<script>
const info = document.getElementById("info")
info.innerHTML += `浏览器应用程序名称: ${navigator.appName} </br>`
info.innerHTML += `浏览器应用程序代码: ${navigator.appCodeName} </br>`
info.innerHTML += `浏览器引擎: ${navigator.product} </br>`
info.innerHTML += `浏览器版本: ${navigator.appVersion} </br>`
info.innerHTML += `浏览器代理: ${navigator.userAgent} </br>`
info.innerHTML += `浏览器平台: ${navigator.platform} </br>`
info.innerHTML += `浏览器语言: ${navigator.language} </br>`
info.innerHTML += `浏览器是否在线: ${navigator.onLine} </br>`
info.innerHTML += `是否启用 cookie: ${navigator.cookieEnabled} </br>`
info.innerHTML += `处理器核心数: ${navigator.hardwareConcurrency} </br>`
</script>
属性
| 属性 | 作用 |
|---|---|
| appName | 浏览器应用程序名称 |
| appCodeName | 浏览器应用程序代码 |
| product | 浏览器引擎 |
| appVersion | 浏览器版本 |
| userAgent | 浏览器代理 |
| platform | 浏览器平台 |
| language | 浏览器语言 |
| onLine | 浏览器是否在线 |
| cookieEnabled | 是否启用 cookie |
| hardwareConcurrency | 处理器核心数 |
Navigator 对象的所有属性都不保证返回值是正确的,因为:
不同浏览器能够使用相同名称 导航数据可被浏览器拥有者更改 某些浏览器会错误标识自身以绕过站点测试 浏览器无法报告发布晚于浏览器的新操作系统
但也不需要过度担心,因为常规用户是不会随意操作这些属性的。
Navigator 上面的属性几乎全部都是只读的。即使你更改了属性的值,也不会有任何作用,当然也不会报错。
navigator.appCodeName = ""
console.log(navigator.appCodeName) // "Mozilla"
其实 navigator 上的大多数属性都是为了兼容老旧的程序,基本上没有什么实际意义。
比如 appName,它的值只有两个,Netscape(网景) 和 Microsoft Internet Explorer(微软 IE)。其中只有低于 11 版本的 IE 浏览器返回 Microsoft Internet Explorer,其他浏览器都是 Netscape。
比较常用的两个属性是 userAgent 和 onLine。
区分移动设备机型:userAgent
userAgent 可以用来识别不同的平台和浏览器。但是由于用户可以修改这个字符串,各类终端设备的不同等原因,用这个属性来识别浏览器不是个好方法,但它可以大致准确的识别出移动设备的机型。如果自己来处理 userAgent 字符串,需要用到大量正则表达式,而且容易出错,总之是一个非常麻烦的过程。比较推荐解析库 ua-parser-js。
它只需要两行代码就可以获取到完整的用户代理信息。
var parser = new UAParser();
console.log(parser.getResult());
监听设备在线离线状态:onLine 的用途
window 提供了 online(上线) 和 offline(离线) 两个事件,可以通过监听这两个事件,并配合 onLine 属性进行判断设备是否离线。
window.addEventListener('online', printNetworkState);
window.addEventListener('offline', printNetworkState);
function printNetworkState() {
console.log(navigator.onLine ? "您的网络恢复了" : "您的网络断开了");
}
调试网络状态可以在浏览器的控制台中选择 Network 面板,点击 WIFI 图标。
这样就进入了 Network conditions 面板,点击 Network throttling,可以选择不同的网络状态。
除了上述的属性外,还有几个属性是在 HTML5 中新添加的,在移动开发中比较有实用价值,比如 getBattery(电池状态)、connection(网络连接状态)和 geolocation(地理位置信息)等。
电池状态:getBattery
该 API 可以获取电池状态,从而做一些你想做的事情,比如在电量较低时减少资源消耗,在电量将要耗尽时存储数据,防止数据丢失等。
getBattery 会返回一个 BatteryManager 对象。
属性
| 属性 | 作用 |
|---|---|
| charging | 布尔值,表示电池当前是否在充电。 |
| chargingTime | Number,表示电池充电前地剩余时间,单位为秒,如果电池已经充满电,则为 0。 |
| dischargingTime | Number,表示电池完全放电和系统暂停前剩余时间,单位为秒。 |
| level | Number,表示电池充电水平,比例为0.0-1.0之间。 |
事件
| 事件 | 作用 |
|---|---|
| onchargingchange | 电池充电状态更新时触发此事件。 |
| onchargingtimechange | 电池充电时间更新时触发此事件。 |
| ondischargingtimechange | 电池放电时间更新时触发此事件。 |
| onlevelchange | 电池电量更新时触发此事件。 |
示例:
navigator.getBattery().then(function(battery) {
function updateAllBatteryInfo(){
updateChargeInfo();
updateLevelInfo();
updateChargingInfo();
updateDischargingInfo();
}
updateAllBatteryInfo();
battery.addEventListener('chargingchange', updateChargeInfo);
function updateChargeInfo(){
console.log(`Battery charging? ${battery.charging ? "Yes" : "No"}`);
}
battery.addEventListener('levelchange', updateLevelInfo);
function updateLevelInfo(){
console.log(`Battery level: ${battery.level * 100}%`);
}
battery.addEventListener('chargingtimechange', updateChargingInfo);
function updateChargingInfo(){
console.log(`Battery charging time: ${battery.chargingTime} seconds`);
}
battery.addEventListener('dischargingtimechange', updateDischargingInfo);
function updateDischargingInfo(){
console.log(`Battery discharging time: ${battery.dischargingTime} seconds`);
}
});
下面是两个在 codepen上面的示例。
充电 Demo 1
充电 Demo 2
网络状态:connection
connection 可以获取系统网络状态,来通过判断网络是蜂窝网还是 WiFi 来提醒用户流量的损耗。还可以通过区分 3G/4G/5G 来提供不同质量的内容。相比于 onLoad 属性,connection 拥有更细粒度的网络状态监控。
示例:
const connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection;
function printConnectionStatus(e) {
console.log(e.target);
}
connection.addEventListener('change', printConnectionStatus);
e.target 是当前的网络状态对象, 它包含如下几个属性。
| 属性 | 类型 | 作用 |
|---|---|---|
| downlink | number | 网络下行速度,单位 M/s |
| effectiveType | string | 网络类型,常见的值有 4g/3g/2g 等。 |
| onchange | null | 有值代表网络状态变化,默认为 null |
| rtt | number | 往返时间(round-trip time)的缩写,单位是毫秒,表示从发送端发送数据开始,到发送端收到来自接收端的确认(接收端收到数据后便立即发送确认,不包含数据传输时间)总共经历的时间。 |
| saveData | boolean | 打开/请求数据保护模式 |
地理位置:geolocation
这个 API 只能在 https 协议下才可以使用,它可以获取地理位置。
但这个 API 的位置偏移较为严重,容易产生定位不精准的问题。
示例:
const options = {
enableHighAccuracy: true,// 高精确度
timeout: 5 * 1000,// 等待响应的最长时间 单位:毫秒
maximumAge: 0// 应用程序愿意接受的缓存位置的最长时间
}
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(console.log, console.log, options)
} else {
console.log('Geolocation is not supported in your browser')
}
在任意一个 https 协议的网页控制台中运行上面这段脚本,会在左上角弹出一个地址授权提示框。
点击允许就完成了授权,这时右侧就会出现一个地址的图标。
如果浏览器自身没有访问位置的权限,需要在操作系统设置中对其授权,比如在 Mac 系统下。
完成授权后可以获得一个 GeolocationPosition 对象,这个对象的 coords 属性记录了当前位置信息。
最重要的是经纬度,比如我的:
latitude: 22.3193039
longitude: 114.16936109999999
现在可以在高德开放平台使用坐标拾取器查询具体的位置了。
输入经纬度,格式是经度加英文逗号加维度。
像高德地图、百度地图、腾讯地图这类地图厂商的 H5 SDK 都是使用这个 API 来完成的。
摄像头和麦克风:WebRTC
上述的几个 API 都是实验性的,通常多用于移动端。
除此之外,还有 WebRTC 技术中需要用到的摄像头/麦克风等功能也属于 Navigator 的范畴。
WebRTC 是 Web Real-Time Communication 的缩写,直译为 Web 实时沟通,通常用于视频聊天、语音聊天之类的场景。
但 WebRTC 的内容相对较多,不在这节课中详述,下面是一个 Demo,有兴趣可以自行学习。
WebRTC Demo
Screen
screen 表示当前窗口所在的屏幕,提供了显示设备的信息。
相比较 Navigator 而言,Screen 则简单很多。
它提供了如下属性:
| 属性 | 作用 |
|---|---|
| height | Number,屏幕高度,单位像素。通常是常量,缩放网页不会发生变化。但调整屏幕分辨率会影响它。 |
| width | 屏幕宽度,其他特性同 height。 |
| availHeight | Number,屏幕可用高度,单位像素。即屏幕总高度减掉系统的任务栏或者其它系统组件的高度。 |
| availWidth | 屏幕宽度,其他特性同 availHeight。 |
| pixelDepth | 屏幕色彩位数,比如 24 表示 24 位色彩。 |
| colorDepth | 表示颜色深度。和 pixelDepth 的区别是 colorDepth 表示应用程序的颜色深度,pixelDepth 表示屏幕的颜色深度。绝大多数情况下是一样的。 |
| orientation | 表示屏幕方向,是个对象。 |
| orientation.angle | Number,屏幕旋转角度。 |
| orientation.type | String,屏幕方向,landscape-primary 横放、landscape-secondary 颠倒的横放、portrait-primary 竖放。 |
Screen 最常见的用途是根据用户设备的不同,提供不同的资源。
比如根据不同分辨率提供两套 html:
if (screen.width > 1920 && screen.height > 1080) {
location.replace('max.html')
} else if (screen.width < 1280 && screen.height < 720) {
location.replace('min.html')
}
Location
记录当前 URL 信息,并提供了一系列对当前 URL 的操作方法。
属性
| 属性 | 作用 |
|---|---|
| href | 整个 URL。 |
| protocol | 当前 URL 的协议,包括冒号(:)。 |
| host | 主机。如果端口不是协议默认的80和433,则还会包括冒号(:)和端口。 |
| hostname | 主机名,不包括端口。 |
| port | 端口号。 |
| pathname | URL 的路径部分,从根路径/开始。 |
| search | 查询字符串部分,从问号?开始。 |
| hash | 片段字符串部分,从#开始。 |
| username | 域名前面的用户名。 |
| password | 域名前面的密码。 |
| origin | URL 的协议、主机名和端口。 |
这些属性中只有 origin 为只读属性,其它属性均为可写。
当你一旦修改了 location 的属性,页面会立即发生变化。比如你修改了 href,页面会立即触发跳转。
跳转锚点
最常用的用法是通过设置 hash,让页面自动滚动到对应的锚点位置。
只要设置过 id 的元素,都可以称为锚点(anchor)。
示例(2 秒后自动跳转到锚点 d3 的位置):
<div id="d1">1</div>
<div id="d2">2</div>
<div id="d3">3</div>
<div id="d4">4</div>
<div id="d5">5</div>
<div id="d6">6</div>
<style>
[id^="d"] {
height: 400px;
text-align: center;
line-height: 400px;
font-size: 24px;
font-weight: 600;
color: #fff;
}
#d1 {
background: #d9e3f0;
}
#d2 {
background: #f47373;
}
#d3 {
background: #697689;
}
#d4 {
background: #37d67a;
}
#d5 {
background: #2ccce4;
}
#d6 {
background: #555555;
}
</style>
<script>
setTimeout(() => {
location.href = "#d3";
}, 2000);
</script>
方法
跳转页面 assign
location.assign('http://www.baidu.com')
如果参数不是一个合法的 URL,不会跳转页面,而是会抛出异常。
替换当前页面 replace
location.replace('http://www.baidu.com')
replace 和 assign 的区别在于,replace 不会存储上一个页面浏览历史,而是用新的 URL 替换掉老的 URL,也就是说意味着页面无法再回退。
重新加载页面 reload
location.reload()
reload 的作用等价于点击浏览器的刷新按钮。
reload 可以传递一个 boolean 类型的参数。
当设置为 true 时会强制从服务器重新请求资源。如果设置为 false,会使用浏览器缓存。默认为 false。
转换为字符串 toString
URL 和 URLSearchParams 对象
- href:完整的 URL。
- protocol:协议,以 : 结尾。常见的有 http 和 https。
- hostname:域名,和协议以 // 分割,和端口以 : 分割。
- port:端口,通常会省略掉,默认为 80。
- pathname:路径,表示网站中的某个资源。
- search:查询参数,以 ? 开头,彼此之间以 & 拼接。
- hash:锚点,以 # 开头。
BOM 提供了 URL 和 URL SearchParams 对象来帮助我们更容易地操作 URL。
虽然大多数情况下我们只需要使用字符串类型的 URL 就足够了,但在某些时候 URL 对象非常有用。
创建 URL 对象
需要使用 new 关键字创建,它有两个参数,第一个是 url,或者是一个路径,必选;第二个是基础路径,可选。
比如百度的URL:
new URL('https://www.baidu.com')
加上 base 的情况:
new URL('/s?ie=UTF-8&wd=url', 'https://www.baidu.com')
url 对象可以在很多情况下替换 url 字符串,因为它会进行隐式转换。
比如:
location.href = new URL('/s?ie=UTF-8&wd=url', 'https://www.baidu.com')
解析 URL 字符串
如果你拥有一个 url 字符串,也可以通过 URL 对它进行解析。
const urlStr = "https://www.baidu.com/s?ie=UTF-8&wd=url"
const url = new URL(urlStr)
console.log(url.protocol)// https:
console.log(url.host)// "www.baidu.com"
console.log(url.search)// "?ie=UTF-8&wd=url"
编码
在 URL 中,很多特殊的字符串是不合法的,比如中文和空格等。
合法符号如下:
- URL 元字符:分号(
;),逗号(,),斜杠(/),问号(?),冒号(:),at(@),&,等号(=),加号(+),美元符号($),井号(#) - 语义字符:
a-z,A-Z,0-9,连词号(-),下划线(_),点(.),感叹号(!),波浪线(~),星号(*),单引号('),圆括号(())
除了这两类,其它所有字符都是不合法的。如果你要在字符串中包含不合法字符,都必须转义,规则是根据操作系统的默认编码,将每个字节转为百分号(%)加上两个大写的十六进制字母。而一个中文是三个字节,所以会被转换为 6 个转义字符。
在 RFC3986 中规定了哪些字符应该被转义。
比如你在地址栏输入:
https://www.baidu.com/s?wd=前端
当你拷贝下来之后会发现变成了下面这个格式:www.baidu.com/s?wd=%E5%89…
URL 中前端两个字被转义成了 E5 89 8D 和 E7 AB AF 这 6 个 UTF8 字节码。
字符转义函数
上面这种情况是浏览器自动帮我们做了转义,但如果我们在 JS 中有一个字符串,是未转义的,那么就需要我们自己来转义。
比如你有一个 url 字符串。
const urlStr = "https://www.baidu.com/s?ws=前端"
你想跳转到对应的网页。
location.href = urlStr
发现会失败,原因就是前端两个汉字没有经过转义。
JavaScript 提供四个 URL 的编码/解码方法。
encodeURI
encodeURI 方法用于编码整个 URL。它的参数是一个字符串,代表整个 URL。它会将元字符和语义字符之外的字符,都进行转义。
encodeURI(urlStr)
// "https://www.baidu.com/s?ws=%E5%89%8D%E7%AB%AF"
encodeURIComponent
encodeURIComponent 方法用于转码 URL 的组成部分,会转码除了语义字符之外的所有字符,即元字符也会被转码。所以,**它不能用于转码整个 URL。**它接受一个参数,就是 URL 的片段。
encodeURIComponent('前端')
// "%E5%89%8D%E7%AB%AF"
如果你将整个 URL 进行转义,是不可以的。
encodeURIComponent(urlStr)
// "https%3A%2F%2Fwww.baidu.com%2Fs%3Fws%3D%E5%89%8D%E7%AB%AF"
上面代码中,encodeURIComponent 会连 URL 元字符一起转义,所以如果转码整个 URL 就会出现问题。
decodeURI
decodeURI 方法用于整个 URL 的解码。它是 encodeURI 方法的逆运算。它接受一个参数,就是转码后的 URL。
const urlEncode = encodeURI(urlStr)
// "https://www.baidu.com/s?ws=%E5%89%8D%E7%AB%AF"
const urlDecode = decodeURI(urlEncode)
// "https://www.baidu.com/s?ws=前端"
decodeURIComponent
decodeURIComponent 用于对 URL 片段进行解码。它是 encodeURIComponent 方法的逆运算。它接受一个参数,就是转码后的 URL 片段。
decodeURIComponent('%E5%89%8D%E7%AB%AF')
// "前端"
URLSearchParams对象
search 字符串是一段并不是很好处理的文本。
比如我有一个如下 URL:
https://www.baidu.com/s?wd=url&ie=utf-8
它的查询参数部分如下:
wd=url&ie=utf-8
这段查询参数,我们如果需要替换 ie 的值或者 wd 的值都需要正则匹配或者使用类似的麻烦技术。(在我接触 URLSearchParams 之前就是用这种方式处理的。)
如果我们能够将这段文本通过类似对象的方式操作是最方便的。URLSearchParams 就提供了这种方式。
let url = new URL('https://www.baidu.com/s?wd=url&ie=utf-8');
url.searchParams.set('wd', 'searchparams');
console.log(url);
除了 set 方法之外,URLSearchParams 还提供了更多的方法,你可以逐一尝试。
| 方法 | 作用 |
|---|---|
| append | 添加新的查询参数 |
| delete | 删除查询参数 |
| get | 获取某个查询参数 |
| getAll | 获取所有查询参数。因为 wd=url&wd=uri 是合法的,所以它会返回一个数组。 |
| has | 检查某个查询参数是否存在。 |
| set | 修改某个查询参数。 |
| sort | 对参数进行排序,很少会用到。 |
encode 函数与 URL/URLSearchParams
encode 函数和 URL/URLSearchParams 对象之间的区别就是,encode 函数基于 RFC2396,这是一个过时的 URL 规范。而 URL 和 URLSearchParams 是基于最新的 RFC3986。
在处理 IPV6 时,encode 就会出现问题。不过这种情况很少见,绝大多数情况下,encode 函数都可以正常运行。