03元素修改

上一课我们学会了「找到元素」——querySelector 把元素抓回手里。 接下来第二步就是「改它」:改文字 textContent、 改 HTML innerHTML、改类名 classList、改样式 style、 改任意属性 attributes。这一课一次收齐,从此告别「只会改文本」的弱鸡玩法。

约 22 分钟 6 个动手实验 DOM 第二步:修改

一、内容三件套:textContent / innerText / innerHTML

想改元素里面的「东西」,有三个长得像的 API——它们名字像, 脾气却完全不一样。textContent 是老实人, innerHTML 是魔法师,innerText 是受样式影响的「近视眼」。

textContent

纯文本模式。原样输出,不会解析 HTML,永远安全。

innerHTML

HTML 模式。字符串里的标签会被解析渲染,强大也危险。

innerText

受 CSS 影响。display:none 的元素读不到,性能也最差。

三 API 对比:性能 / 安全 / 是否解析 HTML / 返回值
API解析 HTML?受 CSS 影响?安全性性能推荐度
textContent✗ 当作纯文本✗ 不受影响 安全,自动转义✓ 最快默认首选
innerHTML✓ 解析为 DOM✗ 不受影响 ✗ 用户输入会 XSS中等场景合适再用
innerText✗ 纯文本✓ 受 display:none 影响 安全✗ 最慢(要重排)几乎不用
全部当文本 */ el.innerHTML = html; /* 渲染:前端 加粗;script 会立刻执行(XSS) */ el.innerText = html; /* 渲染:同 textContent,但内部触发回流,性能差 */
innerHTML 是 XSS 的头号大门。 如果字符串里有 <script> 标签,赋值时立刻执行。 凡是来自用户输入、URL 参数、接口数据的内容—— 永远先用 textContent,或者用 textContent + DOM API 拼接。

二、textContent 安全改造

日常 80% 的场景用 textContent 就够了。 它干三件事:设文本 / 清空内容 / 读取文本。 读取时返回元素「所有后代」的文本拼接,<style> 和 <script> 里的内容也算。

点按钮三连:① 写入一段文字(含尖括号、纯文本显示);② 清空;③ 读取并打印当前 textContent——把 "写点啥都行 <b>前端</b>" 改成你自己的内容
清空一个元素有三种写法:el.textContent = ''(最快)、
el.innerHTML = ''(最常见)、el.replaceChildren()(ES2022,语义最清)。 性能上 textContent 略胜一筹。

三、innerHTML 强大但要小心

innerHTML 是把字符串当 HTML 解析再插入, 适合「结构复杂的整块 HTML 字符串」。它的优势是一次写入、结构自由; 代价是不安全——用户输入直接插等于 XSS。

textContent vs innerHTML:何时选谁
场景推荐理由
改个标题、改段文字textContent快、安全、不想要标签
用户输入、表单回显textContent绝对安全第一
服务端返回的可信 HTML 片段innerHTML结构复杂,字符串拼接方便
清空元素两者皆可前者更快,后者更常见
大块内容一次性写入innerHTML实测 innerHTML 大块时反而更快
点按钮看三场景对比:① 简单文本写入;② 复杂 HTML 结构一次写入;③ 用户输入直接走 innerHTML——警惕 XSS。把用户输入框改成你自己的字符串
XSS 三件套:<script> 直接执行、 <img onerror> 触发、<a href="javascript:..."> 跳转。 用户输入永远走 textContent 或 DOMPurify.sanitize(), 别心存侥幸。

四、classList:类名管理神器

改元素的 class 不要再写 el.className += ' active' 了—— classList 是 DOM 给你的类名管理神器, 五个方法干所有事情。注意 classList 是 DOMTokenList,不是数组(没有 map、filter)。

classList 五个方法:怎么用、何时用
方法作用示例
add(cls)添加类(已存在则跳过)el.classList.add('active')
remove(cls)移除类(不存在也不报错)el.classList.remove('hidden')
toggle(cls, ?force)有则删、无则加;force=true 强制加el.classList.toggle('on', true)
contains(cls)是否存在某类(返回布尔)el.classList.contains('active')
replace(old, new)替换类(不存在则返回 false)el.classList.replace('a', 'b')
点按钮操作舞台方块:① add 新类;② remove 旧类;③ toggle 来回切;④ contains 检测;⑤ replace 改名字。每次操作后实时打印 Array.from(classList) ——把 box 改成 warning 看效果变化
toggle 的第二参数非常实用: el.classList.toggle('on', isMobile)——根据条件强制加/减。 想切换主题?写 document.body.classList.toggle('dark') 一行搞定。

五、style 行内样式

