# 12. BOM
在 web 中使用 Javascript,离不开 BOM(Browser Object Model),BOM 提供了很多对象,用于访问浏览器的功能
- window 对象,全局作用域、打开窗口、窗口位置大小、setTimeout、setInterval,alert等
- location 对象,主要是URL相关信息,href、hash、查询字符串 search,端口等信息
- navigator 对象, 主要用来获取浏览器厂商、UA、平台、语言、是否有网、是否启用了 cookie、安装的插件等信息
- screen 对象,记录屏幕相关信息,分辨率,screen.width * screen.height
- history 对象, 访问的历史记录
# window对象
BOM 的核心对象是 window,它表示浏览器的一个实例。在浏览器中,window 对象有双重角色
- 一是浏览器窗口的 JS 接口
- 二是 ES 中的 Global 对象,在全局作用域中使用定义的任何一个对象、变量、函数(let、const除外),都会变成 window 对象的属性和方法。
# 全局作用域
- 全局定义的方法、属性都可以通过window对象调用;window.的属性、方法,其他位置都可以使用。
var age = 29;
function sayAge(){
alert(this.age);
}
alert(window.age); // 29
sayAge(); // 29
window.sayAge(); //29
- let 与 const 在全局作用域内声明的变量不会挂载到 window 上。这点和 var 是有区别的。
- window.设置的对象,与 var 对象声明的对象,虽然都可以通过 window. 访问,但还是有区别的
// window.设置的变量可以通过 delete删除,而 var age,通过window.age是无法删除的。
// 使用var添加的window属性,有一个名为[[Configurable]]的特性,被设置为false,所以无法删
window.test = 2;
var age = 29;
delete window.test // true
delete window.age // false
# 窗口关系及框架frames(第4版已精简)
一般一个页面只对应一个 html 文件,框架相关的 frameset、frame 元素可以让多个页面 html 在同一个窗口中显示。window.self 等同于 window,window.top 指向最外层的窗口。现在基本不会这样使用了,已过时,第 4 版只用了不到 10 行内容来说明。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>窗口关系及框架frames</title>
</head>
<!-- <body> -->
<frameset rows="20%,*" name="frames">
<frame src="topFrame.html" name="topFrame">
<frameset cols="20%,80%">
<frame src="leftFrame.html" name="leftFrame">
<frame src="rightFrame.html" name="rightFrame">
</frameset>
<noframes>Sorry, your browser does not handle frames!</noframes>
</frameset>
<!-- </body> -->
</html>
- 同页面的每个 frame 对应一个单独的 html,都有独立的 window
- 可以通过 window.frames, window.top.frames,获取页面 window 数组,可以通过 frames[0] 或 frames["topFrame"] 来获取对应的window对象,子 frame 对象可以通过 parent 属性访问上层框架。
- 框架的主页面,不需要包含 body 元素,写在 body 外才行
top.frames[2].name // "rightFrame"
top.frames[2].location.href = "leftFrame.html" // 切换页面
- 注意:本地调试需要用nginx或其他方式开启http服务,不能直接访问页面文件路径,会出现跨域的错误,导致top无法访问frame内的页面window
# 窗口位置、大小、打开新窗口
- 窗口位置,浏览器窗口在屏幕中的位置,通过两个属性来确定:
- 浏览器窗口距离屏幕顶部距离(Number):window.screenTop,window.screenY
- 浏览器窗口距离屏幕左侧距离(Number):window.screenLeft,window.screenX
console.log(`screenLeft: ${window.screenLeft}, screenTop: ${window.screenTop}`)
console.log(`screenX:${window.screenX}, screenY: ${window.screenY}`)
- 窗口大小,浏览器窗口大小(窗口缩放后,大小会跟着改变),从外到内:
- 整个浏览器窗口的宽高,包含 console 调试区域:window.outerWidth, window.outerHeight
- 可视区域的宽高(包含滚动条宽高, 不包含console调试区域,不包括浏览器边框和工具栏):window.innerWidth,window.innerHeight
- html整个文档宽高: document.documentElement.clientWidth, document.documentElement.clientHeight,注意有的是视口宽高,有的是实际内容宽高
- body内容的宽高(实际内容): document.body.clientWidth, document.body.clientHeight
- document 包含整个 文档信息, document.documentElement 是 html 元素不包含 document.doctype, document.body 是 body元素内容,不包含 document.head 部分
- 可通过 window.resizeTo,resizeBy 进行缩放,但一般可能会被禁用。
// 打印窗口大小信息
console.log(`浏览器窗口宽*高: outerWidth: ${window.outerWidth}, outerHeight: ${window.outerHeight};`)
console.log(`可视区域的宽*高: innerWidth: ${window.innerWidth}, innerHeight: ${window.innerHeight}`)
console.log(`html整个文档宽高: document.documentElement.clientWidth: ${ document.documentElement.clientWidth}, document.documentElement.clientHeight: ${ document.documentElement.clientHeight}`)
console.log(`body内容的宽高: document.body.clientWidth: ${document.body.clientWidth}, document.body.clientHeight: ${ document.body.clientHeight}`)
- 打开新窗口 window.open(),该方法接收4个参数
url
要打开的链接,默认在新标签页打开。如果为空,就会打开空白页面,如果非完整 url,会把它当前相对的 url。target
窗口目标, 已有窗口或框架名称,这样就会在对应的窗口或框架内打开页面。如果是_self
则是不打开新窗口,在当前页面跳转。settingStr
一个特性字符串,窗口的宽、高、窗口位置等,如果不传,默认在tab页打开新窗口- true or false 如果非新窗口打开,在当前页面跳转时,是否取代浏览器中的当前页面
let newWindow = window.open('') // 新标签页打开空白页
// 返回新窗口的 window,可以用该值修改空白页的内容
newWindow.document.write('hello')
// 一般新打开的 window,可以通过 window.opener 方法原 window
// 为了安全起见,返回值的 opener 需要设置为 null
newWindow.opener === window // true
newWindow.opener = null
// 打开一个相对的路径 http://xx.com/xxx/123
let newWindow = window.open('123')
// 当前页面跳转
let newWindow = window.open('123', '_self')
// 属性字符串,设置打开窗口的大小,这样打开的窗口,可以设置窗口位、resize大小
myWindow = window.open("test", "", "width=300,height=400,resizable=no");
// 打开console调试,如下命令
// 改变窗口位置
// myWindow.moveTo(100,100); // 移动窗口位置到 (100, 100)
// myWindow.moveBy(-50, 50); // 相对当前位置(x,y), 将窗口移动到 (x-50, y+50) 位置
// 改变窗口大小
// myWindow.resizeTo(600,300); // 改变窗口大小为 600*300
// myWindow.resizeBy(100,200); // 改变窗口大小为 (x-100)*(y+200)
有时间后再第一次使用 window.open 打开时,浏览器可能会由于安全方面的原因,阻止打开,这样返回值就不会有新窗口的 window。这种情况我们需要加一个提示,提示浏览器屏蔽了打开新窗口的操作,请手动允许打开。使用 close() 方法可以关闭打开的窗口。
let newWindow = window.open('http://baidu.com')
if (!newWindow) {
alert('浏览器不允许弹窗,请你手动设置允许打开')
}
// 可以使用 close() 关闭打开的窗口
newWindow.close(); // 关闭弹窗
# 设备像素比与视口位置(滚动距离)
第四版新增。
设备像素比 window.devicePixelRatio,这里要了解 物理分辨率 与 逻辑分辨率。一般情况下,屏幕分辨率越高就越清晰,更细腻。
- 物理分辨率是屏幕的真实分辨率
- 逻辑分辨率是当前屏幕尺寸逻辑上应该对应的分辨率
- DPI (dots per inch) 每英寸像素,表示单位像素密度
- window.devicePixelRatio 设备像素比表示物理像素与逻辑像素之间的缩放系数。用于把物理像素转换为 CSS 像素(浏览器报告的虚拟分辨率)
举个例子,13.3 寸 MacBook Pro 的屏幕分辨率(物理分辨率)是 2880 * 1800,而老款 13.3 寸的 MacBook Air 的屏幕(物理)分辨率是 1440 * 900。
屏幕物理尺寸都是 13.3 寸,Pro 的像素是 Air 的两倍。同样 12px(CSS像素)的文字,如果都按真实的物理像素显示。那么 Air 上看到的字体大小应该是 Pro 上的两倍。这样 Pro 上的字就会很小,看不清。
因此在显示时,会按照一定的比例进行缩放。让内容在不同的分辨率下看到的都一致。缩放后的分辨率就是逻辑分辨率。物理像素比与逻辑像素比之间的缩放系数就是 设备像素比。
同样的 100px * 100px 的内容在 Air 上使用 100 * 100 个物理像素表示。而 Pro 上,会使用 200 * 200 个物理像素表示。Pro 的 设备像素比是 2。Air 的像素比是 1。
在 canvas 绘图时,一定要注意设备像素比。
视口位置/滚动距离,一般文档实际内容要比视口的内容多,因此会有滚动。获取页面滚动距离可以使用:
- window.pageXoffset; window.scrollX 水平滚动距离
- window.pageYoffset; window.scrollY 垂直滚动距离
有三种方法用于滚动页面,处理接收 x, y 参数外,还可以接收一个对象
{ left: xx, top: xx, behavior: 'auto或smooth'}
设置是否平滑滚动。 - window.scroll(x, y) 滚动到距离左侧 x 像素,距离顶部 y 像素的位置
- window.scrollTo(x, y) 滚动到距离左侧 x 像素,距离顶部 y 像素的位置
- window.scrollBy(x, y) 相对当前滚动位置滚动,距离左侧 +x,距离顶部 +y
window.scroll(100, 100)
window.scrollBy(100, -100)
window.scrollTo(100, 900)
// 平滑滚动
window.scrollTo({
left: 100,
top: 900,
behavior: 'smooth'
})
# setTimeout与setInterval
JS是单线程语言,它允许通过设置超时值和间歇时间值来调度代码在特定时间执行。
- setTimeout() 在指定时间后将代码添加到执行队列,待空闲后执行, 清除:clearTimeout(timoutId)
- setInterval() 每隔指定的时间就将代码添加到执行队列,待空闲后执行, 清除:clearInterval(intervalId)
一参为执行的代码,二参为等待时间(毫秒),但经过该时间后,指定的代码并不一定会立即执行,JS是一个单线程解释器,一定时间内只能执行一段代码。为了控制代码执行,就有一个JS任务队列。这些任务会按照他们添加到队列的顺序执行。如果队列为空,会立即执行,如果队列不为空,那么就需要等前面的代码执行完了以后再执行。
// 根据值切换对应的demo
var test = 5;
if (test === 1) {
// 1 3 基本同同时打印
setTimeout(function () {
console.log('1');
}, 2000);
setTimeout(function () {
console.log('3');
}, 2000);
}
if (test === 2) {
// 1 3 打印1后后面间隔500ms
setTimeout(function () {
console.log('1');
}, 2000);
setTimeout(function () {
console.log('3');
}, 2500);
}
if (test === 4) {
// 2 3 5 4 1
// 为什么这里后被执行,因为setTimeout是异步的(是注册事件),他会先把函数注册到事件队列当中,等待主程序走完,然后再被调用。
// JS 事件循环,宏任务与微任务。参考:js 事件循环消息队列和微任务宏任务
// https://www.cnblogs.com/xingguozhiming/p/13276725.html
// 同步任务、I/O(比如文件读写等)、setTimeout、setInterval 是宏任务
// Promise.then/catch/finally,generator,async/await 是微任务
// 某个宏任务执行 ok 后,会看微任务队里是否有,有就执行,然后才是宏任务队列。
setTimeout(function() {
console.log(1);
}, 0);
new Promise(function executor(resolve) {
console.log(2);
for( var i=0 ; i<10000 ; i++ ) {
i === 9999 && resolve();
}
console.log(3);
}).then(function() {
console.log(4);
});
console.log(5);
}
if (test === 5) {
// 5 1 2 4 3
setTimeout(function() {
console.log(1);
}, 0);
setTimeout(function() {
console.log(2);
// for循环里面如果没有阻塞代码,还是会顺序执行
for( var i=0 ; i<1000000000 ; i++ ) {
if (i === 999999999) {
console.log(4);
}
}
console.log(3);
}, 0);
console.log(5);
}
// 清除setTimeout
console.log('testStart');
var t = setTimeout(function() {
console.log('exec');
},5000);
clearTimeout(t);
// 每隔一段时间执行
console.log('testStart2');
var t2 = setInterval(function() {
console.log('exec...');
},1000);
// 3s 后清除
setTimeout(function() {
clearInterval(t2);
},3000);
# 系统对话框
浏览器可以通过alert()、confirm()、prompt()方法调用系统对话框向用户显示消息,系统对话框不包含 html,外观由浏览器决定,不是由 css 决定。
这几个方法打开的对话框都是同步和模态的,显示对话框会阻塞程序向下执行,关掉对话框后程序会继续执行
// alert显示信息 "123",点击确认关闭
alert('123');
console.log(456)
// confirm显示询问框,可点击确认或取消关闭, 分别返回true、false
if (confirm("are you ok?")) {
alert('您点击了确定');
} else {
alert('您点击了取消');
}
// prompt打开一个对话框,里面包含一个文本输入框,第一个参数为提示内容,第二个参数为输入框的默认值
// 当点击取消返回null,更改内容后返回对应输入框的内容,如果填写为空,返回""
alert(prompt('请输入你的姓名?', '张三'));
// 另外两个非同步,异步执行的方法
// 调用打印功能,相当于anle ctrl+p
window.print();
// 查找网页里面是否有某个字符串, 如果有打印true,没有打印false
// <p>sdfsdfdfd打发时间乐山大佛技术对接分类数据砥砺奋进是登录</p>
alert(window.find("sdf"));
# location对象
location是最有用的BOM对象之一,提供了当前窗口中加载文档相关信息,如URL,域名,页面路径,查询字段、协议等
- 可以用来获取和改变页面url,重新加载页面
- 它既是window对象的属性,也是document对象的属性。window.location和document.location引用的是同一个对象
// window.location === document.location // true
// location.href 完整的url
// location.origin 网站的开头部分带host 可以用来做api接口前置
// location.pathname // 路径,不带host
// location.searh // ?a=1&b=2 查询字符串
// location.hash // #1 hash值
// location.protocol 协议
// 以访问这个url为例子,查看window.location 内容
// https://mbd.baidu.com/newspage/data/landingsuper?context=%7B%22nid%22%3A%22news_9011978904606190454%22%7D&n_type=0&p_from=1#1
# URL主要属性及对应的解释
# 获取URL查询字符串
在 URLSearchParams 没出现之前一般使用字符串切分来获取查询字符串,方法如下:
// 前端获取url里面的查询字符串
console.log(getQueryStringArgs());
/**
* 获取url里面的查询信息(window.location.search)
* 如果查询为空 "",如果有查询 "?a=kk"
* @returns 如果有查询语句返回{},如果没有返回 ""
*/
function getQueryStringArgs() {
var searchStr = location.search;
if (!searchStr) {
return "";
}
// 去掉开头的?
var argsArr;
searchStr = searchStr.substring(1);
// 切分后将 xxx=xxx 字符串格式转为 {xxxx:xxxx,....}
argsArr = searchStr.split('&');
var args = {};
for (var i = 0; i < argsArr.length; i++) {
var itemArr = argsArr[i].split('=');
var tempKey = decodeURIComponent(itemArr[0]);
var tempValue = decodeURIComponent(itemArr[1]);
args[tempKey] = tempValue;
}
return args;
}
关于 URLSearchParams 的使用可以参考:URLSearchParams URL查询字符串处理 - dev-zuo 技术日常 (opens new window)
这里简单介绍下,URLSearchParams 构造函数接收 location.searh 查询字符串,生成一个 searchParams 实例,这个实例有下面几个方法
- toString() 用于转字符串,会自动去掉首部 ?,不会进行 decodeURIComponent()
- has(prop) 用于查询字符串中是否有该 prop 字段
- get(prop) 用于获取该查询字段的值,会进行 decodeURIComponent()
- delete(prop) 用于删除 prop 查询字段
- set(prop, value) 用于设置查询字符串,会进行 encodeURIComponent()
- for...of 遍历
[["a", "1"], ["b", "测试"]]
类似于 entries 对象后的结果 - 使用 Object.fromEntries(实例),可以直接转为键值对的形式
let qs = '?a=1&b=%E6%B5%8B%E8%AF%95'; // '?a=1&b=测试' 转码后
let searchParams = new URLSearchParams(qs)
console.log(searchParams.toString()) // "a=1&b=%E6%B5%8B%E8%AF%95"
console.log(searchParams.has('b')) // true
console.log(searchParams.get('b')) // "测试"
searchParams.set('c', '我们')
console.log(searchParams.toString()) // "a=1&b=%E6%B5%8B%E8%AF%95&c=%E6%88%91%E4%BB%AC"
searchParams.delete('c') // a=1&b=%E6%B5%8B%E8%AF%95
for(item of searchParams) {
console.log(item)
}
// ["a", "1"]
// ["b", "测试"]
for([key, value] of searchParams) {
console.log(key, value)
}
// a 1
// b 测试
Object.fromEntries(searchParams)
// {a: "1", b: "测试"}
# 改变当前页面url
使用 location 对象可以通过很多方式来改变当前页面 url,每次修改 location 属性(hash 除外),页面都会以新的 URL 重新加载(刷新)
location.assign("http://zuo11.com"); // 切换url
// 如果将location.href与window.location 设置为一个url,会间接调用location.assign(对应的值)
// 假设初始化URL为 http://www.zuo11.com/test/
// 将URL修改为 http://www.zuo11.com/test/#section1
location.hash ="#section1";
// 将URL修改为 http://www.zuo11.com/test/?k=2
location.search = '?k=2';
// 将URL修改为 http://www.baidu.com/test/
location.hostname = "www.baidu.com/";
// 将URL修改为 http://www.zuo11.com/mydir/
location.pathname = "mydir";
// 将URL修改为 http://www.zuo11.com:8080/test/
location.port = 8080;
location 改变 URL 后,浏览器会产生一条新的记录,可以通过后退,返回到前一个页面,使用 location.replace() ,无法返回上一页
location.reload(); // 重新加载 (可能是从缓存中加载)
location.reload(true); // 重新加载 (从服务器重新加载)
# navigator对象
可以用来获取浏览器厂商、UA、平台、语言、是否有网、是否启用了cookie、安装的插件、 navigator对象可以用来识别客户端浏览器信息,UA等。更多信息,参考 Navigator - Web APIs | MDN (opens new window)。更多详情内容,参见下一章,里面会具体讲 navigator 对象的用法。
# 基本属性
// appCodeName: "Mozilla", // 浏览器名称 Safari、Firefox、Chrome、IE 均是这个值
// appName: "Netscape", // 完整的浏览器名称 Safari、Firefox、Chrome、IE 均是这个值
// product: "Gecko", // 产品名称 Safari、Firefox、Chrome、IE 均是这个值
// appVersion: "", // 浏览器的版本,一般不与实际的浏览器版本对应
// userAgent: "", // 浏览器的用户代理字符串,HTTP规范明确规定,浏览器应该发送简短的用户代理字符串指明浏览器的版本和版本号
// cookieEnabled: true, // 是否启用的cookie
// onLine: true, // 是否有网络
// language: "zh-CN", // 浏览器主语言
// platform: "MacIntel", // 浏览器所在的系统平台, windows为win32
// vendor: "Apple Computer, Inc." // 浏览器的品牌,IE、Edge、Firefox均为"", Safari和Chrome(Google Inc.)有值,
/*
IE11
appVersion: "5.0 (Windows NT 10.0; WOW64; Trident/7.0; .NET4.0C; .NET4.0E; rv:11.0) like Gecko"
userAgent: "Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; .NET4.0C; .NET4.0E; rv:11.0) like Gecko"
Edge
appVersion: "5.0 (Windows NT 10.0; Win64; x64; ServiceUI 9) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36 Edge/15.15063"
userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64; ServiceUI 9) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36 Edge/15.15063"
QQ
appVersion: "5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.26 Safari/537.36 Core/1.63.5702.400 QQBrowser/10.2.1893.400"
userAgent: "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.26 Safari/537.36 Core/1.63.5702.400 QQBrowser/10.2.1893.400"
Chrome
appVersion: "5.0 (Macintosh; Intel Mac OS X 10_13_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36"
userAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36"
Firefox
appVersion: "5.0 (Macintosh)"
userAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.13; rv:61.0) Gecko/20100101 Firefox/61.0"
Safari
appVersion: "5.0 (Macintosh; Intel Mac OS X 10_13_5) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/11.1.1 Safari/605.1.15"
userAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_5) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/11.1.1 Safari/605.1.15"
*/
# 检测插件
navigator.plugins 数组记录了当前浏览器上安装的插件
// 相关属性
// name : 插件的名字
// description: 插件的描述
// filename: 插件的文件名
// length: 插件所处理的MIME类型数量
// 检测插件 (非IE环境)
function hasPlugin(name) {
name = name.toLowerCase();
for (var i = 0; i < navigator.plugins.length; i++) {
if (navigator.plugins[i].name.toLowerCase().indexOf(name) > -1) {
return true;
}
}
return false;
}
// 检测Flash
alert(hasPlugin("flash"));
// IE中检测插件方法
function hasIEPlugin(name) {
try {
new ActiveXObject(name);
return true;
} catch (ex) {
return false;
}
}
// 检测Falsh
alert(hasIEPlugin("ShockwaveFlash.ShockwaveFlash"));
# 注册处理程序
HTML5 中定义了 Navigator.registerProtocolHandler() 可以把网站注册为处理某种特定类型信息应用程序。参考:Navigator.registerProtocolHandler() | MDN (opens new window)
# screen对象
可以查看屏幕分辩率, 在编程中用处不大,screen对象基本上只用来表明客户端能力,记录了屏幕相关信息
// height:900 屏幕像素高度
// width:1400 屏幕像素宽度
// availHeight: 841 有效高度
// availLeft: 0
// availTop: 23
// availWidth: 1440
// colorDepth: 24 色彩位数
// pixelDepth: 24 像素深度
# history对象
可以用来前进、后退,history 对象保存着用户的访问记录,出于安全方面的考虑,开发人员无法获取具体访问的 URL,但可以前进或后退。
利用 history 对象可以以编程的方式实现在历史中导航,而且可以修改历史记录。
history.go(-1); // 后退一页 相当于 histroy.back();
history.go(1); // 前进一页 相当于 history.forward();
history.go("字符串") // 导航到历史中包含该字符串的最近的页面
if (history.length === 1) {
// 这是该窗口或标签页打开的第一个页面
}
# 历史状态管理
现代 web 应用开发中,最难的一个环节之一就是历史记录管理。每次点击都会触发页面刷新的时代已经过去。 "后退" 和 "前进" 只是切换一个状态。
HTML5 为 history 新增了 hashchange 事件,hash 改变时会触发。history.pushState/history.replacestate 可以让开发者在改变 URL 的情况下,不刷新页面。pushState 会创建新的 历史记录。
let stateObj = { foo: "bar" }
// state对象、新状态的标题、可选的相对 URL
history.pushState(stateObj, "title", "xx.html")
// 改变 URL,页面不刷新,
// replace 不会创建新的历史记录,覆盖当前记录
// history.replaceState(stateObj, "title", "xx.html")
pushstate 后,点击后退会触发 window 的 popstate 方法。
HTML SPA 单页面应用需要确保 pushState 创建的每个假 URL 对应者服务器真实的 URL,防止刷新后 404 异常。