Skip to content

读写元素的文本内容(可读可写)与读取元素的尺寸(只读)(Day13 1/3) ​

本页对应 Day13_DOM,共 6 段视频,约 1 小时 28 分。

学习目标 ​

  • 能使用 DOM 获取元素并操作复选框的 checked 属性,独立完成反选、全选与取消全选案例。
  • 能结合 Date 对象和 setInterval 编写倒计时功能,并在倒计时结束时清除定时器、停止更新。
  • 能准确区分 innerHTML、outerHTML、innerText、textContent 的读取差异,并使用它们读写元素文本或 HTML 内容。
  • 能区分 offset、client、scroll 系列尺寸属性的含义,读取元素总宽高、内容加内边距宽高及包含溢出部分的宽高。
  • 能使用 getBoundingClientRect() 获取元素尺寸信息,并说明其宽高与 offsetWidth、offsetHeight 的对应关系。

知识点 ​

一、先理清 DOM 的知识骨架 ​

本节先回顾 DOM 的五类节点:document、元素、属性、文本、注释。实际开发主要用 document 和元素节点;属性节点没必要手动创建,文本和注释节点也很少直接操作。为什么需要:知道节点类型,才能判断拿到的是元素还是集合。怎么想:记住 nodeType:元素是 1,属性是 2,文本是 3,注释是 8,文档是 9。怎么做:元素节点的 nodeName 可读到标签名。易错点:不要背全部 11 种,HTML 里重点关注这 5 种即可。

获取元素要分清“单个元素”和“集合”。getElementById 返回元素或 null;getElementsByTagName、getElementsByClassName 返回 HTMLCollection;querySelector 返回第一个匹配元素,querySelectorAll 返回 NodeList。为什么需要:后面所有操作都建立在“先拿到元素”上。怎么想:优先用 CSS 选择器,范围清楚、写法直接。怎么做:

js
document.getElementById('box');
document.querySelector('#box');
document.querySelectorAll('#list input');

易错点:HTMLCollection 没有 forEach,NodeList 才有;集合不能直接当元素操作,要遍历或取索引。调试时若发现“压根没获取到”,先检查选择器是否漏了 #,转写稿中的全选案例就出现过这个问题。

二、用文档结构关系找元素 ​

已知某个元素后,可以通过关系找它的子元素、父元素、兄弟元素。为什么需要:有时先拿到容器,再操作内部元素更方便。怎么想:只操作元素,不用文本节点。怎么做:重点记 children、firstElementChild、lastElementChild、parentElement、previousElementSibling、nextElementSibling。易错点:childNodes 会包含文本和注释,课堂明确不建议拿它做元素操作;这些关系属性不带括号,是属性不是方法。

三、属性操作与样式操作 ​

内置属性可直接“元素对象.属性名”读写;setAttribute/getAttribute 不区分内置和自定义;data-* 用 dataset 读写,短横线会自动映射成小驼峰。为什么需要:不同属性来源要用不同读写方式。怎么想:内置属性优先点语法,自定义数据优先 dataset。怎么做:

js
box.dataset.userName;
box.classList.add('active');
box.classList.remove('active');
box.classList.toggle('active');
box.classList.contains('active');

样式上,style 读写行内样式;读取计算样式用 getComputedStyle(element);改类名用 className 或更推荐的 classList。易错点:style 只能读到行内样式,没写行内时得到空字符串;class 在 JS 中写作 className。

四、反选与全选案例 ​

反选的核心是把每个复选框的 checked 取反。为什么需要:练习集合遍历和布尔值操作。怎么想:querySelectorAll 得到 NodeList,可直接 forEach。怎么做:

js
checkItems.forEach(function (check) {
  check.checked = !check.checked;
});

全选则是把全选框的 checked 赋给每个子复选框:

js
checkItems.forEach(function (item) {
  item.checked = checkAll.checked;
});

易错点:HTMLCollection 不能直接 forEach;全选案例中 ID 选择器漏写 # 会导致元素获取失败。

五、倒计时案例:先算差,再换算,再补零 ​

