Skip to content

回顾 元素操作与HTML DOM(Day14 1/2) ​

本页对应 Day14_DOM,共 7 段视频,约 1 小时 57 分。

学习目标 ​

  • 能够独立梳理并演示 DOM 元素操作,包括属性、样式、内容、尺寸、位置、滚动位置及元素的创建、添加、删除、替换和克隆。
  • 能够编写图片延迟加载案例,将 NodeList 转为数组,并通过 forEach、索引删除和 delete 稀疏数组处理,实现已加载图片不再重复遍历。
  • 能够实现无缝滚动效果,通过复制元素、计算图片宽度与数量得出临界值,并在达到临界值时重置 scrollLeft 使滚动连续进行。
  • 能够完成随机点名器,使用定时器随机取数组元素,并通过按钮文字切换控制定时器开启与停止,正确使用外层作用域变量保存定时器标识。
  • 能够完成选项卡案例,使用类名操作实现导航与内容联动,并运用“先全部取消选中,再选中当前项”的排他逻辑。
  • 能够说明并演示 HTML DOM 表单相关能力,包括 form 的 length、elements、submit()、reset(),以及输入框和文本域的 focus()、blur()、select() 方法与内容读取方式。

知识点 ​

一、先厘清:DOM 元素操作全景 ​

DOM 是“文档对象模型”,不是“文档对抗模型”。这一节先回顾元素操作:为什么需要?因为页面交互最终都落到“找到元素、读它、改它”。怎么想?按“属性—样式—内容—尺寸—位置—滚动—增删改克隆”分类记。怎么做:内置属性直接点,如 el.id;通用属性用 getAttribute/setAttribute;自定义属性用 dataset;行内样式用 el.style,计算样式用 getComputedStyle(el);类名推荐 classList.add/remove/toggle/contains;内容区分 innerHTML/outerHTML/innerText/textContent;尺寸有 offsetWidth/clientWidth/scrollWidth 和 getBoundingClientRect();位置有 offsetLeft/offsetTop(相对定位祖先)与 getBoundingClientRect()(相对视口);滚动用 scrollLeft/scrollTop,可读可写。创建添加删除替换克隆用 createElement、appendChild、insertBefore、removeChild、replaceChild、cloneNode(true)。易错点:尺寸和位置属性大多只读,改大小位置要用 CSS;style 读不到非行内样式;innerHTML 会解析标签,textContent 不解析;cloneNode() 不传 true 不克隆后代。

二、图片延迟加载:不要重复遍历 ​

为什么需要?图片一开始不设 src,用 data-src 存真实地址,进入视口再加载,能减少无效请求。怎么想?滚动时遍历图片,判断是否进入视口;已加载的图片不应再判断。怎么做?NodeList 不能直接删成员,也不能替换成员,可先转成纯数组:

js
const imageArr = Array.prototype.slice.call(imageItems, 0);

遍历时加载后不要直接 splice/shift,否则数组结构变化会跳元素;课堂用 delete 制造稀疏数组:

js
delete imageArr[index];

forEach 会跳过空位,遍历次数越来越少。易错点:NodeList 没有数组的 pop 等方法;直接删会跳项;delete 不改变 length,但能让 forEach 跳过空位。

三、无缝滚动:临界值 + 复制内容 ​

为什么需要?只滚一遍会停,用户希望看起来无限滚。怎么想?复制一份图片,滚到“一组图片总宽”这个临界值时,瞬间把 scrollLeft 设回 0;因为两组内容相同,用户看不出跳变。怎么做:可把容器内容拼两份:

js
book.innerHTML += book.innerHTML;

临界值 = 单张图宽 × 单组数量:

js
if (book.scrollLeft >= imageWidth * imageLen) {
  book.scrollLeft = 0;
}

宽度不要写死,复制前先算;flex 布局中图片可能因未加载被收缩,给图片设 flex-shrink: 0。易错点:先复制再取数量会取错;图片未加载时宽度可能不是 200;临界值判断要用“大于等于”。

四、随机点名器:定时器必须可清除 ​

为什么需要?一个按钮要在“开始/停止/继续”间切换。怎么想?根据按钮文字判断:显示“停止”就清定时器,否则开启定时器。怎么做:定时器 ID 必须放在外层作用域:

