主题
BOM(Day11 3/3)
本页对应 Day11_BOM&DOM,共 6 段视频,约 1 小时 13 分。
学习目标
- 能够区分单次定时器与多次定时器,使用
setTimeout设置延迟执行、使用clearTimeout在回调执行前取消定时器,并说明单次定时器递归调用实现连续倒计时及停止条件。 - 能够归纳
window的常用属性与方法,说明innerWidth、innerHeight、name的读写特性,并列举弹窗、滚动、定时器及document、history、location、navigator、screen等对象属性。 - 能够使用
history.length获取本窗口历史记录数量,使用back()、forward()、go(n)完成历史记录后退、前进和指定步数跳转。 - 能够读取
location的href、protocol、hostname、port、host、pathname、hash、search等组成部分,并区分修改哪些属性会触发页面跳转、哪些仅更新锚点或参数信息。 - 能够使用
location.reload()刷新页面,使用location.assign()与location.replace()实现跳转,并说明二者在历史记录留存上的差异。 - 能够通过
navigator.userAgent判断客户端浏览器及操作系统信息,并通过screen.width、screen.height读取屏幕分辨率。
知识点
BOM 是什么:从 window 开始
BOM 全称 Browser Object Model,译为浏览器对象模型,是浏览器为 JavaScript 提供的、能够对浏览器进行相关操作的 API。为什么需要:JavaScript 不能只处理页面里的标签,还要控制地址栏、历史记录、定时器、屏幕信息等浏览器能力。怎么想:BOM 的顶层对象是 window,很多“看起来像全局变量/全局函数”的东西,其实都是 window 的属性。document 虽然属于 DOM,但它也是 window 的属性;location、history、navigator、screen 同样如此。怎么做:
js
console.log(window.innerWidth, window.innerHeight); // 视口宽高
console.log(document); // document 也是 window 的属性易错点:innerWidth/innerHeight 是视口宽高,不是屏幕分辨率;它们只读,改设置不会生效。全局函数如 alert、setTimeout 可省略 window.,但本质仍是 window 的方法。
定时器:单次与多次
为什么需要:有些代码要延迟执行,有些代码要按间隔反复执行。怎么想:setTimeout 是单次定时器,时间到只执行一次;setInterval 是多次定时器,每隔一段时间执行。两者参数形式相同:回调函数、毫秒间隔,返回定时器标记;清除时用 clearTimeout / clearInterval,必须保存标记。怎么做:
js
const t1 = setTimeout(function () {
console.log('4 秒后执行一次');
}, 4000);
const t2 = setInterval(function () {
console.log('每隔 1 秒执行');
}, 1000);
// clearTimeout(t1);
// clearInterval(t2);单次定时器也能“循环”起来:在回调函数里再次开启自己。课堂倒计时代码可整理为:
js
let number = 11;
const box = document.getElementById('box');
function runTime() {
number--;
box.innerHTML = number;
if (number === 0) {
return; // 不再开启新定时器
}
setTimeout(runTime, 1000);
}
runTime();调试时曾把 runTime 拼错,导致第一次没执行;改正后能倒计时。又发现首屏先减再渲染会从 9 开始,于是把初始值调成 11,先减后显示就从 10 开始。易错点:setTimeout 只执行一次;清除要在执行前才有意义;单次递归倒计时停止时,不是清除某个“总定时器”,而是用 return 阻止继续开启新定时器。
history:只操作本窗口历史记录
为什么需要:页面中常要后退、前进,例如支付成功后返回原页面。怎么想:history.length 是当前窗口历史记录数量,不是整个浏览器的历史;back() 后退一步,forward() 前进一步,go(n) 正数前进、负数后退。怎么做:
js
console.log(history.length);
// history.back();
// history.forward();
history.go(-2); // 后退两步易错点:history 受安全限制,只限本窗口;窗口关闭再打开就重新计算。back() 不带参数,go(-1) 才表示后退一步。
location:地址栏对象
为什么需要:要获取或修改 URL 的各个部分,实现跳转、刷新、取参数。怎么想:把 URL 拆成协议、主机名、端口、路径、参数、锚点。怎么做:
js
console.log(location.href); // 完整 URL
console.log(location.protocol); // http: / https:
console.log(location.hostname); // 域名或 IP
console.log(location.port); // 端口
console.log(location.host); // 主机名 + 端口
console.log(location.pathname); // 路径
console.log(location.hash); // #锚点
console.log(location.search); // ?参数
// location.reload();
// location.assign('https://www.jd.com'); // 留历史记录
// location.replace('https://www.jd.com'); // 不留历史记录易错点:修改 href/protocol/hostname/port 等通常会引起跳转;hash/search 一般不会整页跳转。assign 会留下历史记录,可后退回来;replace 替换当前记录,回不来。host 含端口,hostname 不含端口。中文路径可用 decodeURI 查看。
navigator 与 screen
为什么需要:网站要统计浏览器/系统,或按系统给不同下载包;也要知道用户屏幕大小。怎么想:navigator.userAgent 获取客户端浏览器信息;screen.width/height 获取屏幕分辨率。怎么做:
js
console.log(navigator.userAgent);
console.log(screen.width, screen.height);易错点:userAgent 在不同浏览器中不同,可用关键字判断,但不绝对;screen 是屏幕分辨率,调整调试工具视口不会改变它。视口大小仍用 innerWidth/innerHeight。
作业清单
先把本节笔记原样保留在这里:
text
1. 页面滚动
2. 电子钟表
3. 倒计时(选做)下面把这三个作业拆成可执行的教程。注意:页面滚动和电子钟表的样式已经提前写好,你不需要重写整套样式;页面滚动里那段“给每一部分设置高度”的代码也先留着,因为里面用到了还没讲的遍历,作业只要求你把“上页、下页、返回顶部”做出来。
页面滚动:先想清楚“一屏”有多高
为什么需要:做整屏滚动页面时,用户点“下一页”,页面应该刚好滚到下一屏,而不是随便滚一个固定像素。固定像素在不同电脑上会错位,所以必须用“视口高度”来算。
怎么想:转写稿里反复强调,一次滚动的距离是“一个视口的高度”,不是屏幕高度,也不是写死的数字。视口高度用 window.innerHeight 获取。调浏览器调试工具会改变视口大小,刷新后页面每一部分的高度也会跟着变,这说明高度是动态算出来的。
怎么做:页面里已经用 JS 给每一部分设置了高度,值就是 innerHeight + 'px'。你只需要绑定按钮事件:下一页用 scrollBy 向下滚一个视口高度,上一页用 scrollBy 向上滚一个视口高度,返回顶部用 scrollTo 回到顶部。核心写法如下:
js
// 下一页
window.scrollBy(0, window.innerHeight);
// 上一页
window.scrollBy(0, -window.innerHeight);
// 返回顶部
window.scrollTo(0, 0);易错点:不要把滚动距离写成固定数字;不要用 screen.height 代替 innerHeight;调整视口后要刷新再观察效果;按钮事件要先获取元素再绑定。
电子钟表:让时间自己走起来
为什么需要:电子钟表不是打开页面显示一次就结束,而是每秒都要更新。这里需要“多次定时器”,让回调函数反复执行。
怎么想:转写稿里说“得来个定时器”“这个定时器也不能结束,是不是一直走啊”。所以思路是:先获取当前时间,把年月日时分秒拼成页面要显示的内容,再用 setInterval 每隔 1000 毫秒执行一次。
怎么做:样式已经写好了,你只需要在定时器回调里取当前时间、格式化、写入对应元素。骨架如下:
js
setInterval(function () {
// 1. 获取当前时间
// 2. 取出年、月、日、时、分、秒
// 3. 拼接成页面需要的格式
// 4. 写入页面元素
}, 1000);易错点:不要用单次定时器 setTimeout 只执行一次;时间格式要注意补零;定时器要一直运行,不要误写清除;写入的是页面元素内容,不是只 console.log。
倒计时(选做):把目标日期换成下一个春节
为什么需要:倒计时和电子钟表相反,它不是显示“现在几点”,而是算“距离目标时间还有多久”。转写稿里明确说,这个作业的目标日期还是之前写的日期,你需要改到“下一个春节的日期”,显示距离春节还有多少天、多少小时、多少秒。
怎么想:先确定目标时间戳,再拿目标时间戳减去当前时间戳,得到剩余毫秒数,最后换算成天、小时、分钟、秒。这个作业稍复杂,选做,不强行要求。
怎么做:可以用 setInterval 每秒刷新,也可以像转写稿里演示的那样用单次定时器“函数里再开下一次定时器”串起来。关键骨架是:
js
var target = new Date('下一个春节日期').getTime();
setInterval(function () {
var now = Date.now();
var diff = target - now;
// 把 diff 换算成天、小时、分钟、秒
// 写入页面
}, 1000);如果模仿转写稿里“单次定时器递归”的写法,要特别注意结束条件:数字到 0 时直接 return,不要再开启下一次定时器。转写稿里调试时还出现过“第一次打开就从 9 开始”的问题,原因是先减再赋值;现场用“从 11 开始”或调整逻辑来规避。你写倒计时也要检查:是先计算再显示,还是先显示再计算。
易错点:目标日期不要沿用旧日期;剩余时间不要出现负数;换算天、时、分、秒时注意取整和取余;如果使用单次定时器递归,结束时要 return,否则会继续开启新的定时器。
提交前的调试建议
先完成页面滚动和电子钟表,倒计时选做。页面滚动重点看“调整浏览器视口大小后,一次是否刚好滚一屏”;电子钟表重点看“是否每秒变化”;倒计时重点看“目标日期是否正确、结束条件是否处理”。课堂案例和代码先整理明白,再做作业;完不成不强行,先把前两个做扎实。
课堂画面






