Skip to content

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办公系统中的历史记录操作示意

看到这类OA界面时,再考虑给“后退”“前进”按钮绑定history方法。

易错点: 不要因为学了history就给所有页面都加前进后退按钮;它管理的是浏览器历史记录,按钮效果依赖当前标签页是否已有可前进、可后退的记录。

常见方法:forward、back与go ​

为什么需要: 浏览器地址栏本身有前进、后退,但页面里的自定义按钮也需要调用对应能力,所以history提供了常见方法。笔记中的常见方法图如下:

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,否则可能报错。转写稿里的调试思路是先存、再取、打印类型,确认“取出来是字符串”,再做转换,这个顺序很适合排查本地存储问题。

课件图示 ​

课件第20页:1.6 本地存储(今日重点) 本地存储:将数据存储在本地浏览器中 常见的使用场景

课件第18页:1.5 histroy对象 l history (历史)是对象,主要管理历史记录

课件第21页:1.6 本地存储分类- localStorage(重点) l 作用: 数据可以长

课件第24页:1.6 本地存储分类- localStorage l 浏览器查看本地数据:

课件第27页:1.6 本地存储分类- localStorage l 解决:需要将复杂数据类型转

课件第28页:1.6 本地存储分类- localStorage l 问题:因为本地存储里面取出

课件第29页:1.6 本地存储分类- localStorage l 解决:把取出来的字符串转换

课件第30页:1.6 本地存储分类- localStorage l 解决方案: 对象 JSON

代码示例 ​

示例 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
2BOM和window对象10:01
3定时器之延迟函数07:16
45秒钟之后关闭的广告04:56

小结 ​

  1. BOM 是“操作浏览器”的那一层,window 是它的顶级对象,DOM 只是被包在里面的一个分支——所以 document 再大也大不过 window。
  2. 本地存储的本质只有一句:只能存字符串。凡是复杂数据,存储链路上必然出现 JSON.stringify / JSON.parse 这对搭档,缺一不可。
  3. getItem 永远要考虑返回 null 的情况,先兜底再使用,是本地存储代码不报错的第一道防线。
  4. 数字、布尔值存进去都会“变味”,取出后必须显式转换类型,否则运算结果会静默出错——这类 bug 不报错,最难查。
  5. 定时器的关键是句柄:开的时候接住 ID,关的时候用 ID;提前关闭是真实业务需求(用户等不及 5 秒),不是可选项。
  6. setTimeout 的回调不阻塞后续代码,且回调里的 this 与事件回调里的 this 不是一回事,涉及元素操作时先保存引用。
  7. 存进去的是字符串、取出来的是字符串,中间任何一次“忘记转换”都会在刷新页面后才暴露——所以本地存储相关功能必须用刷新来验收。
  8. 全局变量的挂载规则(var 会、let/const 不会)提醒我们:window 上的东西不等于你的东西,别把业务数据寄存在 window 上。