倒计时要先创建目标日期和当前日期,用时间戳相减得到毫秒差,再换算天、时、分、秒。为什么需要:练习日期、取整、取余和定时器。怎么想:先取整天,再取不足一天的毫秒;先取整小时,再取不足一小时的毫秒;分钟可合并取余。怎么做:

js
var days = Math.floor(seconds / (24 * 3600 * 1000));
var hours = Math.floor(seconds % (24 * 3600 * 1000) / (3600 * 1000));
var minutes = Math.floor(seconds % (3600 * 1000) / (60 * 1000));
var secs = Math.floor(seconds % (60 * 1000) / 1000);

补零可用三元表达式;定时器前要先手动调用一次计算函数,否则第一秒是空的。结束时用 clearInterval(intervalId); return;。易错点:目标日期已过会出现负数;忘记 return 当前回调还会继续执行;补零忘了真正赋值。

六、读写文本内容与读取尺寸 ​

innerHTML 读写内部 HTML,outerHTML 包含元素自身;innerText、textContent 会去掉标签,设置时把标签当文本。为什么需要:不同场景对“是否解析标签”要求不同。怎么想:要解析标签用 innerHTML,只要文本用 innerText/textContent。易错点:outerHTML 设置时会把元素自身替换掉。

尺寸方面:offsetWidth/offsetHeight 是总宽高;clientWidth/clientHeight 是内容加内边距、不含边框;scrollWidth/scrollHeight 会把溢出部分算进去;getBoundingClientRect() 返回包含尺寸和位置的对象。易错点:这些尺寸属性只能读,不能直接写;client 不算边框,scroll 要结合溢出内容理解。

回顾清单 ​

text
1. 节点
   document、元素、属性、文本、注释

2. 获取元素
   ① 通过ID  document.getElementById()
   ② 通过标签名  document.getElementsByTagName() / 元素..getElementsByTagName()
   ③ 通过类名
   ③ 通过name属性
   ⑤ 使用CSS选择器  querySelector() / querySelectorAll();  文档、元素都有这两个方法
   ④ 快捷方法 document.documentElement、document.body、body.head、document.all

3. 文档结构 根据关系获取元素
   children
   firstElementChild
   lastElementChild
   parentElement
   previousElementSibling
   nextElementSibling

4. 元素的属性操作
   4.1 读写内置属性  元素对象.属性名
   4.2 元素对象.setAttribute() / 元素对象.getAttribute()
   4.3 读写data-形式的自定义属性  元素对象.dataset.属性名
   
5. 元素的样式操作
   5.1 读写行内样式  元素对象.style.属性名
   5.2 读取计算样式  getComputedStyle(元素对象).属性名
   5.2 操作类名
       元素对象.className
       元素对象.classList   add() remove() toggle() contains()

节点类型:先认识“节点”,再谈操作 ​

为什么需要:DOM 操作的对象不是“字符串”,而是节点。知道节点类型,才能判断拿到的是元素、文本还是注释,避免把文本节点当元素操作。

怎么想:HTML 里常见的是 document、元素、属性、文本、注释五类。转写稿强调:实际开发主要用 document 和元素,属性节点、文本节点、注释节点一般不主动创建。每个节点都有 nodeName、nodeValue、nodeType;重点记 nodeType:元素 1、属性 2、文本 3、注释 8、文档 9。元素节点的 nodeName 可读到标签名,有些场景有用。

怎么做:需要判断节点时看 nodeType;需要标签名时看元素节点的 nodeName。注意完整 DOM 有 11 种节点,但 HTML 里重点关注这 5 种即可。

易错点:不要把注释节点、文本节点当元素去点 style、children;不要死记 nodeValue 的所有差异,先记 nodeType 和元素 nodeName 的用途。

获取元素:六种方式与范围差异 ​

为什么需要:JS 要先“拿到元素对象”,才能改内容、属性、样式。拿不到元素,后面全是空操作。

怎么想:按“精确度、范围、返回值”选方法。ID 最精确;标签名、类名、name 返回集合;CSS 选择器最灵活;快捷属性适合 document、body、head、all。

