【轻聊前端】“一统全局”的BOM

现今各种框架、工具‘横行’,到处在讲原理和源码,更有跨端技术需要我们去探索,但如果基本功不好,学什么都是事倍功半,效果很不好,花费时间的同时打击自信心。此篇文章,为我所计划的【轻聊前端】系列第(十三)篇,旨在系统地、逻辑性地把原生JavaScript知识分享给大家,帮助各位较为轻松地理清知识体系,更好地理解和记忆,我尽力而为,望不负期待。

上一篇,我们结束了DOM的讨论,DOM就是网页本身,它关乎展示渲染,关乎交互,很重要,但再重要的角色,靠自己也不能面面俱到,它需要跟其他角色相互配合,才能完成更多事情,比如,BOM——浏览器对象模型。

DOM的强大在于,你可能不知道眼前的网页能做些什么,而BOM的神奇是,你可能不知道手里的设备能做些什么。DOM主内,BOM主外~

值得说明的是,BOM的强大时常被低估,下面我们一一介绍。

window对象

窗口

虽然计算机的历史并不长,但窗口是个老概念,在IT研发还不是一种普遍职业的时候,接受电脑培训的人每天都会听到一句话——”打开一个新窗口“。

这个概念至今未变,我们打开的浏览器窗口,还是那个窗口。跟窗口相关的操作有:打开、关闭、位置、缩放、移动。

开关和位置

打开——open

