主题
2-15BOM
本页对应 第二章WebAPIs,共 4 段视频,约 23 分钟。
学习目标
- 能说出 BOM 与 DOM 的包含关系,并解释 window 作为全局顶级对象的作用。
- 能区分全局变量与全局函数在 window 上的挂载规则,并说明 window 属性和方法调用时可省略 window。
- 能使用
setTimeout编写延迟执行代码,并使用clearTimeout根据定时器 ID 取消延迟任务。 - 能解释延迟函数只执行一次及后续代码优先执行的执行顺序。
- 能实现“5 秒后自动关闭广告”案例,并支持用户点击关闭按钮立即关闭广告。
- 能使用本地存储保存和读取数据,实现页面刷新后数据不丢失。
知识点
history对象是什么:它属于BOM,不是DOM
为什么需要: 页面里的按钮要控制浏览器地址栏的“前进、后退”,就需要一个专门管理历史记录的对象,这个对象就是 history。结合转写稿对BOM的解释,BOM是浏览器对象模型,window是顶级对象,document、history、screen等都在window之下;所以history本质上是window.history,平时可以省略window。
怎么想: 先别把history当成页面元素。它管理的是当前标签页的历史记录,和地址栏的前进、后退相对应。转写稿里反复强调“window是最大的对象,调用时可以省略”,因此案例里直接写history.forward()、history.back()或history.go()即可。
怎么做: 在需要前进、后退的按钮事件里调用history的方法。完整理解可以是window.history.go(1),实际写history.go(1)。
易错点: 笔记注释里写成了histroy,正确拼写是history;转写稿里的“报m/鲍姆”指BOM,“温度/win豆”指window,“稻墓/动墓”指DOM,不要把这些同音词带进代码。history不是DOM元素,不能对它做style.display之类的元素操作。
使用场景:OA办公系统中较少但会出现
为什么需要: 笔记明确说history在实际开发中一般比较少用,但在一些OA办公系统中会见到。原因是这类系统常有“返回上一页、前进到下一页”的办公流程操作。
怎么想: 不是所有页面都要加历史记录按钮。先判断业务是否真的需要模拟浏览器地址栏的前进、后退;如果只是页面内切换内容,应优先用DOM操作或路由,而不是硬套history。
怎么做: 笔记中的使用场景示意图如下:

看到这类OA界面时,再考虑给“后退”“前进”按钮绑定history方法。
易错点: 不要因为学了history就给所有页面都加前进后退按钮;它管理的是浏览器历史记录,按钮效果依赖当前标签页是否已有可前进、可后退的记录。
常见方法:forward、back与go
为什么需要: 浏览器地址栏本身有前进、后退,但页面里的自定义按钮也需要调用对应能力,所以history提供了常见方法。笔记中的常见方法图如下:

