什么是 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上面的示例。
网络状态: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 属性记录了当前位置信息。
最重要的是经纬度,比如我的:
makefile
复制代码
latitude: 22.3193039longitude: 114.16936109999999
现在可以在高德开放平台使用坐标拾取器查询具体的位置了。
输入经纬度,格式是经度加英文逗号加维度。
像高德地图、百度地图、腾讯地图这类地图厂商的 H5 SDK 都是使用这个 API 来完成的。
摄像头和麦克风:WebRTC
上述的几个 API 都是实验性的,通常多用于移动端。
除此之外,还有 WebRTC 技术中需要用到的摄像头/麦克风等功能也属于 Navigator 的范畴。
WebRTC 是 Web Real-Time Communication 的缩写,直译为 Web 实时沟通,通常用于视频聊天、语音聊天之类的场景。
但 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
等同于 location.href。
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 函数都可以正常运行。