js
let timer;
btn.onclick = function () {
  if (btn.innerHTML === '停止') {
    clearInterval(timer);
    btn.innerHTML = '继续';
  } else {
    btn.innerHTML = '停止';
    timer = setInterval(() => {
      const index = Math.round(Math.random() * (names.length - 1));
      box.innerHTML = names[index];
    }, 50);
  }
};

易错点:把 timer 写在回调里,每次调用都是新局部变量,clearInterval 取不到旧 ID,定时器会叠加,越来越快。

五、选项卡:排他思想 ​

为什么需要?导航和内容要一一对应,且一次只选中一个。怎么想?经典排他:先把所有导航和内容取消选中,再给当前项加 active。怎么做:

js
for (let i = 0; i < tabNavItems.length; i++) {
  tabNavItems[i].classList.remove('active');
  tabContentItems[i].classList.remove('active');
}
tabNavItem.classList.add('active');
tabContentItems[index].classList.add('active');

易错点:只清导航不清内容;外层 forEach 只负责绑事件,内层循环在点击回调里执行,不是嵌套循环执行。布局上可用子元素高度盖住父级下边框,形成单边框效果。

六、表单 form:length、elements、submit、reset ​

为什么需要?表单有独有属性和方法。怎么想?form.length 是表单控件数量,不是子元素数量;form.elements 是控件集合。怎么做:

js
form.submit();
form.reset();

可把按钮放表单外,用 dblclick 提交、contextmenu 重置。易错点:length 统计 input、textarea、select 等控件;elements 也不是普通子元素集合。

七、输入框与文本域:focus、blur、select ​

为什么需要?用 JS 控制焦点和选中文本。怎么做:

js
input.focus();
input.blur();
textarea.select();

易错点:课堂按双标签理解文本域内容,用 innerText/textContent 读标签间文本;实际 DOM 中 textarea 也有 value,项目里常用 value,不要把两种读法混用。select() 是选中文本,不是下拉框的 select 元素。

本节总览代码(按转写稿纠正拼写) ​

js
1. 属性操作
   1.1 读写内置属性
   1.2 getAttribute() setAttribute()
   1.3 data-形式的自定义属性

2. 样式操作
   2.1 读写行内样式
   2.2 读取计算样式
   2.3 类名操作
       className 内置属性
       classList add() remove() toggle() contains()

3. 读写元素的内容(可读可写)
   innerHTML
   outerHTML
   innerText
   textContent

4. 读取元素的尺寸(只读)
   offsetWidth / offsetHeight
   clientWidth / clientHeight
   scrollWidth / scrollHeight
   getBoundingClientRect()  width height

5. 读取元素的位置(只读)
   offsetLeft / offsetTop
   clientLeft / clientTop
   getBoundingClientRect()   left top x y right bottom

6. 读写元素中内容滚动的位置(可读可写)
   scrollLeft
   scrollTop

7. 元素的创建、添加、删除、替换、克隆
   7.1 创建元素
       document.createElement()
   7.2 添加元素
      父元素.appendChild(新元素)
      父元素.insertBefore(新元素,旧元素)
   7.3 删除元素
      父元素.removeChild(元素)
   7.4 替换元素
      父元素.replaceChild(新元素, 旧元素)
   7.5 克隆元素
      元素.cloneNode(true)

属性操作:先分清内置、通用和 data- ​

为什么需要:拿到元素后,经常要读标签自带属性,也要读写自定义数据。怎么想:先判断是不是标准内置属性;如果是标签上写过的属性,可用 getAttribute;自定义数据优先用 data-。怎么做:内置属性直接 元素.属性名 读写;通用属性用 getAttribute() / setAttribute();data- 用 元素.dataset.属性名。易错点:getAttribute 不挑内置还是自定义,但 setAttribute 即使标签没写也能加上;自定义属性不要乱造,按 data- 规范写。

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

为什么需要:JS 要读样式、改样式、切换选中态。怎么想:改样式最稳的是改行内或类名;读样式要区分“行内样式”和“最终计算样式”。怎么做:元素.style.样式名 读写行内样式;window.getComputedStyle(元素) 读计算样式;类名可用 className,更推荐 classList.add/remove/toggle/contains。易错点:行内没设置的样式,用 style 读通常是空字符串;计算样式只能读,设置仍要回到行内样式或类名;className 对应标签的 class,因为 class 是保留字。