怎么想: 可以把方法分成两类理解:forward()表示前进,back()表示后退;go(n)用正负数表示方向,go(1)等价于前进,go(-1)等价于后退。案例代码注释里保留了history.forward()和history.back(),实际书写时用了history.go(1)和history.go(-1)。
怎么做: 先获取按钮,再注册点击事件,在事件处理函数里调用history方法。核心就是“点前进按钮走go(1),点后退按钮走go(-1)”。
易错点: 方向不要写反:go(1)是前进,go(-1)是后退;把负号漏掉,后退按钮就会变成前进效果。方法名也不要拼错,是forward、back、go。
案例代码:用按钮控制前进和后退
为什么需要: 只记API不够,笔记用两个按钮把history放进真实点击场景:一个“←后退”,一个“前进→”。
怎么想: 转写稿在讲BOM时说明window可省略,因此代码里直接写history.go(1)、history.go(-1)是正常写法。调试时先让页面产生历史记录,再分别点击两个按钮,观察是否与地址栏前进、后退一致;如果按钮效果相反,就检查go里的正负号。
怎么做: 保留笔记代码如下:
html
<body>
<button class="back">←后退</button>
<button class="forward">前进→</button>
<script>
// histroy对象
// 1.前进
const forward = document.querySelector('.forward')
forward.addEventListener('click', function () {
// history.forward()
history.go(1)
})
// 2.后退
const back = document.querySelector('.back')
back.addEventListener('click', function () {
// history.back()
history.go(-1)
})
</script>
</body>易错点: 代码注释里的histroy应改为history;选择器.forward、.back要和按钮类名一致;history.forward()、history.back()在笔记中被注释掉,实际运行的是go写法,不要同时误写成不存在的方法名。
调试与总结:先确认对象归属,再验证方向
为什么需要: 初学时容易把history和DOM混在一起,或者把前进后退方向写反。转写稿对BOM模型的推导能帮助定位:history挂在window下,属于浏览器对象模型的一部分。
怎么想: 调试时先确认“按钮有没有获取到”,再确认“点击事件有没有触发”,最后确认“go里的数字方向对不对”。如果页面刚打开、没有历史记录,点击后退可能没有预期变化,这是历史记录本身的条件,不是选择器一定写错。
怎么做: 用浏览器实际点击“后退”再点击“前进”,对照地址栏行为;需要时把history.go(1)临时换成history.forward()验证等价思路,但最终保留笔记中的go写法。
易错点: 不要把history写成histroy;不要省略成go(1)而丢掉history.;不要把go(-1)写成go(1)。记住本节核心:history管理历史记录,go(1)前进,go(-1)后退。
本地存储:为什么需要它
为什么需要:页面里的数据如果只放在变量里,一刷新或关闭页面就没了。本节要解决的就是“数据持久化”:把数据存到本地浏览器中,刷新页面数据不丢失。笔记给出的典型场景是 TodoMVC,转写稿里也用“学生就业统计表”演示:新增一条数据、删除一条数据后刷新页面,数据仍然在,说明本地存储能把数据留在浏览器里。
怎么想:先明确本地存储不是把数据存到服务器,而是存在浏览器本地;它适合保存一些不需要后端参与、又希望在刷新后仍保留的数据。笔记强调两个好处:一是刷新或关闭页面不丢失,实现数据持久化;二是容量较大,sessionStorage 和 localStorage 约 5M 左右。
怎么做:根据数据要保留多久,选择 localStorage 或 sessionStorage。本节重点是 localStorage。
易错点:不要把本地存储当成“什么都能直接存”的仓库,它存的是键值对,而且值会以字符串形式保存;容量约 5M,不是无限大。
localStorage 的基本读写
为什么需要:有些数据需要长期保留在本地浏览器中,刷新页面、关闭页面也不丢失,这时用 localStorage。
怎么想:它通过键值对存储,完整写法是 window.localStorage,但 window 是全局对象、顶级对象,平时可以省略,所以直接写 localStorage.setItem(...)。转写稿里也强调 window 可以省略,document、alert 等都是挂在 window 下的,这能帮助我们理解为什么笔记说“省略了 window”。
怎么做:用 setItem(key, value) 存储,用 getItem(key) 获取,用 removeItem(key) 删除。注意笔记代码里存的是数字 18,但获取后打印 typeof 得到的是字符串,说明本地存储保存的是字符串。

