主题
商品参数选项-创建选项元素与商品参数选项-点击选项元素选中(Day21 1/2)
本页对应 Day21_PC项目,共 4 段视频,约 50 分钟。
学习目标
- 能依据商品参数数据遍历每组选项,动态创建并嵌套
DL、DT、DD元素,正确渲染选项结构与文本。 - 能使用事件委托监听选项区域点击,判断目标为
DD,并为当前项添加active、同组其他项移除active,实现组内排他选中。 - 能使用固定长度数组按选项组索引保存被选中的
DD引用,并通过自定义属性获取组索引,保证选中项按组顺序存放且同组互斥。 - 能遍历选中项数组创建选中标签元素,包含选项文本和关闭按钮,并在每次渲染前清空标签容器以避免重复。
- 能在初始化时默认选中每组第一个
DD,并验证点击不同组或同组选项后数组内容、标签顺序和页面选中状态保持一致。
知识点
先把 DL 骨架创建出来
为什么需要:商品参数区的结构是「一组一个 dl,dl 里一个 dt 标题 + 若干 dd 选项」。页面上的组数是动态的,数据里有几组就得建几个 dl,写死 HTML 不现实。
怎么想:转写稿里第一件事就是去 goodsDetail 里翻数据——这个数组有多长,页面上就该有几个 dl。所以直接对数组做遍历,数组元素就叫 dlItem,下标叫 dlIndex,用 dlItem 造 dl,用 dlIndex 记录这是第几组。
怎么做:
js
goodsDetail.forEach((dlItem, dlIndex) => {
const dlEle = document.createElement('dl');
optionsBox.appendChild(dlEle); // 先挂到包裹元素上
// 后面再往里塞 dt / dd
});易错点:不少同学会纠结「必须先创建完 dl、再创建 dt、最后才 appendChild 到 optionsBox 吗」。讲师当场验证过:不需要。dlEle 这个变量永远指向那个 dl 元素,它有没有被挂进 optionsBox,都不影响你继续给它 appendChild。先挂后挂都能加进去,顺序不是硬性要求。
往 DL 里补 DT 和 DD
为什么需要:dl 只是壳子,用户看到的是标题和选项文字。
怎么想:dt 的内容就是 dlItem.title;dd 是「每组里的选项数据」,也就是 dlItem.data——注意 data 又是一个数组,每个成员是对象,选项文字在 type 属性上。所以 dd 要多遍历一层。
怎么做:
js
const dtEle = document.createElement('dt');
dtEle.innerHTML = dlItem.title;
dlEle.appendChild(dtEle);
dlItem.data.forEach((ddItem, ddIndex) => {
const ddEle = document.createElement('dd');
ddEle.innerHTML = ddItem.type;
dlEle.appendChild(ddEle);
});易错点:这里比「单纯创建+添加元素」多套了一层循环,容易把 dlItem.data 写成 dlItem,结果拿到的是 undefined;另外 innerHTML 取的是 ddItem.type,不是 ddItem 本身。
点击选中与同组排他
为什么需要:选项要能点,点中后同组只能亮一个。
怎么想:dd 全是 JS 新建的,用传统「先获取集合再循环绑定」根本拿不到集合。有两条路:创建时顺手绑事件,或者事件委托。讲师选了委托——委托只挂一个监听,不用在循环里再套函数形成闭包,内存占用更小。
怎么做:委托给包裹元素 optionsBox,回调里先判断目标是不是 dd(tagName 是大写的 DD),再排他:
js
optionsBox.addEventListener('click', function (e) {
if (e.target.tagName !== 'DD') return;
const siblings = e.target.parentElement.children;
for (let i = 1; i < siblings.length; i++) { // 0 是 dt,跳过
siblings[i].classList.remove('active');
}
e.target.classList.add('active');
});易错点:parentElement.children 拿到的是 HTMLCollection,没有 forEach,只能写 for 循环;而且它包含 dt,所以从 1 开始;排他时对每个兄弟是 remove('active'),不是 add。
为什么要用一个固定长度的数组
为什么需要:点一下就往页面 append 一个「选中标签」,点几次就有几个,重复点同组还会出现两个「银色」,顺序也乱。
怎么想:选中标签最多 4 个,且位置和组一一对应——第 0 位永远放颜色、第 1 位放内存、第 2 位放版本、第 3 位放购买方式。既然「位置固定、按组对应」,那天然就该用数组:数组下标 = 组下标,同组再点就是覆盖同一个下标,自动实现「互斥」;不同组各占各的格子,自动实现「顺序」。
怎么做:
js
const selectedArr = new Array(goodsDetail.length); // 长度固定,元素为 undefined易错点:把 DD 元素放进数组,只是多了一个引用,不会像 appendChild 那样把元素从原位置挪走,两者有本质区别。
用自定义属性把组索引带出来
为什么需要:点击时只能拿到 e.target,它身上没有任何「我是第几组」的信息。而组索引只在创建时(dlIndex)才有,创建和点击不是同一个时机。
怎么想:既然创建时能拿到,就把它存到元素身上,用的时候再读。用 dataset 加自定义属性最合适。
怎么做:在创建 dd 时加 ddEle.dataset.groupIndex = dlIndex;,点击时读 e.target.dataset.groupIndex,再 selectedArr[groupIndex] = e.target。也可以加在 dl 上,点击时用 e.target.parentElement.dataset.index 取,但加在 dd 上就不用再找父元素。
易错点:属性名带连字符,dataset 读取要转驼峰(group-index → groupIndex)。
根据数组渲染选中标签
为什么需要:数组只是数据,页面上得真的出现标签。
怎么想:封装一个 createSelectedTag(),遍历数组生成标签。数组可能是稀疏的(比如只选了第 1、3 组),forEach 会自动跳过空位,正好合适;for 循环反而不方便。
怎么做:渲染前先 selectedBox.innerHTML = '' 清空,再逐项创建:
js
function createSelectedTag() {
selectedBox.innerHTML = '';
selectedArr.forEach(ddEle => {
const selectedEle = document.createElement('div');
selectedEle.className = 'selected-tag';
selectedEle.innerHTML = ddEle.innerHTML + '<span class="close"></span>';
selectedBox.appendChild(selectedEle);
});
}易错点:不清空就会越点越多(讲师演示时 256G 出现了两次);forEach 遍历出来的元素是 dd,文字要从 dd 的 innerHTML 取。最后补一个默认选中:创建 dd 时判断 ddIndex === 0 就 classList.add('active')——注意这只加了类名,选中标签还没同步,是留给下一步的事。
先明确目标:动态创建参数选项元素
为什么需要:商品参数选项不能写死在页面里,数据来自 goodsDetail,有几组就应生成几个 dl,每组里再生成一个 dt 和若干 dd。
怎么想:先遍历 goodsDetail 创建 dl,把 dl 放进 optionsBox;再在每组里创建 dt,内容取 dlItem.title;最后遍历 dlItem.data 创建 dd,内容取 ddItem.type。
怎么做:
js
goodsDetail.forEach((dlItem, dlIndex) => {
const dlEl = document.createElement('dl');
optionsBox.appendChild(dlEl);
const dtEl = document.createElement('dt');
dtEl.innerHTML = dlItem.title;
dlEl.appendChild(dtEl);
dlItem.data.forEach((ddItem, ddIndex) => {
const ddEl = document.createElement('dd');
ddEl.innerHTML = ddItem.type;
dlEl.appendChild(ddEl);
});
});易错点:转写里“B包”应理解为闭包,“ATM collection”应理解为 HTMLCollection。先 appendChild(dlEl) 再加 dt、dd 也可以,因为 dlEl 变量始终指向同一个 dl;但不要把 dlEl 和 dtEl 混用。HTMLCollection 没有 forEach,遍历它要用普通 for 循环。
点击选中:用事件委托监听 DD
为什么需要:dd 是动态创建的,逐个绑定事件既麻烦又容易形成闭包;委托给父容器 optionsBox 更稳。
怎么想:点击 optionsBox 后,先判断 event.target 是不是 dd,只有点中选项才继续。
怎么做:
js
optionsBox.addEventListener('click', e => {
if (e.target.tagName !== 'DD') return;
const siblings = e.target.parentElement.children;
for (let i = 1; i < siblings.length; i++) {
siblings[i].classList.remove('active');
}
e.target.classList.add('active');
});易错点:tagName 得到的是大写 DD;parentElement.children 包含 dt,所以循环从索引 1 开始;先移除同组其他 dd 的 active,再给当前 dd 加 active。调试时点选项之间的缝隙或 dt,页面不应有反应,说明目标判断生效。
用数组保存被选中选项:解决顺序和同组互斥
为什么需要:直接点一次就创建一个选中标签,会重复、乱序;而选中标签要求同组互斥、不同组按顺序。
怎么想:固定 4 个位置,索引对应组:颜色 0、内存 1、版本 2、购买方式 3。创建 dd 时用自定义属性记录“所在组索引”,点击时把当前 dd 放进 selectedArr[groupIndex]。
怎么做:
js
const selectedArr = new Array(goodsDetail.length);
// 创建 dd 时
ddEl.dataset.groupIndex = dlIndex;
// 点击 dd 后
const groupIndex = e.target.dataset.groupIndex;
selectedArr[groupIndex] = e.target;易错点:这里要的是 dl 的索引,不是 dd 在组内的索引;同组三个 dd 的 groupIndex 应相同。selectedArr 可能是稀疏数组,但 forEach 会自动跳过空位,正好可用。调试时输出 selectedArr:先点黑色只有一项,再点银色仍在同一位置替换,再点内存则出现在下一个位置。
根据数组创建选中标签并处理默认选中
为什么需要:数组只保存了被选中的 dd,页面还要把选中标签渲染出来。
怎么想:每次渲染前先清空 selectedBox,再遍历 selectedArr,为每个 dd 创建 selected-tag,文字取 ddEl.innerHTML,并拼上关闭按钮。
怎么做:
js
function createSelectedTag() {
selectedBox.innerHTML = '';
selectedArr.forEach(ddEl => {
if (!ddEl) return;
const tagEl = document.createElement('div');
tagEl.className = 'selected-tag';
tagEl.innerHTML = ddEl.innerHTML + '<span class="close">×</span>';
selectedBox.appendChild(tagEl);
});
}点击 dd 并更新 selectedArr 后调用 createSelectedTag()。默认选中可在创建 dd 时判断 ddIndex === 0,给第一个 dd 加 active;同时要把该 dd 放入 selectedArr[dlIndex] 并渲染,否则页面上只有选中样式,没有选中标签。
易错点:不先清空 selectedBox 会不断追加,出现重复标签;转写里的“风一尺”即 forEach,“稀数数组”即稀疏数组。渲染函数只负责根据数组显示,不要在这里再次乱改选中状态。
为什么不能“点一下就直接追加标签”
先看直接实现的问题:点击 DD 后创建选中标签,并 selectedBox.appendChild(tag)。第一次点“银色”能出现标签;再点“黑色”会继续追加;再点同组“金色”也不会替换“银色”。结果是标签越来越多、顺序也无法保证。
所以本节的核心不是“创建标签”,而是先解决选中状态如何被有序保存。转写里的思路是:创建一个固定长度数组,数组索引对应参数组顺序,数组元素保存被选中的 DD。这样同组点击会覆盖同一索引,不同组会按索引顺序排列。
还原选项结构:用 goodsDetail 创建 DL、DT、DD
先根据 goodsDetail 遍历创建每组 DL,每组里创建 DT 标题,再遍历该组 data 创建多个 DD。创建 DD 时同时写入组索引,供点击时使用。
js
goodsDetail.forEach((dlItem, dlIndex) => {
const dlEle = document.createElement('dl')
optionsBox.appendChild(dlEle)
const dtEle = document.createElement('dt')
dtEle.innerHTML = dlItem.title
dlEle.appendChild(dtEle)
dlItem.data.forEach((ddItem, ddIndex) => {
const ddEle = document.createElement('dd')
ddEle.innerHTML = ddItem.type
ddEle.dataset.groupIndex = dlIndex
if (ddIndex === 0) ddEle.classList.add('active')
dlEle.appendChild(ddEle)
})
})易错点:DL 先加入 optionsBox 后,仍然可以通过变量继续给 DL 添加子元素;变量始终引用同一个 DL。另外,tagName 得到的是大写 DD,判断时不要写成小写。
点击选中与同组排他:事件委托到 optionsBox
DD 是动态创建的,直接获取集合再绑定不方便,因此用事件委托:监听 optionsBox 的点击,判断 e.target.tagName === 'DD'。同组排他时取 dd.parentElement.children,从索引 1 开始跳过 DT,先移除同组所有 active,再给当前 DD 加 active。
js
optionsBox.addEventListener('click', e => {
if (e.target.tagName !== 'DD') return
const dd = e.target
const groupIndex = dd.dataset.groupIndex
const children = dd.parentElement.children
for (let i = 1; i < children.length; i++) {
children[i].classList.remove('active')
}
dd.classList.add('active')
selectedArr[groupIndex] = dd
createSelectedTag()
})易错点:children 是 HTMLCollection,没有 forEach,这里用 for 循环;不要从 0 开始,因为第 0 个是 DT。groupIndex 来自 data-group-index,同一组 DD 的组索引相同,所以同组会覆盖同一个数组位置。
用固定长度数组保存选中的 DD
数组长度就是参数组数量,可直接用 new Array(goodsDetail.length)。点击某组 DD 时执行 selectedArr[groupIndex] = dd。这不是把 DOM 元素“移动”到数组里,只是多了一个引用;原来页面中的 DD 仍然存在。
易错点:不要用 push,否则同组会不断追加;也不要误以为放进数组会改变 DOM 结构。数组可能是稀疏的,未点击的组对应位置为空。
根据数组重建选中标签,并先清空
每次点击后都根据 selectedArr 重新生成选中标签。关键顺序是:先清空 selectedBox.innerHTML = '',再遍历数组创建标签。forEach 遍历稀疏数组时会跳过空位,比手写 for 更省判断。
js
function createSelectedTag() {
selectedBox.innerHTML = ''
selectedArr.forEach(dd => {
if (!dd) return
const tag = document.createElement('div')
tag.className = 'selected-tag'
tag.innerHTML = dd.innerHTML + '<span class="close">×</span>'
selectedBox.appendChild(tag)
})
}易错点:如果每次只 appendChild 不清空,标签会重复累积;清空后重建,才能让界面与数组实时对应。默认第一个 DD 可在创建时判断 ddIndex === 0 加 active,转写本节先做到默认选中样式;选中标签的默认同步未在本节展开。
课堂画面