内容读写:innerHTML/outerHTML/innerText/textContent ​

为什么需要:经常要读元素里的文本或标签,或整体替换内容。怎么想:要解析标签用 innerHTML;要包含元素自身用 outerHTML;只要文本用 innerText 或 textContent。怎么做:四个属性都可读可写,任何元素都有。易错点:innerHTML 赋值会解析标签;textContent 赋值时标签只当文本;outerHTML 包含自身;读取文本类属性时标签部分不会保留。

尺寸读取:offset/client/scroll/getBoundingClientRect ​

为什么需要:做布局、滚动、懒加载都要知道元素尺寸。怎么想:先看要不要算边框、要不要算溢出。怎么做:offsetWidth/offsetHeight 是内容+内边距+边框的总尺寸;clientWidth/clientHeight 不算边框;scrollWidth/scrollHeight 会把溢出部分算进去;getBoundingClientRect().width/height 也可读总宽高。易错点:这些尺寸属性都是只读,给它赋值没用;要调整大小只能用 CSS 设置宽高,而且通常设置的是行内样式,需要自己算好总宽高。

位置读取:offset/clientLeft/getBoundingClientRect ​

为什么需要:定位、拖拽、判断元素是否进入视口都要位置。怎么想:先明确参照物:相对定位祖先/页面,还是视口。怎么做:offsetLeft/offsetTop 相对第一个定位祖先元素,都不定位则相对页面;clientLeft/clientTop 实际是左边框、上边框宽;getBoundingClientRect() 返回 left/top/x/y/right/bottom,相对视口。易错点:页面滚动后,元素在页面上的位置不变,但在视口上的位置会变;right/bottom 不是到视口右边/下边的距离,而是元素右边/下边到视口左边/上边的位置。位置属性也是只读,改位置仍用 CSS。

滚动位置:scrollLeft/scrollTop ​

为什么需要:做滚动监听、无缝滚动、回到顶部都要读写内容滚动位置。怎么想:它读的是元素内部内容滚动的偏移,不是元素自身位置。怎么做:元素.scrollLeft、元素.scrollTop 可读可写,值是纯数字,方便加减。易错点:视口特殊:可用 document.documentElement 代表视口,读视口尺寸或页面滚动位置;不要和 offsetLeft/offsetTop 混淆。

创建、添加、删除、替换、克隆 ​

为什么需要:动态列表、弹层、轮播都靠 DOM 增删改。怎么想:先创建,再找父元素插入;删除、替换都要通过父元素;克隆用于保留原元素。怎么做:document.createElement('标签名') 创建;父.appendChild(新) 末尾添加;父.insertBefore(新, 旧) 插到旧元素前;父.removeChild(元素) 删除;父.replaceChild(新, 旧) 替换;元素.cloneNode(true) 连后代一起克隆。易错点:创建出的元素类型和 querySelector 获取的一样,之前学的元素操作都能用;把已有元素添加到别处会“移动”,原位置就没了,想在原位置保留要先克隆;replaceChild 参数是“新元素,旧元素”,别写反。

DOM元素操作回顾:先建立“操作地图” ​

为什么需要:DOM 不是零散 API 的堆叠,而是“获取元素后,对属性、样式、内容、尺寸、位置、滚动和节点结构做读写”的一套体系。没有地图,案例一多就容易混。怎么想:按“读/写”分类:属性能读写内置属性,也能用 getAttribute/setAttribute;样式优先操作行内 style,读取计算样式用 getComputedStyle;内容区分 innerHTML/outerHTML 与 textContent/innerText;尺寸和位置大多只读,调整要靠 CSS;节点可创建、添加、删除、替换、克隆。怎么做:例如类名推荐 classList.add/remove/toggle/contains;尺寸读取用 offsetWidth/offsetHeight(含边框)、clientWidth/clientHeight(不含边框)、scrollWidth/scrollHeight(含溢出);位置用 offsetLeft/offsetTop(相对定位祖先)和 getBoundingClientRect()(相对视口)。易错点:style.xxx 读不到非行内样式;getComputedStyle 只读;尺寸属性不能直接赋值来改大小,必须写 CSS。

图片延迟加载:不重复遍历已加载图片 ​

