ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

DOM操作精讲:内容、样式与属性修改指南

DOM操作精讲:内容、样式与属性修改指南 做前端开发的人最早开始“玩”JavaScript几乎都是从DOM操作入手的。哪怕是很多年后用着Vue、React组件里写的其实还是“改内容、改样式、改属性”这套底子只是框架帮你包好了一层。这篇课程九我们就专门把DOM操作中的“修改内容 / 修改样式 / 修改属性”这三件事彻底讲透什么时候用哪个API、它们的区别和坑在哪、实际项目里怎么组合着用。我在写这篇之前翻了一圈热词发现“dom型xss”“虚拟dom和diff算法”“javascript判断数据类型”这些词频繁出现说明大家已经从“多会几个API”进化到“关心底层原理和安全边界”了。这正是这篇要解决的事——不仅给你能抄的代码还告诉你为什么要这么写。适合刚学完JS语法、准备踏上页面交互之路的初学者也适合那些用了很久框架却想回头夯实基座的开发者。1. 先说思路DOM操作到底在操作什么1.1 从DOM树说起页面为什么能被JS“摆弄”浏览器拿到HTML之后会解析成一棵由节点组成的树这棵树就是DOMDocument Object Model文档对象模型。你看到的每个标签都是树上的一个元素节点标签里的文字是文本节点标签的属性挂在元素节点上注释也有自己的节点类型。JavaScript之所以能“控制页面”本质就是因为它能拿到这棵树的任意节点然后把节点上的内容、样式、属性改掉。这个模型很符合直觉你想改页面上的某个按钮文字就先找到这个按钮节点再修改代表文字的那个属性你想把某个区块背景变红就找到这个区块节点改它的style或class你想给链接加上target_blank也是先拿到节点再设置对应的属性。理解了“DOM是一棵树”这个前提很多操作就有了统一的思考路径先定位要操作的节点getElementById、querySelector之类再决定要改它的哪一层内容、样式、属性。这三个层面互不冲突经常是同时发生的。比如一个点赞按钮点击后既要把“点赞”改成“已赞”内容又要把按钮颜色从灰色变红样式还要更新data-state这个标记状态的自定义属性属性。1.2 修改内容、样式、属性的三条主线我把常用API按这条主线整理过一次之后发现真正高频的其实就三类修改内容textContent、innerHTML、innerText、insertAdjacentHTML修改样式element.style.xxx、element.className、classList.add/remove/toggle、getComputedStyle修改属性setAttribute、getAttribute、removeAttribute、dataset很多人学到这里就卡住了因为每个方向都有好几个API不知道优先用哪个。我的建议是先记住“各管一摊”的原则能用属性API改的就别用样式API硬怼能用class控制的就别直接改内联style能用textContent处理纯文本的就别碰innerHTML。这个原则能避免九成以上的安全问题和维护问题。后面几个章节我会把每个方向的API掰开揉碎说清楚它们之间的差别和选型依据。2. 修改内容textContent、innerHTML与innerText的取舍2.1 三兄弟对比取内容与设内容该用谁内容修改最常见的需求有两个读取节点里的文字把一段新的内容塞进去。新手最容易在这三个API上犯迷糊textContent、innerText和innerHTML。表面看都能读写内容实际差别非常大。我整理了一张经常用来给学员讲课的对比表API是否保留HTML标签是否触发渲染性能安全性textContent不保留当纯文本处理不触发直接改文本好安全innerText不保留会受CSS渲染影响比如隐藏元素读不到内容中安全innerHTML保留并能解析HTML会触发解析与渲染中有XSS风险textContent的核心优势是安全。它把赋值的内容一律当作文本处理哪怕字符串里带img onerroralert(1)它也只会原样显示成文字绝不会执行。对展示用户输入数据的场景评论列表、用户名、搜索关键词这是最稳妥的选择。innerHTML的优势是灵活。需要一次性插入一大段带结构的HTML时它确实高效比如渲染一个表格、一批卡片。代价是容易被人利用热词里“dom型xss”指的就是这类漏洞攻击者找到某个页面把用户输入直接拼进innerHTML的位置塞入一段恶意脚本你毫无防备就把它执行了。网上很多案例都是因为开发者图省事把后端返回的数据直接拼成HTML字符串结果一串引号就炸了。innerText我更愿意把它理解成“取出来给用户看的内容”。它会考虑CSS的渲染表现比如display:none元素里的文字用textContent能读到但用innerText读不到。绝大多数业务场景读文案用textContent更可控因为它的行为是纯文本层面的不受样式影响结果更可预测。2.2 动态创建与插入节点别再只盯着innerHTML如果每次插入内容都用innerHTML拼字符串代码会越来越难维护而且容易产生XSS。原生API里其实有完整的一套“创建—插入”组合拳用好了既清晰又高效。基础组合是document.createElement配合appendChildconst li document.createElement(li); li.textContent 新任务; li.className todo-item; list.appendChild(li);这套写法的优点和“在word里用样式而不是手动调格式”是一个道理你要创建什么就是什么节点节点的文本、属性、子节点都是分开设置的逻辑一目了然而且全程没有HTML字符串天然避免注入问题。但我更推荐你掌握另一个APIinsertAdjacentHTML。它能精确控制插入位置解决了appendChild只能把新节点塞到末尾的痛点// 在元素内部开头插入 box.insertAdjacentHTML(afterbegin, p第一段/p); // 在元素内部末尾插入 box.insertAdjacentHTML(beforeend, p最后一段/p); // 在元素前面插入 box.insertAdjacentHTML(beforebegin, h2标题/h2); // 在元素后面插入 box.insertAdjacentHTML(afterend, p结尾语/p);如果插入的内容里含有纯文本变量我建议用textContent创建一个文本节点或者至少在拼接前做一次转义。记住这个习惯后你会发现动态渲染列表不再是“要么innerHTML要么框架”的二选一原生方案也能写得很清晰。3. 修改样式style、classList与getComputedStyle的正确用法3.1 直接改style小范围修改的利器修改元素样式最直观的写法是const tip document.getElementById(tip); tip.style.color #ff5722; tip.style.backgroundColor #fff3e0; tip.style.padding 12px 20px;注意两个细节。第一属性名从CSS里的background-color变成了驼峰式backgroundColor因为style对象是JavaScript对象中划线改名是必然的。第二style只能操作内联样式优先级很高但如果元素本身有一个CSS规则用!important内联样式也压不住。设置单个属性没问题但手写多个属性时代码就很啰嗦了而且每次都触发一次样式重算。如果一组样式要一起改用cssText一次性赋值性能更好tip.style.cssText color:#ff5722; background-color:#fff3e0; padding:12px 20px;;必须提醒的是cssText会把style里已有的所有内联样式清空再写入你给的整串内容。所以它适合给一个“样式干净”的元素做一次性编排不适合在已有内联样式的基础上微调。3.2 classList现代前端最推荐的样式切换方案直接操作style适合“只改一两个属性”的场景但真实项目里几乎没人这么干。原因很简单样式是设计体系的事应该放在CSS里统一管理JavaScript只负责“切换状态”。如果JS里到处写死颜色和尺寸后续改视觉稿就得翻遍JS文件找魔法数字谁碰都得骂娘。classList就是为这个场景生的。它提供了一组“增删查”方法const btn document.querySelector(.btn); // 给元素追加一个class btn.classList.add(btn--active); // 去掉一个class btn.classList.remove(btn--active); // 有就删、没有就加用于切换状态 btn.classList.toggle(btn--active); // 判断是否存在某class返回布尔值 const isActive btn.classList.contains(btn--active);拿切换按钮状态来说CSS里写.btn { background: #ccc; color: #333; } .btn--active { background: #ff5722; color: #fff; }JS只需要btn.classList.toggle(btn--active);一个方法就能完成所有交互切换不用关心具体样式值。这也解释了为什么框架里的动态样式绑定底层思路都是“把class和状态关联起来”。用class控制样式相当于给元素建立了一套“状态机”可读性和可扩展性都远超一条条写内联style。classList.add的一次添加多个class也可以classList.add(a, b)i.e. 支持多个参数实测很方便。但注意classList方法对IE10及以下不友好如果你还在维护古董浏览器环境可能需要用className的字符串替换方案。如今正常业务基本不用担心这个。3.3 读取计算样式getComputedStyle的两个坑改样式相对简单读样式却有一个经典大坑直接用element.style.color只能读到你用JS或样式属性写死的内联样式读不到CSS样式表里生效的样式。原因是style对象对应的是HTML里的style属性而不是元素最终的渲染结果。要拿到“最终渲染后的样式”得用getComputedStyleconst styles window.getComputedStyle(box); const color styles.color; // 永远是rgb()格式 const height styles.height; // 带px单位两点必须注意。第一它返回的对象是只读的你想styles.color #000是无效的不会报错但也不会有任何作用。第二它返回的具体值会受浏览器的单位换算影响比如你写了fontSize: 16px读出来可能就是fontSize: 16px但颜色一定是rgb()形式直接拿去给Canvas用反而更方便如果想拿HEX格式还得做一次格式转换。getComputedStyle的典型使用场景是——检验某个动画是否生效、测量元素实际尺寸、判断某个元素当前处于哪个样式状态。比如你做滚动吸顶效果需要判断position是否已经变成fixed就只能靠它。4. 修改属性setAttribute、property与dataset的边界4.1 标准属性的两种修改方式修改属性的入口有两个直接访问元素对象的属性el.href或者用属性APIel.setAttribute(href, ...)。初学者很容易混淆这两个方式因为它俩在多数情况下效果一样但有些场景差别巨大。const link document.getElementById(link); // 方式一直接修改property link.href /page/about; // 方式二使用attribute API link.setAttribute(href, /page/about);处理id、href、src这些标准属性时两者基本等价。但要注意直接访问property返回的往往是处理后的值比如link.href会自动补全成完整URL而getAttribute(href)返回的是HTML源码里写的原始字符串。再比如复选框的checked这两套逻辑会出现经典分歧——checkbox.checked true能改变选中状态并同步到界面checkbox.setAttribute(checked, )有时候只是加了一个属性标记在部分场景下不会正确改变选中态。我的建议很朴素涉及表单元素的当前状态value、checked、selected、disabled优先操作property涉及自定义数据、以及需要同时管理多个非标准属性时才用attribute API。4.2 property与attribute的不同大多数bug的起源很多令人头秃的bug根源都是分不清property和attribute。用一个例子说明页面上有个输入框input idname value张三用户手动在输入框里把内容改成“李四”。input.value读取的是“用户当前输入的值”是李四。input.getAttribute(value)读取的是“HTML属性里写的初始默认值”是张三。如果你用input.value 王五改值property变了attribute不变但界面显示王五。如果你用input.setAttribute(value, 王五)改属性部分浏览器下property会跟着变界面也会变但这种方式容易出幺蛾子。深层原因是property是JavaScript对象上的一个字段attribute是HTML文档中的一个标记。标准属性在两者之间建立了“联动手感”但联动方向和时机各有不同比如value属性只有初始值时才会同步到property之后就分道扬镳了。React/Vue等框架在表单上会有一堆防坑设计也都是因为这些边界存在。实际开发时判断一个属性该用哪边只看需求要反映用户当前操作过的最新状态用property要读取或重置初始配置用attribute。4.3 自定义属性data-*与dataset的正确打开方式非标准属性不能随便往HTML里塞因为HTML解析器有自己的规范但>div classcard>const card document.querySelector(.card); const id card.dataset.id; // 1024 const userType card.dataset.userType; // vip card.dataset.id 2048; card.dataset.status active;命名规则HTML里是>const btn document.querySelector(button[data-loading]); btn.removeAttribute(disabled); btn.removeAttribute(data-loading);想要“批量清空一堆自定义属性”也可以遍历。这段代码会把元素上所有>[...el.attributes] .filter(attr attr.name.startsWith(data-)) .forEach(attr el.removeAttribute(attr.name));5. 实战用一个待办清单串起所有知识点5.1 需求描述与页面结构概念聊再多不如一个案例来得直接。我手写过一个非常典型的待办清单Demo它同时覆盖了内容、样式、属性三种修改是整个基础课程里我最建议你亲手敲一遍的例子。需求很简单输入框输入文字按回车或点击按钮添加一条待办点击待办划掉并变灰再点一次取消划掉每条待办后面都有删除按钮点击删除这条底部统计“未完成数量”HTML结构如下div idapp input idtodoInput typetext placeholder输入待办事项 button idaddBtn添加/button ul idtodoList/ul p idcountText未完成0 项/p /div5.2 核心代码与解析const input document.getElementById(todoInput); const addBtn document.getElementById(addBtn); const list document.getElementById(todoList); const countText document.getElementById(countText); // 添加一条待办 function addTodo() { const text input.value.trim(); if (!text) return; const li document.createElement(li); li.className todo-item; li.dataset.done false; // 使用data-*记录完成状态 const span document.createElement(span); span.textContent text; // 纯文本插入不拼innerHTML防XSS const delBtn document.createElement(button); delBtn.textContent 删除; delBtn.className del-btn; li.appendChild(span); li.appendChild(delBtn); list.appendChild(li); input.value ; updateCount(); } // 切换完成状态 function toggleDone(li) { li.classList.toggle(done); // 样式切换走classList const done li.dataset.done true; li.dataset.done done ? false : true; // 属性状态同步 updateCount(); } // 删除一条待办 function removeTodo(li) { li.remove(); // 现代浏览器直接移除节点 updateCount(); } // 更新未完成数量 function updateCount() { const items list.querySelectorAll(.todo-item:not(.done)); countText.textContent 未完成${items.length} 项; // 修改内容 } // 事件委托点击list内的任意按钮或span都由list统一处理 list.addEventListener(click, (e) { const li e.target.closest(li); if (!li) return; if (e.target.classList.contains(del-btn)) { removeTodo(li); } else { toggleDone(li); } }); // 回车添加 input.addEventListener(keydown, (e) { if (e.key Enter) addTodo(); }); addBtn.addEventListener(click, addTodo);配套CSS只需两行就能体现classList的优势.todo-item { cursor: pointer; padding: 6px 12px; } .todo-item.done { text-decoration: line-through; color: #aaa; }5.3 关键环节的思考为什么这样设计这个案例值得你反复揣摩的有四处。第一插入文本时用的是textContent而不是innerHTML。因为输入框的内容是用户可操控的用innerHTML拼接等于把XSS漏洞亲手打开。注意我这里是先createElement再加textContent每一步都清晰可拆解。第二完成状态的切换由三部分组成classList.toggle改视觉、dataset.done改数据标记、updateCount()改底部文本。三者分工不同但都服务于同一个状态变化。这其实就是框架中“状态驱动视图”思想的原始形态。第三事件委托。我没有给每个删除按钮单独绑定点击事件而是让ul监听所有点击再利用closest找到对应li。动态新增的元素无需重新绑定事件这个习惯在大型列表里能省下大量内存和代码。第四统计未完成数量用的是CSS选择器.todo-item:not(.done)。这个思路比在JS里遍历数组再判断状态干净得多相当于把部分业务逻辑“下沉”给了CSS让查询代码保持极简。6. 常见问题与排查技巧实录6.1 六个高频问题速查表我把实操过程中学员咨询最多的六个问题整理成了表格每一个我都亲自调试并验证过问题现象可能原因排查思路与解决内容改了但页面不更新选错了DOM节点或者改完又被其他逻辑覆盖在console.dir(el)里确认是哪个节点检查是否有后续赋值style.color改了没反应有CSS规则用了!important或者样式加到了错误元素上打开DevTools看Computed样式确认优先级classList.add之后样式没变CSS文件没加载或者类名写错/样式选择器不匹配检查CSS选择器是否包含元素本身的class前缀getAttribute(value)一直拿不到最新输入值混淆了attribute与property输入内容用el.value不要用getAttribute(value)动态创建的按钮点击无反应忘了用事件委托直接绑定了不存在的元素把监听器放到父级容器用closest找目标移除元素后仍有残留空白只删除了子节点的一部分或文本节点还在el.remove()或el.innerHTML 后再统计子节点6.2 性能坑位为什么批量更新不要频繁操作DOM热词里老有人问虚拟DOM和diff算法其实虚拟DOM的出现恰恰是因为原生DOM操作有个致命弱点每一次节点改动都可能触发浏览器的重排layout和重绘paint频繁改动会带来肉眼可见的卡顿。我举一个经典性能反例往列表里循环插入5000条数据如果用循环里逐条appendChild页面会明显卡顿改用DocumentFragment一次性挂载性能就稳了const fragment document.createDocumentFragment(); for (let i 0; i 5000; i) { const li document.createElement(li); li.textContent 第 i 条; fragment.appendChild(li); } list.appendChild(fragment); // 只一次性操作真实DOM原因在于DocumentFragment是一个“虚拟的临时容器”它存在于内存中不在页面渲染树里。先把节点全部塞进Fragment最后挂一次到页面浏览器只经历一次重构。这个思路和虚拟DOM的“批量比对、统一提交”是相似的哲学只是层面不同。6.3 我的几个避坑经验最后分享几条我自己的实操经验都是踩过坑之后才固化成习惯的。第一能用className就用className能merge class就用classList不到万不得已别直接写style。我接手过一些老项目JS里到处是el.style.marginLeft x px改起视觉稿来崩溃。把“变化的状态”抽象成class把“具体的样式值”留在CSS里代码寿命至少翻一倍。第二凡是涉及用户输入内容的地方一律默认用textContent创建文本节点。“斗地主先出4”容易判断字符串里有没有恶意脚本却很难。与其写转义工具函数不如从源头断开注入路径。如果你确实需要插入HTML就先把所有动态部分转义再拼接。第三事件委托不是高级技巧而是基础习惯。凡是会频繁新增/删除子元素的容器都建议把事件监听挂到容器上。这个习惯能避开动态元素的绑定陷阱也让代码结构更简洁。配合closest方法之后事件处理的优雅程度直接翻倍。第四不要动不动就刷新整块区域。很多人改了某个节点的文本习惯性去重绘整个列表导致不必要的开销。能用textContent精准更新就精准更新能用classList最小化变更就最小化变更。DOM操作不是越少越好但一定是越精准越好。第五用dataset结合attribute状态维护数据展示的一致性。当某个交互需要同时改数据标记和界面表现时先把数据标记更新再把界面同步过去顺序别反。否则界面已经变蓝了后端要的状态数据还是旧值排查起来非常费劲。如果后续想继续深入建议沿着“事件机制 → 事件循环 → 虚拟DOM原理”这条线往下走。但别着急先把DOM操作这三板斧练熟练透你写任何框架代码时都会有大不一样的底气。
返回列表