怎么做:document.getElementById() 返回单个元素或 null;getElementsByTagName() 文档和元素都可调用,返回 HTMLCollection;类名、name 属性了解即可;querySelector() 返回第一个匹配元素,querySelectorAll() 返回 NodeList,文档和元素都有这两个方法。快捷方式:document.documentElement、document.body、document.head、document.all。document.all 是历史语法糖,在 if 中会被特殊处理成 false,不要依赖;ID 同名变量自动生成也不建议用。

易错点:转写稿里的调试很典型——querySelector('#id') 忘了写 #,结果拿不到元素;集合不能直接当元素操作,要索引或遍历;HTMLCollection 通常没有 forEach,NodeList 才有;getElementById 找不到是 null,不是空元素。

文档结构:根据关系拿元素 ​

为什么需要:已经拿到某个元素后,经常要找它的子元素、父元素、兄弟元素,比如列表项联动、删除当前项。

怎么想:只操作“元素”,不操作文本和注释。所以优先用带 Element 的属性。children 拿所有子元素;childNodes 会包含文本和注释,实际很少操作。

怎么做:children、firstElementChild、lastElementChild、parentElement、previousElementSibling、nextElementSibling。它们都是属性,不是方法,使用时不要加小括号。

易错点:firstChild 可能是文本节点;children 是集合,要取第几个用索引;兄弟属性只找元素兄弟,跳过文本和注释。

属性操作:内置属性、Attribute、dataset ​

为什么需要:元素的状态、链接、图片地址、自定义数据都要通过属性读写。

怎么想:内置属性直接点;通用读写用 get/setAttribute;data-* 自定义属性用 dataset,最规范。

怎么做:内置属性如 img.src、input.value 直接读写;setAttribute()/getAttribute() 不区分内置和自定义;data-* 用 元素.dataset.属性名,HTML 中短横线会映射成小驼峰,JS 里不写 data-。

易错点:src、href 等会做特殊处理;布尔属性会映射成布尔值;宽高可能变数字;dataset 属性名不要带 data-;className 是内置属性,但 class 在 JS 中是关键字,所以叫 className。

样式操作:行内样式、计算样式、类名 ​

为什么需要:改样式是 DOM 操作高频需求,但直接改 style 会污染行内样式,读样式又容易读不到。

怎么想:设置优先操作类名;读行内用 style;读最终生效样式用 getComputedStyle;classList 比 className 更精细。

怎么做:元素.style.属性名 读写行内样式,短横线转小驼峰;getComputedStyle(元素).属性名 读取计算样式,是 window 方法;className 整体读写类名;classList.add/remove/toggle/contains 增删切换判断。

易错点:style 只能读行内样式,没写就是空字符串;getComputedStyle 返回只读,不能直接赋值;className 赋值会覆盖原类名;IE 的 currentStyle 不用重点记;classList 的 toggle 是有则删、无则加。

本节核心 API:先分清“内部/自身”和“HTML/文本” ​

本节要解决的是:如何用元素对象读写它“里面”的内容,以及读写“包括自身在内”的内容。四个属性都是元素对象的属性,不是方法,使用时直接“元素对象.属性名”。

html
元素对象.innerHTML		读写内部的html代码和文本内容
元素对象.outerHTML		读写包括元素自身在内的html代码和文本内容
元素对象.innerText		读写内部的文本内容,会剔除掉标签
元素对象.textContent	读写内部的文本内容,会剔除掉标签,读取的值保留空格

为什么需要:页面上的内容既有纯文本,也有标签结构;有时要读“里面写了什么”,有时要连元素自身一起替换。怎么想:先判断范围——inner是内部,outer是包括自身;再判断类型——HTML保留/解析标签,Text只处理文本。怎么做:先获取元素,再点属性读取或赋值。易错点:这四个属性“可读可写”,但读取差异和设置差异不是一回事,尤其innerText和textContent设置时页面表现很接近。

innerHTML:读写内部的 HTML 代码和文本内容 ​