为什么需要:滚动时每次都遍历全部图片,已加载的还会被反复判断,浪费性能。怎么想:最直接是“加载完就从集合删掉”,但 querySelectorAll 得到的是 NodeList,不能删成员也不能替换成员。转写稿中验证过:imageItems.pop() 报不是函数;借数组方法改 this 也不行。于是把 NodeList 转成纯数组,再删除。怎么做:

js
const imageItems = document.querySelectorAll('img');
const imageArr = Array.prototype.slice.call(imageItems, 0);
imageArr.forEach((img, index) => {
  if (img.getBoundingClientRect().top < window.innerHeight) {
    img.src = img.dataset.src;
    delete imageArr[index]; // 不改变数组结构
  }
});

易错点:直接 splice 删除会让后续元素前移,forEach 继续按索引走会“跳过一个”;用 delete 删除后数组变稀疏,forEach 会跳过空位,正好避免重复遍历。注意 dataset 对应 data-* 自定义属性。

无缝滚动:复制内容 + 临界值瞬间归零 ​

为什么需要:只滚一遍就停,不像无缝轮播。怎么想:把内容复制一份,滚到“第二组第一张到达左侧”的临界值时,瞬间把 scrollLeft 设为 0;用户看到的画面仍是同一组图,所以察觉不到。怎么做:转写稿中用 book.innerHTML += book.innerHTML 复制;复制前读取单张宽度和一组数量,临界值为“单张宽 × 一组数量”;滚动到临界值就归零。易错点:在 flex 布局中,图片未加载时伸缩项可能被压缩,读到的宽度可能是 80 而不是 200。转写稿的调试结论是给图片设 flex-shrink: 0,保证读取稳定;尺寸和数量尽量从 DOM/CSS 计算,不写死。

随机点名器:按钮文字切换与定时器作用域 ​

为什么需要:一个按钮要完成“开始/停止/继续”的切换。怎么想:用按钮文字判断状态;开启定时器时保存 setInterval 的返回值,停止时 clearInterval。怎么做:

js
let timer = null;
btn.onclick = function () {
  if (btn.innerHTML === '停止') {
    clearInterval(timer);
    btn.innerHTML = '继续';
  } else {
    timer = setInterval(() => {
      const i = Math.round(Math.random() * (names.length - 1));
      box.innerHTML = names[i];
    }, 50);
    btn.innerHTML = '停止';
  }
};

易错点:把 timer 写在回调函数内部会变成局部变量,每次点击都创建新变量,停止时拿不到上一次的定时器 ID,导致“停不下来、越来越快”。要放在外层作用域。

选项卡案例:排他操作与索引对应 ​

为什么需要:导航和内容要联动,且同一时间只选中一个。怎么想:经典“排他”:先把所有导航和内容取消选中,再给当前项和对应内容加 active。怎么做:tabs.forEach((tab,index)=>{ tab.onclick=()=>{ tabs.forEach(t=>t.classList.remove('active')); panes.forEach(p=>p.classList.remove('active')); tab.classList.add('active'); panes[index].classList.add('active'); }; }); 易错点:外层 forEach 只负责绑定事件,内层循环在点击回调里执行,不是“循环嵌套执行”;边框拼接时可用父元素补左边框/下边框,子元素高度可能盖住父元素下边框,这是布局技巧而非 bug。

表单与输入控件:form、input、textarea ​

为什么需要:表单元素有独有属性和方法。怎么想:form.length 是表单控件数量,不是子元素数量;form.elements 是控件集合;form.submit()、form.reset() 可主动提交/重置。输入框和文本域有 focus()、blur()、select()。怎么做:转写稿中演示了 5 秒后 input.focus(),再 3 秒后 input.blur();双击文本域调用 select() 选中内容。易错点:form.length 容易误认为子元素个数;elements 是集合,可用索引取控件;输入框读内容用 value,文本域按本节讲解作为双标签可用 innerHTML/innerText/textContent 读取内容,注意区分场景。

课堂画面 ​

课堂画面 1

课堂画面 2

课堂画面 3

课堂画面 4

课堂画面 5

课堂画面 6

代码示例 ​