想让元素动态变色、变大、移动?用 el.style.xxx。它直接改元素的 行内 style 属性,等同于 HTML 里写 style="color: red"。驼峰命名是唯一规则。

style 对象三大写法:单属性 / 复合属性(驼峰)/ CSS 变量
写法对应 CSS示例
单属性color / font-sizeel.style.color = 'red'
复合属性(驼峰)background-color / font-weightel.style.backgroundColor = '#fff'
transform / 动画transform / transitionel.style.transform = 'scale(1.1)'
CSS 变量--main-colorel.style.setProperty('--x', '5px')
批量赋值—el.style.cssText = 'color:red; font-size:18px;'
点按钮三连改方块:① 改单属性(颜色/字号);② 改 CSS 变量(--accent);③ 改 transform 复合属性——注意 CSS 变量必须用 setProperty
style 只能改行内,读不到 CSS 类里的样式—— 想读「最终生效值」用 getComputedStyle(el).color。 改 transform 时整段赋值: el.style.transform = 'translateX(10px) scale(1.1)',不要单写一项。

六、attributes 任意属性

除了 class 和 style,元素还有一堆任意 HTML 属性: id / href / src / disabled / data-*… 四个方法搞定一切:getAttribute / setAttribute / removeAttribute / hasAttribute。

属性操作四大方法 + dataset 糖 + 布尔属性速查
API作用示例
getAttribute(name)取值(不存在返回 null)el.getAttribute('href')
setAttribute(name, val)赋值el.setAttribute('id', 'main')
removeAttribute(name)移除el.removeAttribute('disabled')
hasAttribute(name)是否存在(布尔)el.hasAttribute('hidden')
dataset.foo读 data-foo(驼峰)el.dataset.userId ↔ data-user-id
布尔属性hidden / disabled / checkedel.hidden = true(直接属性更顺)
点按钮操作四种属性:① set/get 普通属性;② dataset 取 data-user-id;③ hasAttribute 检测;④ hidden 布尔属性切换——把 data-user-id 改成 data-product-id 同步改 dataset 字段
dataset 是 DOM 给 data-* 属性开的语法糖: data-product-id ↔ el.dataset.productId(连字符变小驼峰)。 自定义状态、配置项、Vue 的 data-v-xxx 都靠它传递。

七、innerHTML vs DocumentFragment:批量插入的性能差距

想往页面塞 1000 个 <li>?两种写法天差地别: 逐个 appendChild 触发 1000 次重排,浏览器累死; DocumentFragment 是「内存中的临时容器」, 攒完一波只触发 1 次重排。差距肉眼可见。

三种插入方式性能对比:1000 个节点耗时参考
方式DOM 写入次数重排次数1000 项参考耗时
逐个 appendChild1000 次可能 1000 次慢(10ms+)
innerHTML 一次写1 次1 次最快
DocumentFragment 批量1 次1 次接近 innerHTML,可控性更强
点运行测三种插入方式的耗时:① 1000 次 appendChild;② innerHTML 一次写;③ DocumentFragment 批量。输出两个耗时(毫秒,数字越小越快)——把 N 改成 5000 看看差距是否更大
循环里不要直接 appendChild。 如果一定要动态创建,先丢进 DocumentFragment,最后一次挂载。 性能差距在 1000 项以上肉眼可见,1 万项可能差10 倍。

八、本章小结

  • 改文本默认用 textContent:安全、快、不解析 HTML。
  • innerHTML 强大但有 XSS 风险:用户输入绝不走 innerHTML,走 textContent 或 DOMPurify。
  • classList 的五个方法(add/remove/toggle/contains/replace)替代老旧的 className 拼接。
  • el.style.xxx 改行内样式,复合属性驼峰命名;CSS 变量走 setProperty。
  • 任意属性:getAttribute/setAttribute/removeAttribute/hasAttribute 四件套;data-* 用 dataset 取。
  • 大量 DOM 插入用 DocumentFragment 批量构建,最后一次挂载,避免多次重排。
  • innerHTML 大块写入反而比逐个 appendChild 快;但可控性差。
动手练习: ① 用 classList 切主题——做一个按钮,点击 document.body.classList.toggle('dark'), 配合 CSS 让 .dark 覆盖背景与文字色; ② 用 dataset 存数据——给 5 张卡片各自加 data-id data-name, 点击后用 JSON.stringify(card.dataset) 打印出来; ③ 用 DocumentFragment 批量插入——写一个循环生成 200 条留言(每条含头像、昵称、内容三段), 先丢进 Fragment 再一次性挂到列表,体会「不抖」的渲染; ④ XSS 实验:故意把 el.textContent 换成 el.innerHTML, 输入 <img src=x onerror=alert(1)> 看浏览器弹窗——记住这个教训。