html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>本地存储-localstorage</title>
</head>
<body>
<script>
// 本地存储 - localstorage 存储的是字符串
// 1. 存储
localStorage.setItem('age', 18)
// 2. 获取
console.log(typeof localStorage.getItem('age'))
// 3. 删除
localStorage.removeItem('age')
</script>
</body>
</html>易错点:第一,键名要写一致,setItem('age', 18) 后要用 getItem('age') 取;第二,不要误以为存进去的 18 还是数字,取出来是字符串;第三,删除用 removeItem,不要和清空整个存储混淆。
sessionStorage:了解它的生命周期
为什么需要:有些数据只需要在当前会话里临时保存,不希望长期留在本地,就可以用 sessionStorage。
怎么想:它的用法跟 localStorage 基本相同,区别在生命周期:当页面浏览器被关闭时,存储在 sessionStorage 的数据会被清除。也就是说,选择哪一个,关键看“数据要留多久”。
怎么做:存储用 sessionStorage.setItem(key, value),获取用 sessionStorage.getItem(key),删除用 sessionStorage.removeItem(key)。
易错点:不要把 sessionStorage 当成长期存储;它和 localStorage 的 API 很像,写代码时不要把对象名写混。笔记要求这一部分“了解”,重点仍是 localStorage。
localStorage 存储复杂数据类型
为什么需要:本地只能存储字符串,无法直接存储对象、数组等复杂数据类型。笔记里的 goods 是一个对象,如果直接存,不能按对象结构保留下来。
怎么想:存之前先把复杂数据类型转成 JSON 字符串;取出来时,再把 JSON 字符串转回对象。也就是“存前 JSON.stringify,取后 JSON.parse”。
怎么做:先用 JSON.stringify(goods) 转成 JSON 字符串,再 setItem;获取后用 JSON.parse(localStorage.getItem('goods')) 转回对象。调试时可以观察 typeof localStorage.getItem('goods') 是字符串,而 JSON.parse(...) 后才是对象。
html
<body>
<script>
// 本地存储复杂数据类型
const goods = {
name: '小米',
price: 1999
}
// localStorage.setItem('goods', goods)
// console.log(localStorage.getItem('goods'))
// 1. 把对象转换为JSON字符串 JSON.stringify
localStorage.setItem('goods', JSON.stringify(goods))
// console.log(typeof localStorage.getItem('goods'))
// 2. 把JSON字符串转换为对象 JSON.parse
console.log(JSON.parse(localStorage.getItem('goods')))
</script>
</body>易错点:第一,JSON 字符串首先是字符串,属性名要用双引号,不能单引号;属性值如果是字符串型也必须双引号。第二,取出来后不能直接当对象用,要先 JSON.parse。第三,不要对普通字符串随意 JSON.parse,否则可能报错。转写稿里的调试思路是先存、再取、打印类型,确认“取出来是字符串”,再做转换,这个顺序很适合排查本地存储问题。
课件图示








