Skip to content

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.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

课堂画面 2

课堂画面 3

课堂画面 4

课堂画面 5

课堂画面 6

代码示例 ​

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,启动前先清理,停止按钮和页面卸载时清理。

练习 ​

  1. 改错题:用单次定时器实现 10→0 倒计时,现有代码到 0 后仍继续执行,请指出问题并修改。
  2. 实现“上一页 / 下一页 / 返回顶部”页面滚动,要求浏览器窗口高度变化后仍能一次滚动一屏。
  3. 设计三个按钮:后退一步、后退两步、前进一步;写出测试顺序、预期结果,并说明何时按钮会无效。
  4. 用 setInterval 完成电子钟,要求显示年月日时分秒,并提供停止按钮;说明定时器标记如何保存和清理。
  5. 选做:完成春节倒计时,明确目标时间、刷新频率、到点后的处理方式。

原视频对照 ​

#视频时长
1单次定时器13:50
2window对象属性方法总结06:00
3history12:28
4location27:02
5navigator08:03
6screen06:29

小结 ​

  • 定时器题先处理“何时停止”,再处理显示效果,否则容易出现停不下来或重复启动。
  • 倒计时首屏值和减减顺序要专门验证,不能只看运行几秒后的结果。
  • 页面滚动距离必须动态取视口高度,不能把某一台电脑的尺寸当成通用值。
  • history 只影响当前窗口历史,测试前要先确认历史记录是否真的存在。
  • location 的属性修改很容易触发跳转,改协议、主机、端口前要确认目标可用。
  • assign 和 replace 的差异要落到“能否后退回来”这一实际效果上验证。
  • 作业先保证页面滚动和电子钟两个基础题可运行,再做倒计时选做题;调试优先看控制台报错和拼写。