为什么需要:当内部既有文字又有后代标签时,用innerHTML能一次性读到或写入完整结构。怎么想:只要是双标签包裹的内容都算“内部内容”,包括直接文字、ul/li/p等后代元素。怎么做:例如先获取#box,点击读取按钮后输出box.innerHTML,控制台会看到标签、换行、缩进都带着;设置时若msg里写<h1 class="item">你好</h1>,赋给box.innerHTML后,浏览器会解析成真正的h1。易错点:设置innerHTML会替换掉元素内部原有全部内容;读取时控制台保留格式,不代表页面显示也保留这些缩进。

outerHTML:读写包括元素自身在内的 HTML 代码和文本内容 ​

为什么需要:有时不仅要读内部,还要把“当前元素自己”一起拿走或替换。怎么想:inner是“里边的”,outer是“外边的”,所以读取时会连div自身一起带出来。怎么做:输出box.outerHTML,能看到包括#box这个元素在内的完整HTML;设置box.outerHTML = msg时,连box自身都会被新内容替换。易错点:outerHTML设置后,原来的box元素可能已经不在文档里了,后续再拿旧变量操作要格外小心;它不如innerHTML常用,因为多数操作只想改内部。

innerText:读写内部文本内容,会剔除标签 ​

为什么需要:当只想要“文本”,不想要标签结构时用它。怎么想:读取时把标签部分去掉,只保留文字;后代元素里的文字也会被取出。怎么做:读取box.innerText,li、p里的文字会出来,但标签消失;设置box.innerText = msg时,msg里的<h1>不会被解析,而是当纯文本内容放进去。易错点:读取时它不保留缩进,空格会被处理;转写稿里“inert”应纠正为innerText。设置时它与textContent在页面上往往看不出明显差别,因为页面本身不会显示多余控制空白。

textContent:读写内部文本内容,会剔除标签,读取保留空格 ​

为什么需要:既要纯文本,又希望读取时保留原有空白和缩进。怎么想:它和innerText很像,都会剔除标签,但读取时textContent保留空格、缩进等文本节点内容。怎么做:读取box.textContent,标签被去掉,但缩进空格还在;设置box.textContent = msg同样不解析标签。易错点:转写稿里的“tex content/tax content”应纠正为textContent;不要把“读取时保留空格”误认为“设置时也会在页面上保留空格”,页面显示时控制空白通常看不出来。

调试过程:先读后设,观察四个属性的差异 ​

演示时先写一个#box,内部放直接文字、ul/li和p,再放“读取内容”按钮。点击后在控制台分别输出四个属性:innerHTML带标签和格式,outerHTML连自身一起带出,innerText只剩文字且不保留缩进,textContent只剩文字但保留空格。调试中若报错,先检查document是否拼错、选择器是否漏写#。再做设置:定义含<h1 class="item">的msg,分别赋给四个属性,可看到innerHTML解析标签,outerHTML替换自身,innerText和textContent把标签当纯文本。这样就能把“读取差异”和“设置差异”分开理解。

为什么需要读取元素尺寸(只读) ​

做页面效果时,经常要知道元素现在到底多宽多高:做拖拽要限制移动范围,做溢出判断要知道内容有没有超出盒子,做弹层定位要知道元素位置和尺寸。这里的“读取”很关键:offsetWidth、clientWidth、scrollWidth 以及 getBoundingClientRect() 都是只读的,不能通过给它们赋值来改变元素大小。要改大小,仍然去改 style.width、style.height 或类名。

先想清楚盒子模型:三组宽高分别算谁 ​

元素盒子可以粗略分成:内容、内边距、边框、外边距,内容还可能溢出。三组属性就是围绕这些部分做取舍:

  • offsetWidth / offsetHeight:总宽总高,包含内容、内边距、边框,不包含外边距。
  • clientWidth / clientHeight:内容 + 内边距,不包含边框,也不包含溢出部分。
  • scrollWidth / scrollHeight:在 client 的基础上,把溢出的部分也算进来;它用于判断滚动内容实际占多大。

所以不要一上来就背属性名,先问:我要的是“实际占位总宽高”,还是“可视内容区宽高”,还是“包含溢出后的宽高”。

怎么做:读取三组尺寸 ​