代码示例
1. 单次定时器 setTimeout 实现“循环倒计时”
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>单次定时器实现倒计时</title>
<style>
#box {
font-size: 400px;
text-align: center;
}
</style>
</head>
<body>
<div id="box">10</div>
<script>
// 记录倒计时的数字:从 11 开始,因为函数一进来就先 --,避免首次直接显示 9
let num = 11;
const box = document.getElementById('box');
// 给回调函数起个名字,才能在函数内部再调用自己
function runtime() {
num--; // 先减 1
box.innerHTML = num; // 再改元素内容
if (num === 0) {
return; // 到 0 直接 return,不再开启新的定时器
}
setTimeout(runtime, 1000); // 1 秒后把 runtime 再执行一遍(注意:不加小括号)
}
runtime(); // 先手动调用一次,启动这条“链”
</script>
</body>
</html>关键行与易错点
setTimeout(runtime, 1000):只执行一次,所以必须在回调函数内部再次开启定时器,才能形成“一次接一次”的效果。- 传函数名
runtime,不能写runtime(),否则会立即执行并把返回值传进去。 - 结束倒计时用
return,而不是clearTimeout:每一秒都是新开的定时器,清掉当前这一个没有意义,关键是“不让它再开新的”。 - 变量初值取 11:因为
runtime()一进来就先num--,若从 10 开始,页面第一眼看到的就是 9(转写稿中提到的那个 bug)。 - 这种“回调里再开定时器”的写法类似递归,但定时器不会造成内存泄漏,可以放心使用。
2. history + location:前进后退、刷新、跳转与 URL 组成
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>history 与 location</title>
</head>
<body>
<button id="btn01">后退一步</button>
<button id="btn02">后退两步</button>
<button id="btn03">前进一步</button>
<button id="btn04">前进两步</button>
<hr>
<button id="btnReload">刷新页面</button>
<button id="btnAssign">assign 跳转</button>
<button id="btnReplace">replace 跳转</button>
<script>
// ---------- history:操作本窗口的历史记录 ----------
console.log('历史记录条数:', history.length);
document.getElementById('btn01').onclick = function () {
history.back(); // 回到历史记录的上一个
};
document.getElementById('btn02').onclick = function () {
history.go(-2); // 负数后退,-2 就是退两步
};
document.getElementById('btn03').onclick = function () {
history.forward(); // 前进到历史记录的下一个
};
document.getElementById('btn04').onclick = function () {
history.go(2); // 正数前进,2 就是前进两步
};
// ---------- location:地址栏相关 ----------
document.getElementById('btnReload').onclick = function () {
location.reload(); // 重新加载当前页面
};
document.getElementById('btnAssign').onclick = function () {
location.assign('https://www.jd.com'); // 跳转,会留下历史记录
};
document.getElementById('btnReplace').onclick = function () {
location.replace('https://www.jd.com'); // 跳转,不留下历史记录,回不来
};
// ---------- 读取 URL 的各个组成部分 ----------
console.log('完整 URL:', decodeURI(location.href)); // 中文被编码过,解码后好看
console.log('协议:', location.protocol); // http: / https:
console.log('主机名:', location.hostname); // IP 或域名
console.log('端口号:', location.port);
console.log('主机名+端口号:', location.host);
console.log('路径:', decodeURI(location.pathname));
console.log('参数信息:', location.search); // ?name=100&h=10
console.log('锚点信息:', location.hash); // #first
</script>
</body>
</html>关键行与易错点
history是本窗口的历史记录,不是浏览器全部历史;history.length只是条数参考,不能读具体内容。history.back()等价于go(-1),forward()等价于go(1);go(n)一步、两步都行,back/forward只能一步。assign与replace都能跳转,区别只在历史记录:assign相当于普通跳转,可以后退回来;replace不留记录,点后退回不来(广告页常用)。location.href、location.pathname里带中文时会被编码,展示给用户前可用decodeURI()解码;protocol、hostname、port一般没有中文,不用解码。- location 的属性都是可读可写的:改
href、hostname、protocol、port都会让页面跳转;只有search(参数)和hash(锚点)不会跳转,可用来拼接参数和锚点。 - 用
file://双击打开时,protocol是file:,hostname、port为空,需要用本地服务器(如 Live Server)打开才能看到http:、127.0.0.1、5500这类信息。
3. 页面滚动:用 innerHeight 做“上一页 / 下一页 / 返回顶部”
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>页面滚动</title>
<style>
* {
margin: 0;
padding: 0;
}
.page {
display: flex;
align-items: center;
justify-content: center;
font-size: 100px;
background: #f5f5f5;
margin-bottom: 10px;
}
.bar {
position: fixed;
right: 20px;
bottom: 20px;
}
.bar button {
display: block;
margin-top: 8px;
padding: 6px 16px;
}
</style>
</head>
<body>
<div class="page" id="p1">1</div>
<div class="page" id="p2">2</div>
<div class="page" id="p3">3</div>
<div class="page" id="p4">4</div>
<div class="page" id="p5">5</div>
<div class="bar">
<button id="prev">上一页</button>
<button id="next">下一页</button>
<button id="top">返回顶部</button>
</div>
<script>
// 1. 每一屏的高度 = 当前视口高度,绝不写死数字
const pages = document.querySelectorAll('.page');
for (let i = 0; i < pages.length; i++) {
pages[i].style.height = window.innerHeight + 'px';
}
// 2. 下一页:向下滚一个视口的高度
document.getElementById('next').onclick = function () {
window.scrollBy(0, window.innerHeight);
};
// 3. 上一页:向上滚一个视口的高度
document.getElementById('prev').onclick = function () {
window.scrollBy(0, -window.innerHeight);
};
// 4. 返回顶部:直接滚到 (0, 0)
document.getElementById('top').onclick = function () {
window.scrollTo(0, 0);
};
// 顺带认识 navigator 与 screen
console.log('浏览器信息:', navigator.userAgent); // 含浏览器名、版本号、操作系统
console.log('屏幕宽高:', screen.width, screen.height);
</script>
</body>
</html>关键行与易错点
window.innerHeight是视口高度,screen.height是屏幕分辨率高度,两者不是一回事:前者随调试工具/窗口变化,后者固定。innerWidth / innerHeight是只读的,赋值没有任何效果;用户设备五花八门,高度只能动态读取,写死数字在别人电脑上一定错位。scrollBy(x, y)是相对当前滚动位置滚多少,scrollTo(x, y)是滚到绝对位置;下一页用scrollBy(0, innerHeight),返回顶部用scrollTo(0, 0)。navigator.userAgent是个字符串,判断浏览器要靠“是否包含关键字”(如chrome、firefox、edg),不同浏览器输出内容不一样,不能直接相等比较。- 视口尺寸只在页面加载时读了一次,运行中拖动窗口不会自动重算;如果需要跟随变化,要监听
resize重新赋值。
常见错误
- 单次定时器做倒计时,数字到 0 后仍继续执行或报错 → 回调中仍继续开启下一次
setTimeout,未终止 → 在number === 0时直接return,不再开启下一次。 - 倒计时首次打开直接显示 9,而不是 10 → 先执行减减再渲染 → 调整为先渲染当前值再减,或明确初始值策略并验证首屏。
- 页面滚动“下一页”滚不满一屏,缩放窗口后错位 → 把滚动距离写死,或误用屏幕分辨率 → 用
window.innerHeight动态取视口高度,并在窗口尺寸变化后重算。 - 点击“后退一步”没反应 → 当前窗口没有上一条历史记录,或测试时新开窗口导致历史为空 → 先确认本窗口历史记录,用正常跳转制造记录后再测试。
- 修改
location.protocol、hostname或端口后页面打不开 → 随意改协议/主机/端口会触发跳转且目标不可用 → 只改必要属性;跳转用assign/replace,并明确是否需要保留历史记录。 - 用
navigator.userAgent判断浏览器误判 → UA 字符串包含大量兼容信息,Edge/Chrome 等可能互相包含关键字 → 不依赖单一关键字,结合特征判断或能力检测。 - 电子钟不跳秒或越走越快 → 定时器标记未保存、重复启动多个
setInterval→ 保存intervalId,启动前先清理,停止按钮和页面卸载时清理。
练习
- 改错题:用单次定时器实现 10→0 倒计时,现有代码到 0 后仍继续执行,请指出问题并修改。
- 实现“上一页 / 下一页 / 返回顶部”页面滚动,要求浏览器窗口高度变化后仍能一次滚动一屏。
- 设计三个按钮:后退一步、后退两步、前进一步;写出测试顺序、预期结果,并说明何时按钮会无效。
- 用
setInterval完成电子钟,要求显示年月日时分秒,并提供停止按钮;说明定时器标记如何保存和清理。 - 选做:完成春节倒计时,明确目标时间、刷新频率、到点后的处理方式。
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 单次定时器 | 13:50 |
| 2 | window对象属性方法总结 | 06:00 |
| 3 | history | 12:28 |
| 4 | location | 27:02 |
| 5 | navigator | 08:03 |
| 6 | screen | 06:29 |
小结
- 定时器题先处理“何时停止”,再处理显示效果,否则容易出现停不下来或重复启动。
- 倒计时首屏值和减减顺序要专门验证,不能只看运行几秒后的结果。
- 页面滚动距离必须动态取视口高度,不能把某一台电脑的尺寸当成通用值。
history只影响当前窗口历史,测试前要先确认历史记录是否真的存在。location的属性修改很容易触发跳转,改协议、主机、端口前要确认目标可用。assign和replace的差异要落到“能否后退回来”这一实际效果上验证。- 作业先保证页面滚动和电子钟两个基础题可运行,再做倒计时选做题;调试优先看控制台报错和拼写。