1. 选项卡(HTML + CSS + JS) ​

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>选项卡</title>
<style>
  * { margin: 0; padding: 0; }
  ul { list-style: none; }

  .table { width: 800px; margin: 100px auto; }

  /* 导航:父元素提供左边框和下边框,子项提供上边框和右边框,拼出单边框效果 */
  .table-nav {
    display: flex;
    height: 41px;              /* 内容高 41,子项 40+1 正好贴合 */
    border-left: 1px solid #ccc;
    border-bottom: 1px solid #ccc;
  }
  .table-nav li {
    padding: 0 20px;
    line-height: 40px;         /* 子项继承行高 */
    background: #f5f5f5;
    border-top: 1px solid #ccc;
    border-right: 1px solid #ccc;
    cursor: pointer;
  }
  /* 选中项:高度多 1 像素,往下溢出,把父元素的下边框盖住 */
  .table-nav li.active {
    background: #fff;
    height: 41px;
  }

  /* 内容区:默认全部隐藏,只显示带 active 的那个 */
  .table-content li {
    padding: 20px;
    border: 1px solid #ccc;
    border-top: none;          /* 去掉上边框,避免与导航下边框重合 */
    min-height: 200px;
    display: none;
  }
  .table-content li.active { display: block; }
</style>
</head>
<body>
<div class="table">
  <ul class="table-nav">
    <li class="active">未付款订单</li>
    <li>未发货订单</li>
    <li>未收货订单</li>
    <li>未评价订单</li>
    <li>已完成订单</li>
  </ul>
  <ul class="table-content">
    <li class="active">这是未付款的订单</li>
    <li>这是未发货的订单</li>
    <li>这是未收货的订单</li>
    <li>这是未评价的订单</li>
    <li>这是已完成的订单</li>
  </ul>
</div>

<script>
  (function () {
    // 拿到两个集合,它们的索引是一一对应的
    var navItems = document.querySelectorAll('.table-nav li');
    var contentItems = document.querySelectorAll('.table-content li');

    navItems.forEach(function (navItem, index) {
      navItem.onclick = function () {
        // 排他:先把所有导航项和内容项的 active 都去掉
        for (var i = 0; i < navItems.length; i++) {
          navItems[i].classList.remove('active');
          contentItems[i].classList.remove('active');
        }
        // 再把当前点击的导航项、以及它对应的内容项加上 active
        navItem.classList.add('active');
        contentItems[index].classList.add('active');
      };
    });
  })();
</script>
</body>
</html>

关键行与易错点

  • navItems.forEach(function (navItem, index) {...}):第二个参数 index 必须取,内容区靠它跟导航一一对应,漏写就拿不到对应项。
  • 排他操作:先循环把所有 active 去掉,再给当前项加上。这是选项卡的核心思路,不要写成“先加再想办法减掉上一次的”。
  • for 循环里同时 remove 两个集合的 active,因为 navItems 和 contentItems 长度相同、索引相同,一次循环能搞定两边。
  • 外层 forEach 和事件回调里的 for 不算嵌套循环:forEach 只负责给每个导航项挂事件,回调要等用户点击才执行,那时外层循环早就结束了。
  • 边框拼接技巧:相邻 li 都加四边框会出现双线(1+1=2px),所以 li 只给上、右,父元素给左、下,拼出单边框。
  • 高度覆盖技巧:li 有 border-top: 1px + 内容高 40 = 41,父元素内容高也是 41 正好贴合;选中项设成 height: 41px 后总高 42,多出的 1 像素溢出,把父元素那条下边框盖住,于是选中项看起来“没有下边框”。这是布局技巧,不是删边框。
  • .table-content li 要 border-top: none,否则和导航的下边框叠成 2px。

2. 图片延迟加载(不重复遍历,NodeList → 纯数组) ​

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>图片延迟加载</title>
<style>
  * { margin: 0; padding: 0; }
  body { display: flex; flex-wrap: wrap; }
  img {
    width: 200px;
    height: 200px;
    margin: 10px;
    background: #eee;      /* 没加载出来时先占个位 */
  }
