BOM(Browser Object Model)
window对象
BOM的核心是window对象,表示浏览器实例
window对象在浏览器中有两重身份,一个是es中的global对象,另一个是浏览器窗口的js接口
网页中定义的所有对象、变量和函数都以window作为其Global对象,都可以访问其上定义的全局方法
Global作用域
所有通过var声明的全局变量和函数都会称为window对象的属性和方法
窗口关系
top:始终指向最上层窗口(最外层),即浏览器窗口本身
parent:始终指向当前窗口的父窗口
self:始终指向window,实际上self和window就是同一个对象
window.top、window.parent、window.self
窗口位置与像素比
screenTop、screenLeft可以确定窗口相对于屏幕的位置,返回像素
可以使用moveTo(x, y)和moveBy(x, y)方法移动窗口,前者接收要移动到位置的绝对坐标,后者接收要移动的像素
像素比
window.devicePixelRatio表示物理像素与逻辑像素之间的缩放系数
窗口大小
window.outerWidth/outerHeight
返回浏览器窗口自身大小(不管是在最外层window上使用还是对<frame>使用)
window.innerWidth/innerHeight
IE8以及IE8以下不兼容
返回浏览器窗口中页面视口的大小
document.documentElement.clientWidth/clientHeight
标准模式下正常使用,任意浏览器都兼容
返回页面视口的宽高
document.body.clientWidth/clienHeight
怪异模式
浏览器窗口的精确尺寸不好确定,但是可以确定页面视口大小
封装getViewportOffset方法
document.compatMode返回当前浏览器模式:BackCompat(向后兼容)、CSS1Compat(标准模式)
function getViewportOffset() {
if (window.innerWidth) {
return {
w: window.innerWidth,
h: window.innerHeight
}
} else {
if (document.compatMode === "BackCompat") {
return {
w: document.body.clientWidth,
h: document.body.clientHeight
}
} else {
return {
w: document.documentElement.clientWidth,
h: document.documentElemnet.clientHeight
}
}
}
}
resizeTo()、resizeBy()
调整窗口大小,这两个方法都接受两个参数
前一个方法表示要缩放到的宽高,后者表示要缩放的宽高
视口位置
window.pageXOffset(scrollX)/pageYOffset(scrollY)
查看滚动条的滚动距离,IE8以及以下不兼容
document.body.scrollLeft/scrollTop、document.documentElement.scrollLeft/scrollTop
兼容IE8以及IE8以下的浏览器,但是这两个属性兼容性混乱,使用的时候将俩值相加,因为不会存在俩值同时存在
封装getScrollOffset方法
function getScrollOffset() {
if (window.pageXOffset) {
return {
x: window.pageXOffset,
y: window,pageYOffset
}
} else {
return {
x: document.body.scrollLeft + document.documentElement.scrollLeft,
y: document.body.scrollTop + document.documentElement.scrollTop
}
}
}
让滚动条滚动
1、window.scroll()
2、window.scrollTo()
3、window.scrollBy()
前两个方法类似,都是传入x,y坐标,使滚动条滚动到当前位置;第三个会在之前的数据上累加x,y
这几个方法也接收一个ScrollToOptions字典,除了提供偏移值,还可以通过behavior属性告诉浏览器是否平滑移动
window.scroll({
left: 100,
top: 100,
behavior: 'auto'
});
//平滑滚动
window.scroll({
left: 100,
top: 100,
behavior: 'smooth'
});
导航与打开新窗口
window.open()
该方法可以用于导航到指定的URL,一可以用于打开新窗口
该方法接收四个参数:要加载的URL、目标窗口、特性字符串、表示新窗口在浏览器历史记录中是否替代当前加载页面的布尔值
通常只用前三个参数,最后一个参数只有在不打开新窗口时才会使用
如果第二个参数是一个已经存在的窗口或窗格(frame)的名字,则会在对应的窗口或窗格中打开URL
弹出窗口
如果window.open第二个参数不是已有窗口,则会打开一个新窗口或标签页,第三个参数即特性字符串用于指定新窗口的配置,如果不传,则新窗口默认为所有浏览器的默认特性;如果打开的不是新窗口则忽略第三个参数
特性字符串是逗号分隔的设置字符串,指定新窗口的特性,有很多键值:
**yes/no:**fullscreen(新窗口是否最大化,仅IE支持)、location(是否显示地址栏)、Menubar(是否显示菜单栏)、resizable(是否允许拖动改变新窗口大小)、scrollbars(是否在内容过长时滚动)、status(是否显示状态栏)、toolbar(是否显示工具栏)
一般默认属性都为no
**数值:**height(不能小于100)、left(不能是负值)、top(不能是负值)、width(不能小于100)
window.open返回一个新建窗口的引用,与普通window对象没有区别,方便控制新窗口
还可以通过xinWindow.close()来关闭新窗口,但是关闭后只能检查xinWindow的closed属性了
let xinWindow = window.open(...);xinWindow.close();alert(xinWindow.closed); //true
创建新窗口的window对象有一个opener属性,指向打开它的窗口,这个属性只在弹出窗口的最上层window对象(top)有定义
可以将新打开的标签页的opener设置为null,这样就切断了当前页与标签页的通讯,表示新标签页可以独立运行,但是这样切断后就不能再恢复了
安全限制
各种浏览器对弹出窗口进行了诸多限制,限制不一
弹窗屏蔽程序
大多数浏览器都有内置的弹窗屏蔽程序,要检测弹窗是否被屏蔽,可以通过window.open的返回值和try/catch(因为扩展程序和其他程序屏蔽弹窗时,window.open通常会报错)来实现
let blocked = false;
try {
let xinWindow = window.open(...);
if (xinWindow == null) {
blocked = true;
}
} catch(e) {
blocked = true;
}
if (blocked == true) {
alert('the popup is blocked');
}
定时器
前文es中有涉及,所以这里不涉及使用方法
因为js是单线程的,所以每次只能执行一段代码,为了调度不同代码执行,js维护了一个任务队列,其中的任务会按照添加到队列中的先后顺序执行
setTimeout的第二个参数只是告诉js引擎在指定毫秒数后把任务添加到这个队列,但是不保证立即执行,因为此时队列不一定是空的;所以才有了上文说的那种时间不准的情况
setInterval的第二个参数只是告诉js引擎在循环的指定毫秒数后把任务添加到这个队列,但是不保证立即执行,因为此时队列不一定是空的;所以才有了上文说的那种时间不准的情况
setTimeout会返回一个超时排期的数值ID、setInterval会返回一个循环定时ID,这两个ID都能用于取消相关任务
系统对话框
alert、confirm、promt方法,可以调用浏览器系统对话框
alert()——警告框:只接收一个参数,一般是字符串,如果不是则调用toString方法将其转换为字符串
confirm()——确认框:与警告框接收参数一样,但是可以通过其返回值判断用户是点击了确认按钮还是取消按钮
if (confirm("are you sure?")) {} else {}
promt()——提示框:接收俩个参数,第一个是展示给用户的文本,第二个是文本输入框的默认值(可以是空字符串);如果用户点击了确认按钮,则返回输入框内的值,如果点击了取消按钮则返回null
print()——打印对话框:无返回值,通过在window对象上调用;异步,所以不会阻塞代码执行,但是代码也不能知道用户的相关操作,用户禁用弹框对其没有影响
find()——查找对话框:无返回值,通过在window对象上调用;异步,所以不会阻塞代码执行,但是代码也不能知道用户的相关操作,用户禁用弹框对其没有影响
location对象
location对象是BOM最有用的对象之一,提供了当前窗口中加载文档信息,以及通常的导航功能
它既是window的属性也是document的属性,window.location和document.location指向同一个location
它还储存了URL解析后的信息
假设URL是:http://foouser:barpassword@www.wrox.com:80/WileyCDA/?q=javascript#contents
属性 | 值 | 说明 |
---|---|---|
location.hash | “#contents” | URL散列值(井号后面跟0或者多个字符),如果没有则为空字符串 |
location.host | “www.wrox.com:80” | 服务器名以及端口号 |
location.hostname | “www.wrox.com” | 服务器名 |
location.href | “http://foouser:barpassword@www.wrox.com:80/WileyCDA/?q=javascript#contents” | 当前页面完整的URL(location的toString) |
location.pathname | “/WileyCDA/” | URL中的路径和(或)文件名 |
location.port | “80” | 请求的端口,如果没有则返回空字符串 |
location.protocol | “http:” | 页面使用的协议 |
location.search | “?q=javascript” | URL的查询字符串,以问号开头 |
location.username | “foouser” | 域名前指定的用户名 |
location.password | “barpassword” | 域名前指定的密码 |
location.origin | “http://www.wrox.com” | URL的源地址(只读) |
查询字符串
URLSearchParams提供了一组标准API方法,通过它们可以检查和修改字符串
向URLSearchParams构造函数传入一个查询字符串,就可以创建一个实例,这个实例暴露了get()、set()、delete()方法,可以对查询字符串执行相应的操作
大多数实现了URLSearchParams的浏览器也支持迭代其实例
let params = new URLSearchParams("?q=javascript&num=10");params.toString(); //"q=javascript&num=10"params.has("num"); //trueparams.get("num"); //10params.set("page", "3");params.toString(); //"q=javascript&num=10&page=3"params.delete('q');for (let param of params) { console.log(param);}//["num", "10"]//["page", "3"]
操作地址
可以通过location对象修改浏览器地址,最常用的方法是assign()方法
location.assign("http://www.wrox.com");
它会立即启动导航到新URL操作,同时在浏览器历史记录中增加一条记录,如果给location.href和window.location设置一个URL也会以这个URL调用assign方法
修改其他location属性(除了hash值)都会导致URL改变,而且页面会重新加载
location.replace()方法将不会增加历史记录,也就是页面刷新后不能返回前一个页面了
location.reload()方法会重新加载当前页面;如果不传参数,则页面会以最有效的方式重新加载,比如直接从缓存中读取(从上次加载后就未修改过);如果想从服务器重新加载,可以传一个true给reload()函数(脚本中位于reload方法后的代码可能执行也可能不执行,这取决于网络延迟和系统资源的问题)
navigator
navigator最早是由Netscape Navigator2引入的,现在已经成为客户端标识浏览器的标准
只要启动浏览器JavaScript,navigator对象就一直存在
navigator属性方法有很多,详情参见红宝书p375
检测插件
除IE10以及更低版本外的浏览器,都可以通过plugins数组来确定浏览器是否安装了插件
数组中的每一项都包含几个属性:name、description、filename、length(由当前插件处理的MIME类型数量)
其实每个数组对象中还有一个MineType属性,只能通过中括号访问,每个MineType有四个属性:description(描述MIME类型)、enabledPlugin(指向插件对象的指针)、suffixes(该MIME类型对应扩展名的逗号分隔的字符串)、type(完整的MIME类型字符串)
let hasPlugin = function(name) { name = name.toLowerCase(); for (let plugin of window.navigator.navigator.plugins) { if (plugin.name.toLowerCase().indexOf(name) > -1) { return true; } } return false;}hasPlugin("Flash"); //true//旧版本IE检测方式function hasIEPlugin(name) { try { new ActiveXObject(name); return true; } catch(e) { return false; }}hasIEPlugin("QuickTime.QuickTime");
plugins还有一个refresh()方法用于刷新plugins属性以反映新安装的插件,该方法接受一个布尔值,表示是否重新加载页面;如果传入true则所有包含插件的页面都要重新加载;否则,只有plugins会更新,但页面不会重新加载
注册处理程序
现代浏览器支持navigator上的registerProtocolHandler()方法,这个方法可以把一个网站注册为处理某种特定类型信息应用程序
可以借助这个方法将Web应用程序注册为像桌面软件一样的默认应用程序
该方法接收三个参数:要处理的协议(如“mailto”)、处理该协议的url、应用名称
screen对象
不常用的对象,这个对象中保存的是客户端能力信息,也就是了浏览器窗口外客户端显示器信息,例如像素宽度和像素高度
每个浏览器都会在screen对象上暴露不同属性
详情请参阅红宝书p379
history对象
表示当前窗口首次使用以来用户的导航记录,每个window都有自己的history对象
history对象不会暴露url,只能前进或后退
导航
go()方法可以向任何方向导航,向前或向后都行,该方法接收一个参数,正数表示在历史记录中向前,负数表示在历史记录中向后,如果是字符串则导航到最近的url包含该字符串的位置
forward()、back()模拟浏览器前进和后退按钮
length:表示历史记录中有多少条目(2009年以来发布的主流浏览器,改变URL散列值也会增加一条记录)
历史状态管理
history.pushState()方法,这个方法接收三个参数:state对象(只包含可被序列化的信息)、新状态标题、相对URL(可选)
history.pushState({foo:'bar'}, "title", "baz.html");
state对象大小有限制:500KB-1MB以内
触发pushState会创建新的历史记录,所以会相应的启用”后退“按钮,如果点击就会触发window对象上的popstate事件,该事件对象有一个state属性,包含pushState传入的第一个参数
window.addEventListener("popstate", (event) => { let state = event.state; if (state) {//第一个页面加载时状态是null processState(state); }})
同样可以用history.replaceState()方法来更新状态,该方法接收两个参数,这两个参数和pushState方法前两个参数一样
该方法不会创建新的历史记录,只会覆盖当前状态
客户端检测
能力检测
能力检测又称为特性检测,即在js运行时使用一套简单的逻辑,测试浏览器是否支持某种特性
if (object.propertyInQuestion) { //使用object.propertyInQuestion}
这种方法应该先检测通用方法,再对个别方法进行检测
安全能力检测
因为有可能某个对象的属性名正好是所需要检测的属性名,但是它又不是我们真正需要的属性,只是名字重复而已,这时能力检测就拉垮了
这时我们需要检测该属性的类型以便进一步确认
function isSortable(obj) { return typeof obj.sort == "function";}
但是IE又有问题了,因为在IE8以及更低版本里,DOM是通过COM实现的,所以:
function isCreateElement(obj) { return typeof obj.createElement == "function";}//这个在IE8以及更低版本里typeof obj.createElement返回object,因为它被实现为COM对象
基于能力检测进行浏览器分析
使用能力检测可以精准的分析运行代码的浏览器,而且伪造用户代理字符串很简单,但是伪造能够欺骗能力检测的浏览器特性却很难
检测特性
最好集中检测所有能力,而不是等到要用的时候再重复检测,例如:
//检测浏览器是否支持Netscape式的插件let hasNSPlugins = !!(navigator.plugins && navigator.plugins.length);
检测浏览器
根据浏览器特性的检测并与已知特性对比,确认浏览器;这套方法可能在未来浏览器版本中不适用
class BrowserDetector { constructor() { //测试条件编译,IE6-10支持 this.isIE_Gte6Lte10 = /*@cc_on!*/false; //测试documentMode,IE7-11支持 this.isIE_Gte7Lte11 = !!document.documentMode; //测试StyleMedia构造函数,Edge20及以上版本支持 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”字样,Safari3-9.1支持 this.isSafari_Gte3Lte9_1 = /Constructor/i.test(window.Element); //推送通知API暴露在window对象上,Safari7.1及以上版本支持 this.isSafari_Gte7_1 = (({pushNotification = {}} = {}) => pushNotification.toString() == '[object SafariRemoteNotification]')(window.safari); //测试addons属性,Opera20及以上版本支持 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}}
随着浏览器变迁可以不断调整底层逻辑
能力检测的局限
能力检测往往不具备全面性,可能这个浏览器的特性功能别的浏览器也实现了
能力检测适合决定下一步怎么做,而不能用来作为决定浏览器的标识
用户代理检测
用户代理检测通过浏览器用户代理字符串确定使用什么浏览器,用户代理字符串包含在每个HTTP请求的头部,在js中可以通过navigator.userAgent访问
在很长一段时间里,浏览器都通过在用户代理字符串中错误的或误导信息来欺骗服务器,这背后的原因需要了解自Web出现后用户代理字符串的历史(红宝书p386-p392)
浏览器分析
毕竟用户代理是可以伪造的,有些浏览器提供了某些私有方法,对付这种造假是吃力不讨好的事
我们从用户代理可以获取相关环境信息:浏览器、浏览器版本、渲染引擎、设备类型…
GitHub上有相关第三方用户代理解析程序,这里不建议自己手写
软件与硬件检测
现代浏览器提供了一组与页面执行环境相关的信息,这些属性可以通过暴露在window.navigator上的一组API获得,但是这些API跨浏览器支持不是很好
强烈建议在使用前先检测相关它们是否存在
识别浏览器与操作系统
navigator.oscpu
返回操作系统/系统架构信息字符串
navigator.vendor
返回浏览器开发商信息字符串
navigator.platform
返回操作系统字符串
screen.colorDepth、pixelDepth
返回显示器每像素的位深
screen.orientation
返回ScreenOrientation对象,其中包含Screen Orientation API定义的屏幕信息,最有意思的属性是angle和type,前者返回默认状态下屏幕的角度,后者返回四值之一:portrait-primary、portrait-secondary、landscape-primary、landscape-secondary
浏览器元数据
Geolocation API
navigator.geolocation属性暴露了Geolocation API,可以让浏览器脚本感知当前设备的地理位置,这个API只在安全执行环境(通过HTTPS获取的脚本)中可用
这个API可以查询宿主系统并尽可能地精确的返回设备的位置信息;根据宿主系统的硬件和配置,返回结果的精度可能不一样
根据Geolocation API规范:地理位置信息主要来源是GPS和IP地址、频射识别(RFID)、WIFI及蓝牙Mac地址、GSM/CDMA蜂窝ID以及用户输入等信息
可以使用getCurrentPosition()方法获取浏览器当前的位置,这个方法返回一个coordinates对象,其中包含的信息不一定依赖宿主系统的能力:
navigator.geolocation.getCurrentPosition((position) => p = position);p.timestamp; //查询时间的时间戳p.coords; //Coordinates {...}p.coords.latitude; p.coords.longitude; p.coords.accuracy; //经度,纬度,精度(单位:m)p.coords.altitude; p.coords.altitudeAccuracy; //海拔,精度(单位:m)[需要GPS或高度计]p.coords.speed; p.coords.heading; //每秒移动速度,相对于正北方向移动的角度(0<=heading<360)[需要加速计或指南针]
获取位置数据不一定成功,getCurrentPosition()接收第失败回调函数作为第二个参数,该参数是一个PositionError对象,会包含两个属性:code(整数)、message(错误的简短描述)
navigator.geolocation.getCurrentPosition(() => {}, (e) => {console.log(e.code);console.log(e.message);})//有三种错误//PERMISION_DENIED:浏览器未被允许访问设备位置;要么是用户不同意授权,要么就是在不安全的环境下访问了Geolocation API//POSITION_UNAVAILABLE:系统无法返回任何位置信息;//TIMEOUT:系统不能在规定时间内返回位置信息
Geolocation API位置请求可以通过PositionOptions对象来配置,作为第三个参数;该对象可以配置三个属性:
enableHighAccuracy:布尔值,true表示返回位置尽量精确,默认为false
timeout:毫秒,表示等待位置返回的最长时间,默认为2^23 - 1
maximumAge:毫秒,表示返回坐标的最长有效期,默认为0
Connection State、NetworkInformation API
浏览器会跟踪网络连接状态并以两种方式暴露这些信息:连接事件和navigator.onLine属性
当设备连网时,浏览器会记录这个事实并触发window对象上的online事件;当设备断开网络连接后,浏览器会触发window对象上的offline事件;任何时候都可以通过navigator.onLine属性来确定浏览器连网状态,该属性返回一个布尔值
const connectionStateChange = () => console.log(navigator.onLine);window.addEventListener("online", connectionStateChange);window.addEventListener("offline", connectionStateChange);
有些浏览器认为连接到局域网就算”在线“、
navigator还暴露了NetworkInformation API,可以通过navigator.connection属性使用,该API提供了一些只读属性:
downlink:整数,表示当前设备的带宽(以Mbit/s为单位),可能根据历史网络吞吐量或者连接技术能力计算
downlinkMax:整数,表示当前设备最大下行带宽(以Mbit/s为单位),根据网络的第一跳来决定
effectiveType:字符串枚举值,连接速度和质量
slow-2g(往返时间>2000ms,下行带宽<50kbit/s)、2g(1400ms<=往返时间<2000ms,50kbit/s<=下行带宽<70kbit/s)、3g (…)、4g(…)
rtt:毫秒,表示网络实际的往返时间,可能根据历史网络吞吐量或者连接技术能力计算
type:字符串枚举值,表示网络连接技术
bluetooth、cellular、ethernet、none、mixed、other、unknown、wifi、wimax
saveData:布尔值,表示用户是否启用了“节流”模式
onchange:事件处理程序,会在任何连接状态变化时激发一个change事件
Battery Status API
浏览器可以访问设备电池和充电状态的信息,navigator.getBattery()会返回一个期约实例,解决为一个BatteryManager对象
navigator.getBattery().then((b) => console.log(b)); //BatteryManager {...}
BatteryManager提供四个只读属性:
charging:布尔值,表示设备是否在充电
chargingTime:整数,表示距离充满还有多少秒
dischargingTime:整数,表示预计电池耗尽还有多少秒
level:浮点数,表示电量百分比
这个API还提供了四个事件属性:
onchargingchange:充电状态变化时的处理程序
onchargingtimechange:充电时间变化的处理程序
ondischargingtimechange:放电时间变化的处理程序
onlevelchange:电量百分比变化的处理程序
navigator.getBattery().then((battery) => { const handler = () => console.log('xxx'); battery.onXXXX = handler; //or battery.addEventListener('XXXX', handler);})
硬件
浏览器硬件检测能力相当有限
处理器核心数
navigator.hardwareConcurrency,(无法确定,则这个值为1)这个值表示浏览器可以并行执行的最大工作线程数量,不一定是实际的CPU核心数
设备内存大小
navigator.deviceMemory,返回大致的系统内存大小,包含单位为GB的浮点数
最大触点数
navigator.maxTouchPoints,返回屏幕支持的最大关联触点数量