代码示例
示例一:创建选项元素(dl / dt / dd)
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>商品参数选项 - 创建选项元素</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
.options-box { width: 620px; margin: 30px auto; font-size: 14px; }
.options-box dl { display: flex; align-items: center; margin-bottom: 10px; }
.options-box dt { width: 80px; color: #999; }
.options-box dd {
padding: 4px 10px; margin-right: 10px; cursor: pointer;
border: 1px solid #ccc; border-radius: 3px;
}
</style>
</head>
<body>
<div class="options-box" id="optionsBox"></div>
<script>
// 演示数据:外层每组一个对象(title + data),data 里每个成员还有 type
var goodsDetail = [
{ title: '颜色', data: [{ type: '金色' }, { type: '银色' }, { type: '黑色' }] },
{ title: '内存', data: [{ type: '16G' }, { type: '64G' }, { type: '128G' }, { type: '256G' }] },
{ title: '版本', data: [{ type: '移动版' }, { type: '电信优惠版' }, { type: '公开版' }] },
{ title: '购买方式', data: [{ type: '官方标配' }, { type: '套餐一' }, { type: '套餐二' }] }
];
var optionsBox = document.getElementById('optionsBox');
// 一组数据 对应 一个 dl
goodsDetail.forEach(function (dlItem, dlIndex) {
var dlEle = document.createElement('dl');
dlEle.dataset.index = dlIndex; // 记录所在组的索引,后面点击时要用
optionsBox.appendChild(dlEle); // 先挂到容器上(先挂后加子元素也可以)
// 组标题 dt
var dtEle = document.createElement('dt');
dtEle.innerHTML = dlItem.title; // 注意是 dlItem.title
dlEle.appendChild(dtEle);
// 组内选项 dd —— 要再遍历一层 data
dlItem.data.forEach(function (ddItem, ddIndex) {
var ddEle = document.createElement('dd');
ddEle.innerHTML = ddItem.type; // 注意是 ddItem.type
ddEle.dataset.groupIndex = dlIndex; // 把组索引挂到 dd 上,点击时直接用
dlEle.appendChild(ddEle);
});
});
</script>
</body>
</html>关键行与易错点
- 数据结构是“两层”:
goodsDetail每一项是一个组(有title和data),data的成员才带type,所以必须遍历两层才能建出多个dd。 dlEle这个变量始终指向同一个dl元素,所以先appendChild到optionsBox再往里加dt/dd完全没问题;变量指向的是元素本身,跟元素有没有被挂到文档里无关。- 别写混:
dt取dlItem.title,dd取ddItem.type。 - 本节先埋两个伏笔:
dl上的dataset.index、dd上的dataset.groupIndex,是给后面“点击时拿组索引”用的,因为点击事件里根本拿不到forEach的index。
示例二:点击选项选中 + 同组排他(事件委托)
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>商品参数选项 - 点击选中</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
.options-box { width: 620px; margin: 30px auto; font-size: 14px; }
.options-box dl { display: flex; align-items: center; margin-bottom: 10px; }
.options-box dt { width: 80px; color: #999; }
.options-box dd {
padding: 4px 10px; margin-right: 10px; cursor: pointer;
border: 1px solid #ccc; border-radius: 3px;
}
.options-box dd.active { border-color: #e4393c; color: #e4393c; }
</style>
</head>
<body>
<div class="options-box" id="optionsBox"></div>
<script>
var goodsDetail = [
{ title: '颜色', data: [{ type: '金色' }, { type: '银色' }, { type: '黑色' }] },
{ title: '内存', data: [{ type: '16G' }, { type: '64G' }, { type: '128G' }, { type: '256G' }] },
{ title: '版本', data: [{ type: '移动版' }, { type: '电信优惠版' }, { type: '公开版' }] },
{ title: '购买方式', data: [{ type: '官方标配' }, { type: '套餐一' }, { type: '套餐二' }] }
];
var optionsBox = document.getElementById('optionsBox');
goodsDetail.forEach(function (dlItem, dlIndex) {
var dlEle = document.createElement('dl');
dlEle.dataset.index = dlIndex;
optionsBox.appendChild(dlEle);
var dtEle = document.createElement('dt');
dtEle.innerHTML = dlItem.title;
dlEle.appendChild(dtEle);
dlItem.data.forEach(function (ddItem) {
var ddEle = document.createElement('dd');
ddEle.innerHTML = ddItem.type;
ddEle.dataset.groupIndex = dlIndex;
dlEle.appendChild(ddEle);
});
});
// 元素是新创建的,取不到集合再逐个绑事件 —— 直接委托给外层容器
optionsBox.addEventListener('click', function (e) {
// nodeName 是大写 DD
if (e.target.nodeName !== 'DD') return;
// 同组排他:父元素的所有子元素(含 dt,是 HTMLCollection)
var siblings = e.target.parentElement.children;
for (var i = 1; i < siblings.length; i++) { // 从 1 开始,跳过第 0 个 dt
siblings[i].classList.remove('active');
}
// 当前点击的 dd 选中
e.target.classList.add('active');
});
</script>
</body>
</html>关键行与易错点
- 选项元素是 JS 动态创建的,
querySelectorAll拿不到“未来”的元素,所以用事件委托绑在optionsBox上,既省循环也避免大量闭包。 - 判断目标是
dd还是容器/dt:e.target.nodeName !== 'DD'。nodeName返回大写,写成'dd'永远不成立。 - 排他范围是“同组”:
e.target.parentElement.children拿到的是dl的所有子元素,第 0 个是dt,循环必须从i = 1开始,否则会把dt也当选项处理。 children是HTMLCollection,没有forEach,只能用for循环;要清掉别人的active用classList.remove,最后再给当前项add。
示例三:用数组存储被选中的选项(固定长度 + 索引即组序)
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>商品参数选项 - 数组存储选中项</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
.options-box { width: 620px; margin: 30px auto 10px; font-size: 14px; }
.options-box dl { display: flex; align-items: center; margin-bottom: 10px; }
.options-box dt { width: 80px; color: #999; }
.options-box dd {
padding: 4px 10px; margin-right: 10px; cursor: pointer;
border: 1px solid #ccc; border-radius: 3px;
}
.options-box dd.active { border-color: #e4393c; color: #e4393c; }
.selected-box { width: 620px; margin: 0 auto; font-size: 14px; display: flex; flex-wrap: wrap; }
.selected-tag {
padding: 2px 8px; margin-right: 8px; border-radius: 3px;
border: 1px solid #e4393c; color: #e4393c;
}
.selected-tag .close { margin-left: 6px; cursor: pointer; }
</style>
</head>
<body>
<div class="options-box" id="optionsBox"></div>
<div class="selected-box" id="selectedBox"></div>
<script>
var goodsDetail = [
{ title: '颜色', data: [{ type: '金色' }, { type: '银色' }, { type: '黑色' }] },
{ title: '内存', data: [{ type: '16G' }, { type: '64G' }, { type: '128G' }, { type: '256G' }] },
{ title: '版本', data: [{ type: '移动版' }, { type: '电信优惠版' }, { type: '公开版' }] },
{ title: '购买方式', data: [{ type: '官方标配' }, { type: '套餐一' }, { type: '套餐二' }] }
];
var optionsBox = document.getElementById('optionsBox');
var selectedBox = document.getElementById('selectedBox');
// 1. 创建选项元素
goodsDetail.forEach(function (dlItem, dlIndex) {
var dlEle = document.createElement('dl');
dlEle.dataset.index = dlIndex;
optionsBox.appendChild(dlEle);
var dtEle = document.createElement('dt');
dtEle.innerHTML = dlItem.title;
dlEle.appendChild(dtEle);
dlItem.data.forEach(function (ddItem, ddIndex) {
var ddEle = document.createElement('dd');
ddEle.innerHTML = ddItem.type;
ddEle.dataset.groupIndex = dlIndex; // 组索引挂到 dd 上
if (ddIndex === 0) ddEle.classList.add('active'); // 默认选中每组第一个
dlEle.appendChild(ddEle);
});
});
// 2. 固定长度数组:索引 = 组索引,长度 = 组数
var selectedArr = new Array(goodsDetail.length);
// 3. 根据数组创建选中标签(渲染前先清空)
function createSelectedTag() {
selectedBox.innerHTML = ''; // 不清空会一直追加,越点越多
selectedArr.forEach(function (ddEle) { // 稀疏数组,forEach 会自动跳过空位
var tagEle = document.createElement('div');
tagEle.className = 'selected-tag';
tagEle.innerHTML = ddEle.innerHTML + '<span class="close">×</span>';
selectedBox.appendChild(tagEle);
});
}
// 4. 点击:选中 + 排他 + 存数组 + 重新渲染
optionsBox.addEventListener('click', function (e) {
if (e.target.nodeName !== 'DD') return;
var siblings = e.target.parentElement.children;
for (var i = 1; i < siblings.length; i++) {
siblings[i].classList.remove('active');
}
e.target.classList.add('active');
// 用组索引作为下标:同组的 dd 互相覆盖 => 同组互斥 + 永远按组顺序
selectedArr[e.target.dataset.groupIndex] = e.target;
createSelectedTag();
});
// 5. 初始化:把默认选中的第一个 dd 也放进数组并渲染
optionsBox.querySelectorAll('dl').forEach(function (dl) {
selectedArr[dl.dataset.index] = dl.querySelector('dd');
});
createSelectedTag();
</script>
</body>
</html>关键行与易错点
- 数组用
new Array(goodsDetail.length)定长,下标就是组索引:颜色组永远落在[0]、内存组落在[1]……所以同组选项互相覆盖(互斥),不同组各占一格(顺序固定),先点后面的组也不会乱序。 - 点击时拿不到
forEach的index,索引必须在创建 dd 时用ddEle.dataset.groupIndex = dlIndex存下来,点击时再e.target.dataset.groupIndex取(data-group-index会自动转小驼峰)。 selectedArr[i] = ddEle只是给元素多了一个引用,不会把元素从 DOM 里挪走(和appendChild移动元素是两回事)。selectedArr是稀疏数组(跳着填的),只能用forEach遍历,它会自动跳过空位;用普通for会拿到undefined而报错。createSelectedTag开头必须selectedBox.innerHTML = ''先清空,因为每次都是按数组“整体重画”,不清空就会重复追加。- 排他循环仍要从
i = 1开始跳过dt;children是HTMLCollection,不能用forEach。
常见错误
- 现象:点空白、DT 或分隔区域也触发选中逻辑。→ 原因:事件委托后未判断实际点击目标。→ 修法:在委托回调中先判断
e.target.tagName是否为DD,不是则直接返回。 - 现象:同组排他时把 DT 也处理了,或 active 清理不干净。→ 原因:直接遍历 DL 的全部子元素,未区分 DT/DD。→ 修法:从 DD 起始位置遍历,或筛选出 DD 后再清理同组 active,最后给当前
e.target加 active。 - 现象:连续点击后选中标签重复、顺序错乱、同组不互斥。→ 原因:每次点击直接向容器追加标签,没有统一状态源。→ 修法:用固定长度数组按组索引保存被选中的 DD,渲染前先清空标签容器,再遍历数组重建。
- 现象:读取组索引得到
undefined。→ 原因:索引属性加在 DL 上,却在 DD 上直接读dataset。→ 修法:统一属性挂载位置;若加在 DL 上则读e.target.parentElement.dataset.index,若加在 DD 上则直接读e.target.dataset.groupIndex。 - 现象:用
forEach遍历 DOM 集合报错,或用普通for遍历稀疏数组出现空位异常。→ 原因:HTMLCollection没有forEach;稀疏数组索引不连续。→ 修法:DOM 集合转数组或用普通循环;稀疏数组用forEach,空位会自动跳过。 - 现象:默认选中第一个 DD 后,只有 active 样式,没有对应选中标签。→ 原因:只改了视图状态,没有同步数组和渲染函数。→ 修法:初始化默认选中时同时写入
selectedArr,再调用统一渲染函数生成标签。
练习
- 事件委托为什么绑定到
optionsBox而不是新创建的 DD 或 DL?点击 DT 时如何保证不进入后续逻辑? selectedArr的长度为什么取组数?点击同组不同 DD 时,数组对应索引会发生什么变化?- 将组索引从 DL 的
data-index改为每个 DD 的data-group-index,需要修改哪些读写代码? - 默认选中每组第一个 DD 时,如何保证 active 样式、
selectedArr、选中标签三者一致? - 如果要求再次点击已选中的 DD 可以取消选中,数组和渲染逻辑应如何调整?
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 商品参数选项-创建选项元素 | 08:10 |
| 2 | 商品参数选项-点击选项元素选中 | 10:55 |
| 3 | 商品参数选项-使用数组存储被选中的选项 | 22:12 |
| 4 | 商品参数选项-根据数组创建选中标签元素 | 08:55 |
小结
- 选中状态应集中保存在数组中,DOM 只作为状态的渲染结果。
- 固定组索引决定选中标签的顺序,同组覆盖实现互斥。
- 事件委托要过滤
e.target,只处理真正的 DD。 - 同组排他应先清理同组 active,再设置当前项。
- 选中标签每次应按数组全量重建,重建前先清空容器。
- 组索引属性挂载位置必须与读取位置一致。
- 默认选中必须同步状态、样式和标签渲染。
- 数组保存的是 DOM 引用,不是把元素从原位置移动。