本节笔记中的核心 API 如下(已纠正笔记里的拼写:offfsetWidth → offsetWidth,offsetHeihgt → offsetHeight):

js
元素对象.offsetWidth / 元素对象.offsetHeight  // 获取元素的总宽总高
元素对象.clientWidth / 元素对象.clientHeight  // 获取元素宽高,内容+内边距
元素对象.scrollWidth / 元素对象.scrollHeight  // 获取元素宽高,client加上溢出的部分

元素对象.getBoundingClientRect() // 返回对象,对象包含元素的位置和尺寸信息
元素对象.getBoundingClientRect().width  // 同 offsetWidth
元素对象.getBoundingClientRect().height // 同 offsetHeight

转写稿里的调试过程很典型:先输出 offsetWidth 和 clientWidth,发现两者相差 4,检查样式后确认是左右边框各 2px,所以 offset 比 client 多了边框。随后给内部 P 设置较大宽度,让它溢出,再观察 scrollWidth 变大,说明它把溢出部分也计算在内。即使给外层设置 overflow: hidden 或 overflow: auto,scrollWidth 仍然会把溢出内容计入,只是视觉上隐藏或出现滚动条。

易错点:clientWidth 不算边框;offsetWidth 算边框但不算外边距;scrollWidth 不是“滚动条宽度”,而是包含溢出内容后的宽度。读取结果是数字,不带 px。

getBoundingClientRect:尺寸和位置一起拿 ​

getBoundingClientRect() 返回一个对象,里面不只有尺寸,还有位置信息。笔记只列了尺寸部分:

js
元素对象.getBoundingClientRect().width  // 同 offsetWidth
元素对象.getBoundingClientRect().height // 同 offsetHeight

怎么想:如果只想要总宽高,用 offsetWidth/offsetHeight 更直接;如果还要知道元素相对视口的位置,就用这个方法返回的对象。易错点:width、height 是返回对象的属性,不是元素对象自己的属性;方法名较长,注意是 getBoundingClientRect,不要拼错。

获取视口尺寸:是否包含滚动条 ​

笔记中给出席视口尺寸代码:

js
// 会包括滚动条本身的宽度
window.innerWidth
window.innerHeight

// 不会包括滚动条本身的宽度
document.documentElement.clientWidth
document.documentElement.clientHeight

为什么需要:做全屏布局、弹层遮罩、响应式判断时,要知道可视区域大小。怎么想:window.innerWidth/innerHeight 是浏览器窗口内部尺寸,包含滚动条;document.documentElement.clientWidth/clientHeight 是文档根元素的客户区尺寸,不包含滚动条。易错点:笔记里 documentElenment 是拼写错误,正确是 documentElement;两者在有无滚动条时可能不相等,不能混用。

调试顺序与总结 ​

实际调试建议按这个顺序:先确认元素已经获取到;再输出 offsetWidth/offsetHeight 看总占位;输出 clientWidth/clientHeight 看内容加内边距;制造溢出后看 scrollWidth/scrollHeight 是否变大;最后用 getBoundingClientRect() 同时看尺寸和位置。记住结论:offset 含边框,client 不含边框,scroll 含溢出;getBoundingClientRect().width/height 与 offsetWidth/offsetHeight 一致;视口尺寸注意滚动条差异。

课堂画面 ​

课堂画面 1

课堂画面 2

课堂画面 3

课堂画面 4

课堂画面 5

课堂画面 6

代码示例 ​

示例一:读写元素的文本内容(innerHTML / outerHTML / innerText / textContent) ​

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>读写元素内容</title>
  <style>
    #box {
      width: 600px;
      padding: 20px;
      border: 1px solid #999;
    }
    .item {
      padding: 20px;
      background: #ccc;
    }
  </style>
