
先问一个特别现实的问题你在写前端代码的时候有没有遇到过这种需求——页面上有一堆商品卡片点击之后要弹窗展示详情但卡片的HTML早就渲染完了你不想再反查接口拿数据只想把商品ID、名称、价格这些信息直接“挂在”卡片对应的DOM节点上。很多同学的第一反应是随便写几个自有属性比如item-id、product_name然后用getAttribute(item-id)去取。其实HTML5早就给了我们一套更正统、更好用的方案就是>div>div>const id el.getAttribute(data-id); el.setAttribute(data-name, 键盘);现代浏览器原生支持一个更直观的接口Element.dataset。它会把元素上所有>div idproduct>const product document.getElementById(product); // 读取 console.log(product.dataset.id); // 1024 console.log(product.dataset.name); // 无线机械键盘 console.log(product.dataset.price); // 399 // 修改 product.dataset.price 329; product.dataset.discount 20%; // 新增属性对应>function getAllDataAttrs(el) { const result {}; Array.from(el.attributes).forEach(attr { if (attr.name.startsWith(data-)) { const key attr.name.slice(5).replace(/-([a-z])/g, (_, c) c.toUpperCase()); result[key] attr.value; } }); return result; }这个函数自己实现了从>.order-card[data-statuspending] { border-left: 4px solid #fa8c16; } .order-card[data-statuspaid] { border-left: 4px solid #52c41a; background-color: #f6ffed; } .order-card[data-statuscanceled] { border-left: 4px solid #d9d9d9; background-color: #fafafa; color: #999; }JavaScript里只需要做一件事function updateOrderStatus(card, status) { card.dataset.status status; }整个视觉切换完全由CSS根据属性值自动匹配业务逻辑里再也看不到任何className拼接的痕迹。这个思路的本质是把“状态”从视觉类名中抽离出来变成一个独立的数据维度。好处非常明显样式层的变化只影响样式数据的变化只影响数据两者通过属性选择器这一层解耦。后续你要新增一个状态只需要在CSS里加一条规则JavaScript一行都不用改。3.2 attr()函数把数据渲染到伪元素CSS里还有一个和>button classtooltip-btn>.tooltip-btn { position: relative; } .tooltip-btn::after { content: attr(data-tooltip); position: absolute; bottom: calc(100% 8px); left: 50%; transform: translateX(-50%); padding: 4px 8px; background-color: rgba(0, 0, 0, 0.75); color: #fff; border-radius: 4px; font-size: 12px; white-space: nowrap; opacity: 0; pointer-events: none; transition: opacity 0.2s; } .tooltip-btn:hover::after { opacity: 1; }这段CSS不需要JavaScript参与提示内容就来自>div classorder-card>.order-card { width: 300px; padding: 16px; border: 1px solid #eee; border-radius: 8px; margin-bottom: 12px; transition: background-color 0.3s; } .order-card[data-statuspending] { background-color: #fffbe6; border-color: #ffe58f; } .order-card[data-statuspending] .card-status::before { content: 待支付; color: #d48806; } .order-card[data-statuspaid] { background-color: #f6ffed; border-color: #b7eb8f; } .order-card[data-statuspaid] .card-status::before { content: 已支付; color: #389e0d; } .order-card[data-statuscanceled] { background-color: #fafafa; border-color: #e8e8e8; } .order-card[data-statuscanceled] .card-status::before { content: 已取消; color: #999; }JavaScript只负责维护状态值// 模拟把订单状态更新为“已支付” const card document.querySelector(.order-card); card.dataset.status paid;页面刷新后背景色、边框色、状态文案都会跟着>div classchart>document.querySelectorAll(.chart).forEach(chartEl { const config { type: chartEl.dataset.chartType, api: chartEl.dataset.api, title: chartEl.dataset.title }; initChart(chartEl, config); });以后要新增一个图表复制的就是一个带>table idorderTable tr>const table document.getElementById(orderTable); table.addEventListener(click, (event) { const btn event.target.closest([data-action]); if (!btn) return; const tr btn.closest(tr); const orderId tr.dataset.orderId; const status tr.dataset.status; const action btn.dataset.action; switch (action) { case confirm: handleConfirm(orderId, status); break; case refund: handleRefund(orderId); break; case detail: showDetail(orderId); break; } });closest([data-action])会从触发事件的目标元素向上找最近的带>a href/product/1024>document.addEventListener(click, (event) { const tracker event.target.closest([data-track]); if (!tracker) return; const eventName tracker.dataset.track; const params tracker.dataset.trackParam; let parsedParams {}; if (params) { try { parsedParams JSON.parse(params); } catch (e) { console.warn(埋点参数解析失败:, params); } } report(eventName, parsedParams); });业务同学只需要在模板上写>div classcollapse-wrap button classcollapse-trigger>.collapse-panel { height: 0; overflow: hidden; transition: height 0.3s ease; }JavaScriptdocument.querySelectorAll(.collapse-trigger).forEach(trigger { trigger.addEventListener(click, function () { const targetSelector this.dataset.target; const target document.querySelector(targetSelector); if (!target) return; const isCollapsed this.dataset.collapsed true; const duration Number(target.dataset.animationDuration || 300); target.style.transition height ${duration}ms ease; if (isCollapsed) { target.style.height target.scrollHeight px; this.dataset.collapsed false; } else { target.style.height 0px; this.dataset.collapsed true; } }); });这里有个细节值得单独说this.dataset.collapsed true是判断收起状态因为dataset里的值是字符串true或false直接写this.dataset.collapsed true永远不成立。而Number(target.dataset.animationDuration || 300)则做了字符串到数字的安全转换还顺带处理了未配置时的默认值。这个案例虽然小却把>$0.dataset就能看到这个元素上所有>const el document.getElementById(someId); console.log(el.getAttributeNames());这样能直接看到HTML解析后真实的属性名是什么样。如果发现属性名里有小写字母就不要再按驼峰去猜了。5.2 字符串陷阱类型转换问题dataset返回的一律是字符串这一点带来了很多隐蔽的bug。第一个坑是数字比较。假设>if (Number(el.dataset.price) Number(el.dataset.baseline)) { // 才符合直觉 }第二个坑是布尔值判断。>let config; try { config JSON.parse(el.dataset.config); } catch (e) { console.error(data-config解析失败:, el.dataset.config); config {}; }项目里如果封了一个通用的readDataAttr工具函数把类型转换和异常处理都放在里面会省去大量重复的防御代码。5.3 性能、SEO和滥用问题先说结论>