主题
回顾 元素操作与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. 选项卡(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,再给当前项添加。
练习
- 改造图片延迟加载案例:要求已加载图片不再参与后续滚动判断,并保证遍历删除待处理项时不跳项。
- 完成无缝滚动:不写死图片宽度和数量,动态计算临界值;处理图片未加载时 flex 收缩导致宽度读取不准的问题。
- 实现随机点名器:一个按钮在“开始/停止/继续”之间切换,确保多次点击不会叠加定时器。
- 实现选项卡:上下两组列表联动,使用排他思想保证同一时间只有一个导航项和一个内容项处于选中/显示状态。
- 实现表单小练习:用
form的专有属性和方法统计控件数量、提交和重置表单;让输入框定时获取/失去焦点,让文本域双击后选中全部内容。
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 回顾 | 19:27 |
| 2 | 图片延迟加载-不重复遍历 | 30:18 |
| 3 | 实现无缝滚动 | 17:55 |
| 4 | 随机点名器 | 14:03 |
| 5 | 选项卡案例 | 21:31 |
| 6 | HTMLDOM-表单-form元素 | 09:23 |
| 7 | HTMLDOM-表单-输入框元素和文本域元素 | 05:09 |
小结
- 元素尺寸、位置等读取属性多为只读,调整尺寸和位置仍要通过 CSS 完成。
NodeList适合读取和遍历,不适合直接增删成员;需要结构操作时先转纯数组。- 遍历中删除元素要考虑索引变化,避免漏掉下一个元素。
- 定时器 ID 必须跨事件共享,否则会叠加定时器,出现停不下来或越来越快。
- 排他操作的稳定套路是“先全部清除,再设置当前项”。
- 动态尺寸和数量不要写死;图片加载时机与 flex 收缩会影响读取结果。
- 表单元素有专有属性和方法,能简化控件统计、提交、重置、聚焦等操作。