</head>
<body>
  <div id="box">
    这是 div 中直接包裹的文本内容
    <ul>
      <li>列表项一</li>
      <li>列表项二</li>
      <p>段落中的文字</p>
    </ul>
  </div>

  <button id="getBtn">读取内容</button>
  <button id="setBtn">设置内容</button>

  <script>
    var box = document.querySelector('#box');
    var getBtn = document.querySelector('#getBtn');
    var setBtn = document.querySelector('#setBtn');

    // ---------- 读取 ----------
    getBtn.onclick = function () {
      console.log('innerHTML:', box.innerHTML);   // 带标签、带缩进换行
      console.log('outerHTML:', box.outerHTML);   // 连 div 自身都带上
      console.log('innerText:', box.innerText);   // 剔除标签,不保留缩进
      console.log('textContent:', box.textContent); // 剔除标签,保留空格换行
    };

    // ---------- 设置 ----------
    var msg = '<h1 class="item">Hello 你好</h1>';

    setBtn.onclick = function () {
      box.innerHTML = msg;      // 会解析标签,h1 生效,原来的内容被整体替换
      // box.outerHTML = msg;   // 连 box 自己都被替换掉,页面里 box 就没了
      // box.innerText = msg;   // 不解析标签,标签当成纯文字显示
      // box.textContent = msg; // 与 innerText 设置效果一致,标签当纯文字
    };
  </script>
</body>
</html>

关键行与易错点

  • 四个属性都是 元素对象上的属性,不是方法,读写都直接用 =,不加括号。
  • innerHTML / outerHTML 读取时保留标签和缩进换行;outerHTML 多带元素自身那对标签,所以设置 outerHTML 会把元素本身替换掉,页面上原元素就消失了。
  • innerText 读取时剔除标签、不保留缩进空格;textContent 读取时剔除标签但保留空格与换行,这是两者唯一的读差异。
  • 设置时的差异:innerHTML 会解析标签(<h1> 真的变成大标题);innerText / textContent 不解析,标签原样当文本显示,二者设置效果肉眼无差别。
  • 日常写案例多用 innerHTML(纯文字也能用);只想塞纯文本时用 innerText / textContent 更安全。

示例二:读取元素的尺寸与位置 ​

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>读取元素尺寸</title>
  <style>
    #wrapper {
      width: 600px;
      padding: 20px;
      border: 2px solid #999;
      overflow: hidden;   /* 换成 auto 会出现滚动条,scrollWidth 仍包含溢出部分 */
    }
    #content {
      width: 800px;       /* 故意让子元素溢出 */
      margin: 0;
      padding: 20px;
    }
  </style>
</head>
<body>
  <div id="wrapper">
    <p id="content">这是 wrapper 里的一段文字,宽度故意设成 800px 造成溢出。</p>
  </div>

  <script>
    var wrapper = document.querySelector('#wrapper');

    // 总宽总高:内容 + 内边距 + 边框
    console.log('offsetWidth / offsetHeight:', wrapper.offsetWidth, wrapper.offsetHeight);

    // 内容 + 内边距,不含边框,不含溢出部分
    console.log('clientWidth / clientHeight:', wrapper.clientWidth, wrapper.clientHeight);

    // client + 溢出部分
    console.log('scrollWidth / scrollHeight:', wrapper.scrollWidth, wrapper.scrollHeight);

    // 返回对象,含位置和尺寸信息
    var rect = wrapper.getBoundingClientRect();
    console.log('rect.width / rect.height:', rect.width, rect.height); // 同 offsetWidth / offsetHeight

    // 视口尺寸
    console.log('含滚动条:', window.innerWidth, window.innerHeight);
    console.log('不含滚动条:', document.documentElement.clientWidth, document.documentElement.clientHeight);
  </script>
</body>
</html>

关键行与易错点

  • 尺寸属性全部只读,只能读不能赋值;得到的值是数字(不是字符串)。
  • offsetWidth/Height = 内容 + 内边距 + 边框,是元素在文档流中实际占的尺寸。
  • clientWidth/Height = 内容 + 内边距,不算边框,也不算溢出;做拖动限制时常用它取"可移动范围"。
  • scrollWidth/Height = client + 溢出部分,无论 overflow 是 hidden 还是 auto 都会把溢出部分算进去;没有溢出时与 client 完全相等。
  • getBoundingClientRect() 是方法要加括号,返回的是对象;rect.width / rect.height 永远等于 offsetWidth / offsetHeight,另外还带位置信息。
  • 视口两组的区别:window.innerWidth/Height 包含滚动条,document.documentElement.clientWidth/Height 不包含滚动条。
  • 易错:getBoundingClientRect().height 的属性名是 height 不是 offsetHeight,别把对象属性写成元素属性。