代码示例
示例 1:history 对象实现前进 / 后退
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>history 对象</title>
</head>
<body>
<button class="back">←后退</button>
<button class="forward">前进→</button>
<script>
// history 对象:管理历史记录,与浏览器地址栏的前进后退对应
// 1. 前进
const forward = document.querySelector('.forward')
forward.addEventListener('click', function () {
// history.forward()
history.go(1)
})
// 2. 后退
const back = document.querySelector('.back')
back.addEventListener('click', function () {
// history.back()
history.go(-1)
})
</script>
</body>
</html>关键行与易错点
history.go(1)等价于history.forward(),history.go(-1)等价于history.back(),正数前进、负数后退。history是浏览器内置对象,不能写成histroy(笔记标题里的拼写是笔误),也不要加window.之外的自定义前缀。- 单独打开一个本地文件测试时,历史记录里可能没有可前进/后退的页面,按钮点击后"没反应"是正常现象,需在同一标签页里先跳转过页面再测。
- 先
querySelector拿到按钮再注册click事件,顺序不能反,否则拿到null报错。
示例 2:localStorage 存储基本类型与复杂数据类型
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>本地存储-localStorage</title>
</head>
<body>
<script>
// 本地存储 - localStorage 存储的是字符串
// 1. 存储
localStorage.setItem('age', 18)
// 2. 获取
console.log(typeof localStorage.getItem('age')) // string
// 3. 删除
localStorage.removeItem('age')
// ===== 本地存储复杂数据类型 =====
const goods = {
name: '小米',
price: 1999
}
// localStorage.setItem('goods', goods) // 错误:会存成 [object Object]
// console.log(localStorage.getItem('goods'))
// 1. 把对象转换为 JSON 字符串 JSON.stringify
localStorage.setItem('goods', JSON.stringify(goods))
// console.log(typeof localStorage.getItem('goods')) // string
// 2. 把 JSON 字符串转换为对象 JSON.parse
console.log(JSON.parse(localStorage.getItem('goods')))
</script>
</body>
</html>关键行与易错点
setItem(key, value)的 key 和 value 都必须是字符串,数字18存进去再取出来是字符串'18',typeof是'string',参与运算前要手动转换。- 直接
setItem('goods', goods)会调用对象的toString(),存成[object Object],取出来再也还原不回去——复杂类型必须先JSON.stringify。 - 取出后是字符串,必须
JSON.parse才能当对象用(.name、.price才能访问),JSON.parse(null)不报错但返回null,取值前最好判空。 JSON.stringify生成的字符串属性名和字符串值都是双引号,不能用单引号,这是 JSON 格式规范。removeItem('age')只删指定键,想全清用localStorage.clear();存储容量约 5M。
示例 3:5 秒钟之后自动关闭的广告(延迟函数)
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>5秒钟之后关闭的广告</title>
<style>
.ad {
position: fixed;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: 300px;
height: 200px;
background-color: #fff;
box-shadow: 0 0 10px rgba(0, 0, 0, .3);
}
.ad img {
width: 100%;
height: 100%;
}
.close {
position: absolute;
right: 0;
top: 0;
width: 30px;
height: 30px;
line-height: 30px;
text-align: center;
cursor: pointer;
background-color: #eee;
}
</style>
</head>
<body>
<div class="ad">
<img src="https://dummyimage.com/300x200/4a90e2/fff&text=AD" alt="广告">
<span class="close">×</span>
</div>
<script>
// 第一步:点击关闭按钮,关闭广告
const closeBtn = document.querySelector('.close')
closeBtn.addEventListener('click', function () {
// 关闭的是广告盒子,也就是关闭按钮的爸爸
this.parentNode.style.display = 'none'
})
// 第二步:用户不点击,5 秒钟之后自动关闭(延迟函数只执行一次)
setTimeout(function () {
closeBtn.click() // 程序自动调用一次点击事件,无需用户手动点击
}, 5000)
</script>
</body>
</html>关键行与易错点
setTimeout(回调函数, 毫秒数)是延迟函数,只执行一次;区别于setInterval的反复执行。- 第二个参数单位是毫秒,5 秒要写
5000,写成5就是 5 毫秒,广告一闪就没了。 this.parentNode里this是点击的关闭按钮,要关的是它的父级广告盒子;如果直接this.style.display = 'none'只会把叉号藏起来,广告还在。closeBtn.click()是让程序自动触发一次点击事件,复用第一步已写好的关闭逻辑,不必重写display = 'none'。- 延迟函数是耗时操作,后面的代码不会等它执行完才执行,而是先往下走,时间到了再回头执行回调。
- 若中途不想让它执行了,需先
let timerId = setTimeout(...)拿到返回的正整数编号,再clearTimeout(timerId)清除。
常见错误
1. 直接存对象,取出来是 [object Object]
- 现象:
localStorage.setItem('goods', goods)后,getItem('goods')得到"[object Object]",刷新后数据彻底不可用。 - 原因:
setItem的第二个参数会被隐式toString(),对象被转成了这串无意义字符,原始信息已丢失。 - 修法:存前
JSON.stringify(goods),取后JSON.parse(...);养成“写存必配读转”的成对习惯。
2. 取出的 JSON 字符串当对象用
- 现象:
JSON.parse之后却写成const g = localStorage.getItem('goods'); g.name→undefined;或首次进入页面JSON.parse(null)得到null,紧接着.name报Cannot read properties of null。 - 原因:
getItem永远返回字符串或null,没有做转换;且首次访问时 key 尚不存在。 - 修法:统一写成
const g = JSON.parse(localStorage.getItem('goods')) || {}(数组用|| []),先兜底再使用。
3. 数字存进去变成字符串,累加变成拼接
- 现象:
let n = localStorage.getItem('count') + 1,18 变成"181",翻页/计数越滚越大。 - 原因:本地存储只存字符串,取出即字符串,
+优先做拼接。 - 修法:
Number(localStorage.getItem('count')) || 0再运算;需要小数用parseFloat。
4. 定时器句柄没接住,或 clearTimeout 传了函数
- 现象:用户已点关闭,5 秒后仍报错/仍执行;或写了
clearTimeout(closeBtn)毫无效果。 - 原因:
setTimeout返回的才是 ID,clearTimeout只认 ID;传函数、传元素都不生效。 - 修法:
const timerId = setTimeout(fn, 5000),关闭时clearTimeout(timerId),并置timerId = null防重复清除。
5. setTimeout 回调里 this 不是当初那个元素
- 现象:点击关闭的写法在事件回调里正常,搬进
setTimeout(function(){ this.parentNode.style.display='none' })后报this.parentNode is undefined。 - 原因:普通函数中的
this由调用者决定,定时器回调由 window 调用,this变成 window。 - 修法:事件回调里先
const ad = this.parentNode存起来,定时器里用ad;或回调写成箭头函数继承外层this。
6. 以为 let 声明的全局变量能挂到 window 上
- 现象:
let age = 18后window.age打印undefined,而var声明却能拿到。 - 原因:只有
var声明的全局变量和全局函数才会成为 window 的属性/方法,let、const不会。 - 修法:不要依赖
window.xxx访问自己的变量,需要共享就显式挂window.xxx或用模块/对象管理。
练习
1. 搜索历史(数组存取) 输入框回车后把关键字存进 localStorage,刷新后历史仍在;要求最新在前、自动去重、最多保留 5 条,并带“清空历史”按钮。要点:数组 → stringify 存,取出 parse 后 unshift 再整体存回。
2. 学生就业统计表(今日综合案例精简版) 用数组维护数据列表,新增/删除都同步写回本地存储;刷新页面数据不丢失,顶部实时显示“共 N 条”。要点:任何一次数据变更后都要重新 setItem,不能只改内存数组。
3. 5 秒广告的加固版 广告 5 秒自动关闭,点 × 也能关闭。要求:用户提前点关闭后,5 秒时不得再执行任何逻辑(控制台不能出现报错);用 console.log 证明定时器确实被清除了。
4. 前进/后退按钮的状态 用 history 实现页面上的“←后退”“前进→”两个按钮,并根据 history.length 在无可后退记录时把按钮置灰或提示。思考并写出:为什么刷新后“前进”常常点不动?
5. localStorage 与 sessionStorage 对照实验 同一页面同时向两者存入同一个对象(含 name、age),关闭标签页后重新打开,观察差异并写出结论;再补一个“存数字读出来做加法”的小验证,说明类型问题。
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 今日内容 | 01:41 |
| 2 | BOM和window对象 | 10:01 |
| 3 | 定时器之延迟函数 | 07:16 |
| 4 | 5秒钟之后关闭的广告 | 04:56 |
小结
- BOM 是“操作浏览器”的那一层,window 是它的顶级对象,DOM 只是被包在里面的一个分支——所以 document 再大也大不过 window。
- 本地存储的本质只有一句:只能存字符串。凡是复杂数据,存储链路上必然出现
JSON.stringify/JSON.parse这对搭档,缺一不可。 getItem永远要考虑返回null的情况,先兜底再使用,是本地存储代码不报错的第一道防线。- 数字、布尔值存进去都会“变味”,取出后必须显式转换类型,否则运算结果会静默出错——这类 bug 不报错,最难查。
- 定时器的关键是句柄:开的时候接住 ID,关的时候用 ID;提前关闭是真实业务需求(用户等不及 5 秒),不是可选项。
setTimeout的回调不阻塞后续代码,且回调里的this与事件回调里的this不是一回事,涉及元素操作时先保存引用。- 存进去的是字符串、取出来的是字符串,中间任何一次“忘记转换”都会在刷新页面后才暴露——所以本地存储相关功能必须用刷新来验收。
- 全局变量的挂载规则(
var会、let/const不会)提醒我们:window 上的东西不等于你的东西,别把业务数据寄存在 window 上。