</style>
</head>
<body>
<script>
  // 造 120 张图:先不设 src,只设 data-src(自定义属性,浏览器不会去加载)
  function makeSrc(i) {
    var color = 'hsl(' + (i * 37 % 360) + ', 70%, 65%)';
    var svg = '<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200">'
            + '<rect width="200" height="200" fill="' + color + '"/>'
            + '<text x="100" y="115" font-size="48" text-anchor="middle" fill="#fff">' + i + '</text>'
            + '</svg>';
    return 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svg);
  }

  for (var i = 1; i <= 120; i++) {
    var img = document.createElement('img');
    img.setAttribute('data-src', makeSrc(i));
    document.body.appendChild(img);
  }

  // 1. 借用数组的 slice,把 NodeList 变成纯数组(slice 返回的永远是纯数组)
  var imgArr = Array.prototype.slice.call(document.querySelectorAll('img'), 0);
  // 等价写法:var imgArr = [].slice.call(document.querySelectorAll('img'), 0);

  function lazyLoad() {
    imgArr.forEach(function (img, index) {
      // 元素距离视口顶部的距离 < 视口高度 → 已经出现在视口里
      if (img.getBoundingClientRect().top < window.innerHeight) {
        img.src = img.dataset.src;   // 真正设置 src,浏览器才开始下载
        delete imgArr[index];        // 用 delete 删除,不改变数组结构
      }
    });
  }

  window.addEventListener('scroll', lazyLoad);
  lazyLoad();                        // 首屏先跑一次
</script>
</body>
</html>

关键行与易错点

  • 不设 src 只设 data-src:data- 属性对浏览器没有实际意义,图片不会去下载;只有真正给 src 赋值才会加载。
  • 为什么要转成纯数组:querySelectorAll 得到的是 NodeList,它不能 splice / shift(方法不存在,报 not a function),也不能把某个成员删掉或换成别的(imgArr[0] = null 无效),数量永远固定。所以先 slice(0) 复制出一个纯数组,数组里的成员还是那些 img 元素。
  • Array.prototype.slice.call(集合, 0):借数组的截取方法作用于 NodeList,从第 0 个截到最后,等于复制一份,返回值是纯数组。
  • 必须用 delete imgArr[index],不能 splice / shift:直接删会让后面元素整体前移一位,forEach 的下标却继续往后走,结果漏掉一个元素(第一次就少加载一张)。delete 只把那个位置变成空值,数组长度和索引都不变,形成稀疏数组,forEach 遍历时自动跳过没值的位置,正好符合“加载过的不再判断”。
  • delete 之后数组长度仍是 120,但空位越来越多,遍历次数越来越少,最后全是空位时一次都不执行。
  • 判断条件用 getBoundingClientRect().top < window.innerHeight:距离视口顶部小于视口高度,说明已经进入视口。

3. form 元素 + 输入框 / 文本域方法(HTML + JS) ​

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>form 元素</title>
</head>
<body>
<form id="myForm" action="/submit">
  <table>
    <tr>
      <td>用户名:</td>
      <td><input type="text" name="username" value="请输入用户名"></td>
    </tr>
    <tr>
      <td>简介:</td>
      <td><textarea name="intro" cols="30" rows="4">这是文本域里的内容</textarea></td>
    </tr>
    <tr>
      <td>城市:</td>
      <td>
        <select name="city">
          <option>北京</option>
          <option>上海</option>
          <option>广州</option>
        </select>
      </td>
    </tr>
  </table>
</form>

<button id="btn">双击提交 / 右击重置</button>

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

  // form 独有的两个属性
  console.log(formBox.length);    // 3 —— 表单控件的数量(input + textarea + select)
  console.log(formBox.elements);  // 表单控件元素的集合,可用索引取出每个控件

  // form 独有的两个方法:按钮写在 form 外面也能操作表单
  var btn = document.querySelector('#btn');
  btn.ondblclick = function () {
    formBox.submit();             // 双击提交表单
  };
  btn.oncontextmenu = function () {
    formBox.reset();              // 右击(上下文菜单事件)重置表单
  };

  // 输入框:5 秒后自动获取焦点,再过 3 秒失去焦点
  setTimeout(function () {
    formBox.elements[0].focus();
  }, 5000);
  setTimeout(function () {
    formBox.elements[0].blur();
  }, 8000);

  // 文本域:双击选中里面全部文字
  formBox.elements[1].ondblclick = function () {
    this.select();
  };
</script>
</body>
</html>