示例三:春节倒计时(定时器 + 日期差 + 结束判断) ​

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>春节倒计时</title>
  <style>
    #box { font-size: 20px; color: #f60; }
  </style>
</head>
<body>
  <div id="box"></div>

  <script>
    (function () {
      var box = document.querySelector('#box');

      // 目标日期:2024年2月10日 0时0分0秒(月份从 0 开始,1 表示 2 月)
      var destDate = new Date(2024, 1, 10);

      var timerId = null;

      function runTime() {
        // 当前时间必须写在函数内部,每次执行都重新创建
        var nowDate = new Date();
        var seconds = destDate.getTime() - nowDate.getTime(); // 相差的毫秒数

        // 到达或超过目标日期,结束倒计时
        if (seconds <= 0) {
          clearInterval(timerId);
          box.innerHTML = '春节快乐';
          return;   // 不写 return 本次回调仍会执行完
        }

        // 从毫秒数中提取 天 / 时 / 分 / 秒
        var d = Math.floor(seconds / (24 * 3600 * 1000));
        var h = Math.floor(seconds % (24 * 3600 * 1000) / (3600 * 1000));
        var i = Math.floor(seconds % (3600 * 1000) / (60 * 1000));
        var s = Math.floor(seconds % (60 * 1000) / 1000);

        // 个位数补零
        d = d < 10 ? '0' + d : d;
        h = h < 10 ? '0' + h : h;
        i = i < 10 ? '0' + i : i;
        s = s < 10 ? '0' + s : s;

        box.innerHTML = '距离春节还有 ' + d + ' 天 ' + h + ' 小时 ' + i + ' 分 ' + s + ' 秒';
      }

      runTime();                        // 先手动执行一次,避免第一秒是空白
      timerId = setInterval(runTime, 1000);
    })();
  </script>
</body>
</html>

关键行与易错点

  • 外面套立即执行函数 (function(){ ... })(),目的是产生独立作用域,页面里多个特效时同名变量互不干扰。
  • new Date(2024, 1, 10) 的月份从 0 开始,写 1 才是 2 月;后面的时分秒不写默认为 0。
  • destDate 是固定值,写在函数外面创建一次即可;nowDate 必须写在函数里面,否则时间不会流逝。
  • 提取天数的除数是一天的毫秒数 24 * 3600 * 1000;提取小时、分钟要先 % 取余拿到"不足一天 / 不足一小时"的毫秒数,再除,最后 Math.floor 取整去掉小数。
  • 分钟那步可以合并化简:seconds % (3600 * 1000) / (60 * 1000),因为 24 * 3600 * 1000 本身就是 3600 * 1000 的整数倍。
  • 先 runTime() 再 setInterval,否则页面第一秒是空的。
  • 结束判断里 clearInterval 之后必须 return,否则本次回调后面的拼接代码还会继续执行完。
  • 补零的三元判断在倒计时结束后要注意:负数也小于 10,会补出 0-1 这种结果,所以一定要先拦截 seconds <= 0。