open()接收4个参数:

  • URL:要打开的网页地址(http://baidu.com)
  • 目标窗口:可传窗口的名称,将内容打开至一个已有窗口,或是新建一个命名窗口;也可以传_self(自身)、_parent(父窗口)、_top(最外层窗口)或_blank(新的空白窗口)。
  • 特性字符串:一个逗号分隔的字符串,用于指定新窗口包含的特性。(宽、高、位置、大小是否可变、是否显示菜单栏等)
  • 新窗口的替代性:这个替代指的是在浏览器历史记录中,只有在不打开新窗口时才会使用

window.open()方法返回一个对新建窗口的引用,与普通window对象没有区别,只是为控制新窗口提供了方便。

1
window.open('www.baidu.com','newWindow','width=400,height=400,left=10,top=10,resizable=yes')

关闭——close

比如:如果要关闭打开的窗口,可以这样:

1
2
let newWindow = window.open('http://baidu.com','newWindow')
newWindow.close()

window对象的位置可以通过不同的属性和方法来确定。

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

可以使用moveTo()和moveBy()方法移动窗口,使用resizeTo()和resizeBy()方法缩放窗口。这些方法都接收两个参数,一个是改变到多少,一个是改变多少,比较容易理解。

在早些年的网页中,会有一些“鼠标跟随移动、打开页面的漂浮窗口广告”等场景,对于现代Web来说已经很少直接使用窗口了,了解就好,不赘述。

相比之下,另外一个跟窗口有关的概念——“像素比”,就较为常见了。

像素比

什么是像素比?我们知道,CSS当中有像素来作为Web开发的统一单位,但设备也有它自己的像素概念,称为“物理像素”,一个物理像素相当于多少CSS像素呢,window.devicePixelRatio就是用来表示物理像素与逻辑像素之间的缩放系数的。

不同像素密度的屏幕下就会有不同的缩放系数,以便把物理像素转换为CSS像素。

其实还有另一个东西跟它担当着类似角色,就是DPI,单位像素密度,我们在做响应式的时候,有一种不太好的实践是根据屏幕的尺寸设定某种规则,但尺寸的范围是不确定的,很容易顾此失彼,有所遗漏,而使用像素比,就能够较为容易的覆盖到更多设备,同时不用设定多个范围。

1
2
3
@media only screen and (min-device-pixel-ratio: 2) {
/*use CSS to swap out your low res images with high res ones here*/
}

视口

聊DOM的时候,我们提过一个东西——滚动尺寸,也提到一个方法scrollTo(),这个方法,就是和视口相关的。

视口,可以直观理解为可见的网页区域。

文档相对于视口滚动距离的属性有两对,返回同样的值:

window.pageXoffset/window.scrollX

window.pageYoffset/window.scrollY

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

1
2
window.scrollBy(0,100)
window.scrollTo(100,100)

这几个方法也都接收一个ScrollToOptions,可以通过behavior属性告诉浏览器是否平滑滚动,来优化体验。

1
2
3
4
5
window.scrollTo({
left:100,
top:100,
behavior:'smooth'
})

全局

window对象的属性在全局作用域中有效,所以很多浏览器API及相关构造函数都以window对象属性的形式暴露出来。

网页中定义的所有对象、变量和函数都以window作为其Global对象,都可以访问其上定义的全局方法,比如很常见的parseInt(),不需要定义,直接使用即可。

通过var声明的全局变量和函数都会自动成为window对象的属性和方法。

如果使用let或const替代var,则不会把变量添加给全局对象,即ES6之后新增的块级作用域。

定时器

定时器相关的方法有两个:

setTimeout():参数是一个回调函数和一个时间,实现的是一定时间间隔后执行一次回调函数。

setInterval():参数也是一个回调函数和一个时间,实现的是每隔一定时间间隔执行一次回调函数。

二者的区别是,“执行一次”和“执行多次”。

需要注意的是:

1、传入的时间是毫秒。

2、JS是单线程的,为了调度不同代码的执行,JavaScript维护了一个任务队列。其中的任务会按照添加的先后顺序执行。定时器的第二个参数只是告诉JavaScript引擎在指定的毫秒数过后把任务添加到这个队列。如果队列是空的,立即执行,如果队列非空,必须等待前面的任务执行完才能执行。所以这个执行时间并非严格精确,即使传的是0,也不代表立即执行,只是“尽早”执行。

3、传入的回调函数会在全局作用域中的一个匿名函数中运行,因此函数中的this值在非严格模式下指向window,严格模式下是undefined。一般情况下,定时器都是执行跟当前上下文相关的代码(变量),建议使用箭头函数,这样this会保留为定义它时所在的作用域,避免出错。

在过去,定时器是很常用的异步、动画实现方式,现在并不推荐使用,特别是setInterval(),如果忘记清除,在页面卸载之前会一直执行,是无谓的资源占用和消耗。可以使用新的方法替代,比如promise、requestAnimationFrame、CSS帧动画等。

location对象

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

文档信息

听着笼统,但实用,挑重点来说一下。

  • hash:跟在#号后的散列值 (#content)
  • href:当前页面的完整URL
  • search:问号开头的查询字符串 (?username=’idea’)
  • host:服务器名和端口号(www.xxx.com:80)
  • protocol:页面使用的协议(http、https)
  • origin:URL的源地址

有很多熟悉面孔,也好理解,但有两个需要单独拎出来深挖一下。

查询字符串

查询字符串通常用于在当前页面获取相应信息,然后再通过请求拉取需要的数据,我们要拿到它的字段和值,问题是URL中的查询字符串并不易用,虽然location.search返回了从问号开始直到URL末尾的所有内容,但没有办法逐个访问每个查询参数,这时候,就需要一个专门的处理方法。

比如,在百度输入一个关键字“查找”,浏览器地址栏会出现类似如下的查询字符串。

1
//?wd=查找&rsv_spt=1&rsv_iqid=0xcb3e

观察规律:

  • ?开头
  • 不同字符串间‘&’分割
  • 一组字符串的key和value是‘=’分割

这样以来思路就出来了,先截取,再分割。看代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
function QueryString(){
let qs = location.search.length > 0 ? location.search.substring(1):'',
args = {};
const qsList = qs.split('&').map(kv=>kv.split('='))
for(let item of qsList){
let name = decodeURIComponent(item[0]),
value = decodeURIComponent(item[1]);
if(name.length){
args[name] = value
}
}
return args
}

值得一提的是,这会出现在某些公司的面试题中,虽然现在需要手动处理的情况不多,方法还是要了解。

除此之外,其实还有一个原生 API 能够帮我们做到类似的事情——URLSearchParams。是不是很兴奋,因为往往原生能力的升级意味着便捷,没错,URLSearchParams提供了一组标准API方法,通过它们可以检查和修改查询字符串。给URLSearchParams构造函数传入一个查询字符串,就可以创建一个实例。实例上暴露了get()、set()和delete()等方法,可以对查询字符串执行相应操作。

1
2
3
4
5
//?wd=查找&rsv_spt=1&rsv_iqid=0xcb3e
let searchParams = new URLSearchParams()
searchParams.has('wd') //true
searchParams.get('wd') // 查找
searchParams.delete('wd') // ?rsv_spt=1&rsv_iqid=0xcb3e

大多数支持URLSearchParams的浏览器也支持将URLSearchParams的实例用作可迭代对象,其中每一项是查询键值对,这样就可以使用迭代方法很方便地获取它们的值了。

网址导航

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

比如,使用assign()方法,并传入一个URL。

1
location.assign('www.baidu.com')

当然,还有大家更熟悉的:

1
2
location.href = 'www.baidu.com'
window.location = 'www.baidu.com'

这两种方法相当于隐式调用了assign()方法,最常用的是第二种——设置href。

不仅如此,还有很多属性的修改也会造成当前页面的变动,比如:hash、search、hostname、pathname和port等。

其中,除了hash,其他属性变化都会使页面重新加载。这一点很关键哦,现在流行的单页应用框架就是用hash来模拟前进和后退,同时不会因导航变化触发页面刷新。

除了修改属性,还有两个方法也能起到让页面刷新的效果。

replace()

使用前面的方式改变URL,都会增加一条历史记录,如果不希望增加历史记录,可以使用replace()方法,调用replace()之后,用户不能回到前一页。

reload()

最后一个修改地址的方法是reload(),调用reload()而不传参数,页面会以最有效的方式重新加载,有效的意思是,如果页面自上次请求以来没有修改过,浏览器可能会从缓存中加载页面。如果想强制从服务器重新加载,可以给reload()传个true。

这个东西什么时候用呢,比如页面有刷新按钮时,要么发请求重新拉取数据,要么就可以重新加载页面。

脚本中位于reload()之后的代码可能执行也可能不执行,这取决于网络延迟和系统资源等因素。所以最好把reload()作为最后一行代码。

介绍完location,就该另一个“重量级”角色登场了,为什么是“重量级”,因为它承受了太多~

在文章的开头提到过一种描述——‘手里的设备’,在Web端,手里的设备是什么呢,是手机、也可以是浏览器,我们可以称之为“客户端”,navigator对象,就是承载这些客户端信息的。

想象一下,我们是否经常会遇到,检测用户使用的是哪种浏览器、网络情况如何、运行的什么系统等,下面就看看具体内容。

  • appName:浏览器名称
  • userAgent:用户代理字符串
  • platform:浏览器运行的系统平台

    这三个用于获取用户端所运行设备的平台、系统、浏览器等信息,以便做区分处理。

  • connection:返回NetworkInformation对象

  • online:表示浏览器是否联网

这两个用于检测用户侧当前的网络状态,进而进行弱网或者断网情况的处理。

  • serviceWorker:用于跟ServiceWork交互的ServiceWorkContainer

充当Web应用程序、浏览器与网络之间的代理服务器。旨在创建有效的离线体验,它会拦截网络请求并根据网络是否可用来采取适当的动作、更新来自服务器的的资源。它还提供入口以推送通知和访问后台同步 API。

你可以完全控制应用在特定情形(最常见的是网络不可用)下的表现。我们所熟知的PWA,它的核心能力,就是由serviceWorker来提供的。

值得注意的是:Service worker运行在worker上下文,因此它不能访问DOM。另外它是异步的,相对于主JavaScript线程,它运行在其他线程中,所以不会造成阻塞。

  • geolocation:返回Geolocation对象

获取设备地理位置的可编程的对象,获取的是经纬度信息,有两个方法:

getCurrentPosition():确定设备的位置并返回一个携带位置信息的Position对象。

watchPosition():注册一个位置改变监听器,每当位置改变时,返回一个long类型的ID值。

不用说你也能才猜到,在我们使用地图、外卖、打车的时候,这些会派上用场。

  • language:返回浏览器主语言

如果只做特定区域的网页应用,这个属性可能用不到,但如果是国际化产品,就能知道浏览器当前使用的语言类型,然后进行相关特异性设置。

  • battery:返回BatteryManager对象

navigator.getBattery方法返回一个promise对象,提供一个BatteryManager接口,可以从Battery Status API 查询到相关信息。如:设备是否在充电,还要充多久等。

  • mediaDevices:可用的媒体设备

一个重要应用场景就是“音视频”。通常,使用音视频有video和audio就够了,但它们仅仅是简单的应用,对于更复杂的控制和优化就显得无能为力了。

近几年比较热门的直播、短视频、远程会议/面试,使得音视频的需求越来越多,应用较广的一个音视频实时通信标准是WebRTC,但它不是一门独立的技术,而是与浏览器深度绑定的。它很强大,强大的能力需要设备作为支撑,浏览器就是它们的接口。

screen对象

screen在日常编程中较少使用,它存储着浏览器窗口外的客户端显示器的信息,比如屏幕的可用宽高、位置等。

但它有一个属性比较实用,即屏幕朝向——orientation,返回的是类似’0, 90’, 这样的角度。可以得知屏幕旋转了多少度,横屏或者竖屏,你可以发挥想象去做一些有意思的事情。

当然,某些场景下,我们使用CSS媒体查询的orientation设置也能起到类似的效果, 它有两个关键字:portrait(纵向)、landscape(横向),用于判断屏幕朝向,根据需要改变页面布局。

history对象

history对象是本文要介绍的最后一个重要BOM对象。它表示当前窗口首次使用以来的导航历史记录。

因为history是window的属性,所以每个window都有自己的history对象。出于安全考虑,它并不会暴露用户访问过的URL,但可以通过它在不知道实际URL的情况下“前进”和“后退”。

导航

有几个属性和方法来认识一下:

  • length:历史记录的长度,可用于判断用户是否以当前页面作为入口。
  • go():参数可以是一个整数,表示前进或后退多少步。负值后退,正值表示前进。也可以是字符串,导航到最近包含传入字符串的网址。
  • forward():go的便捷替代方法,表前进。
  • back():同上,表后退。

历史状态管理

上面提到的方法都只是在执行某个动作,但动作执行完我们能做什么呢?曾经,这种状态的管理能力是有所缺失的,用户每次点击都会触发页面刷新,“后退”和“前进”按钮对用户来说就代表“切换状态”,现在,这些都已成为历史。在新标准中,这些能力得到了完善。

  • hashchange:
    hashchange会在页面URL的散列值变化时被触发,开发者可以在此时执行某些操作。

  • pushState()

这个方法接收3个参数:一个state对象、一个新状态的标题和一个(可选的)相对URL。

第一个参数应该包含正确初始化页面状态所必需的信息,第二个参数可以是空字符串,也可以是个短标题。

1
2
let stateObj = {foo:'bar'}
history.pushState(stateObj,'my title','baz.html')

执行该方法后,状态信息就会被推到历史记录中,浏览器地址栏也会改变以反映新的相对URL。

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

  • replaceState()

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

状态管理API则可以让开发者改变浏览器URL而不会加载新页面。

tips:使用HTML5状态管理时,要确保通过pushState()创建的每个“假”URL背后都对应着服务器上一个真实的物理URL。否则,单击“刷新”按钮会导致404错误。所有单页应用程序(SPA, Single Page Application)框架都必须通过服务器或客户端的某些配置解决这个问题。

总结

本文的内容着实不少,提到了很多跟BOM相关的能力,很多API,它们当中随便选一个都可以独立成文,细节很多,这就意味着它们在应用中充当着重要角色。

多并不可怕,只要我们知道它属于哪一类,可以用来干什么,应用就不是问题,相互之间联系起来,进行系统性的记忆和理解也就不是难事了。

至此,关于JavaScript的纯代码部分已经进行差不多了,不知道从第一篇一直跟到现在的读者朋友看出是怎样的逻辑了吗?希望后面的文章能够给你带来更多惊喜,欢迎留言交流~