关键行与易错点

  • formBox.length 和 formBox.elements 是 form 元素独有的属性,数的是表单控件(input / textarea / select),不是子元素个数——这里子元素只有 table 一个,但 length 是 3。
  • formBox.elements[0] 就是第一个控件(input),[1] 是 textarea,[2] 是 select,可直接拿来用,不必再 querySelector 一次。
  • submit() / reset() 是 form 独有的方法,按钮可以写在 form 外面,照样能提交和重置;action="/submit" 是随手写的地址,提交动作会真的发生(页面跳 404 属正常)。
  • 事件名别写错:双击是 ondblclick(单击是 onclick),右击是 oncontextmenu(上下文菜单事件)。
  • 三个焦点/选择方法:focus() 获取焦点、blur() 失去焦点、select() 选中里面的文字,input 和 textarea 都有。
  • 取内容要分清元素:input 用内置属性 value;textarea 是双标签,按课堂讲法读内容用 innerText / textContent。

常见错误 ​

  • 现象:图片延迟加载每次滚动都遍历全部图片,已加载图片仍反复判断。原因:待处理集合没有收缩,仍按原始 NodeList 全量遍历。修法:先把 NodeList 转成纯数组,加载成功后从待处理数组中移除;移除时避免遍历跳项。
  • 现象:直接对 NodeList 使用 splice、shift 或给成员赋空值,报错或无效。原因:NodeList 不是纯数组,不能像数组一样增删替换成员。修法:用 Array.prototype.slice.call(nodeList, 0) 或 [...nodeList] 转成纯数组后再操作。
  • 现象:在 forEach 中直接删除当前元素后,下一个元素被跳过。原因:删除后数组索引前移,而遍历索引继续递增。修法:用 delete arr[index] 保留位置并让 forEach 跳过空位,或改用倒序/重建集合等方式。
  • 现象:无缝滚动中单张图片宽度有时是 200,有时是 80。原因:图片未加载完成时,flex 收缩项目没有内容支撑,被收缩;加载完成后才得到预期宽度。修法:给伸缩项目设置 flex-shrink: 0,并在复制图片前动态获取单图宽度和数量。
  • 现象:随机点名器点击“停止”停不下来,继续点击越来越快。原因:定时器 ID 存在事件回调的局部变量中,每次点击都新建定时器,旧定时器未清除。修法:把定时器 ID 放到外层作用域/闭包中共享,停止时 clearInterval。
  • 现象:选项卡点击后多个选项同时高亮。原因:只给当前项加 active,没有先清除其他项的选中状态。修法:排他处理,先遍历清除所有 active,再给当前项添加。

练习 ​

  1. 改造图片延迟加载案例:要求已加载图片不再参与后续滚动判断,并保证遍历删除待处理项时不跳项。
  2. 完成无缝滚动:不写死图片宽度和数量,动态计算临界值;处理图片未加载时 flex 收缩导致宽度读取不准的问题。
  3. 实现随机点名器:一个按钮在“开始/停止/继续”之间切换,确保多次点击不会叠加定时器。
  4. 实现选项卡:上下两组列表联动,使用排他思想保证同一时间只有一个导航项和一个内容项处于选中/显示状态。
  5. 实现表单小练习:用 form 的专有属性和方法统计控件数量、提交和重置表单;让输入框定时获取/失去焦点,让文本域双击后选中全部内容。

原视频对照 ​

#视频时长
1回顾19:27
2图片延迟加载-不重复遍历30:18
3实现无缝滚动17:55
4随机点名器14:03
5选项卡案例21:31
6HTMLDOM-表单-form元素09:23
7HTMLDOM-表单-输入框元素和文本域元素05:09

小结 ​

  • 元素尺寸、位置等读取属性多为只读,调整尺寸和位置仍要通过 CSS 完成。
  • NodeList 适合读取和遍历,不适合直接增删成员;需要结构操作时先转纯数组。
  • 遍历中删除元素要考虑索引变化,避免漏掉下一个元素。
  • 定时器 ID 必须跨事件共享,否则会叠加定时器,出现停不下来或越来越快。
  • 排他操作的稳定套路是“先全部清除,再设置当前项”。
  • 动态尺寸和数量不要写死;图片加载时机与 flex 收缩会影响读取结果。
  • 表单元素有专有属性和方法,能简化控件统计、提交、重置、聚焦等操作。