常见错误 ​

  1. 现象:对 getElementsByTagName() 的结果直接调用 forEach(),报 xxx.forEach is not a function。
    原因:得到的是 HTMLCollection,没有 forEach;querySelectorAll() 得到的才是 NodeList。
    修法:改用 for 循环,或 Array.from(集合).forEach(...),或统一改用 querySelectorAll()。

  2. 现象:querySelector('checkAll') 拿到 null,后续操作报 Cannot read properties of null。
    原因:按 ID 获取时漏写 #,选择器被当成标签名。
    修法:写成 querySelector('#checkAll'),单个元素也可直接用 getElementById()。

  3. 现象:用 setAttribute('checked', 'false') 取消复选框,结果仍然选中。
    原因:checked 是布尔属性,setAttribute 写入字符串 'false' 仍会被视为存在。
    修法:直接赋值 checkbox.checked = false,全选/反选都用布尔值。

  4. 现象:倒计时数字不变化,始终显示第一次的值。
    原因:new Date() 和时间差计算写在了 setInterval 外面,只执行一次。
    修法:把“当前时间 + 时间差计算”放进定时器回调内部,目标日期可放外面。

  5. 现象:倒计时到 0 后继续出现负数,补零后显示成 0-1 之类。
    原因:没有终止定时器,也没有阻止后续渲染。
    修法:判断剩余毫秒 <= 0 时 clearInterval(timerId),并 return 结束本次回调。

  6. 现象:补零后仍然显示 0 而不是 00。
    原因:三元表达式只计算了结果,没有重新赋值给变量。
    修法:写成 d = d < 10 ? '0' + d : d;,时、分、秒同理。

  7. 现象:用 innerText 设置含 <h1> 的内容,页面上原样显示标签文本。
    原因:innerText / textContent 按纯文本处理,不解析 HTML。
    修法:需要解析标签用 innerHTML;需要纯文本才用 innerText 或 textContent。

  8. 现象:设置 outerHTML 后,原来保存的元素引用再操作样式或内容无效。
    原因:outerHTML 连元素自身一起替换,原元素已不在 DOM 中。
    修法:要保留自身就用 innerHTML;确实替换自身后需重新获取元素。

  9. 现象:用 el.style.width 读取样式表中的宽度,得到空字符串。
    原因:style 只读行内样式。
    修法:读取最终生效样式用 getComputedStyle(el).width。

  10. 现象:做拖拽或限制移动范围时用 offsetWidth,元素能拖到边框上。
    原因:offsetWidth 包含边框,可移动区域通常只算内容加内边距。
    修法:改用 clientWidth / clientHeight 限制范围。

练习 ​

  1. 实现“反选”按钮:用 querySelectorAll() 获取复选框并反转每个 checked;再用 getElementsByTagName() 获取一次,改用 for 循环实现,比较两种集合的遍历方式。

  2. 实现全选/取消全选联动:点击全选框控制所有子复选框;手动取消任一子复选框时,全选框自动取消。要求全部使用 checked 布尔赋值,不使用 setAttribute。

  3. 封装春节倒计时 runTime():先手动调用一次,再 setInterval;对天、时、分、秒补零;剩余时间 <= 0 时清除定时器、return,并显示“春节快乐”,不能出现负值。

  4. 写一个包含文本、子元素和空白的容器,分别输出 innerHTML、outerHTML、innerText、textContent;再用 innerHTML 和 innerText 设置同一段含 <h1> 的内容,记录并解释差异。

  5. 给容器设置宽度、内边距、边框,并让子元素宽度溢出;输出 offsetWidth、clientWidth、scrollWidth 和 getBoundingClientRect().width;再把 overflow 分别改为 hidden、auto 观察结果并说明原因。

原视频对照 ​

#视频时长
1回顾15:10
2反选案例04:06
3点击复选框全选案例04:55
4倒计时案例32:46
5读写元素中的文本内容17:45
6读取元素的尺寸14:06

小结 ​

  1. DOM 批量操作第一步不是遍历,而是先确认拿到的是单个元素、NodeList 还是 HTMLCollection。
  2. 内容读写要按“是否解析标签、是否包含自身、是否保留空白”三个维度选属性,不能习惯性只用 innerHTML。
  3. 尺寸读取要按“含不含边框、含不含溢出”选系列;需要位置和尺寸一起取时用 getBoundingClientRect()。
  4. 样式读取必须区分行内样式和计算样式;批量改样式优先操作 classList,不要逐个写行内样式。
  5. 定时器类效果要写全“首次执行、终止条件、清理定时器”三件套,否则容易出现空窗、负值和停不下来。
  6. checked、disabled 这类布尔属性直接赋布尔值,不要用 setAttribute 写字符串。
  7. 多效果页面用立即执行函数隔离作用域,变量和元素获取集中管理,避免同名污染。
  8. 换算类逻辑不要干想,先写起来,按“先取整、再取余、再换算”的顺序拆解,小学数学足够应付。