DOM 节点增删改查:现代 API 写法
DOM 增删改查只记现代 API,老的 getElement 系列 + innerHTML 拼接可以忘了。
- 查询:querySelector / querySelectorAll 一对,覆盖 6 个老方法
- 插入:createElement + insertAdjacentHTML,比 appendChild 简洁
- 删除:el.remove() 一行替代 parentNode.removeChild 来回找
- 修改:dataset / classList / textContent,避免 innerHTML 拼接 XSS
- 2026 默认:框架项目里不要写裸 DOM API,那是 jQuery 时代肌肉记忆
查询:querySelector 一对搞定
querySelector / querySelectorAll 接受 CSS 选择器,返回匹配的节点(一个 / 全部),能覆盖以下老方法所有场景:
| 老方法 | 替代 |
|---|---|
getElementById('x') | querySelector('#x') |
getElementsByClassName('a b') | querySelectorAll('.a.b') |
getElementsByTagName('div') | querySelectorAll('div') |
getElementsByName('foo') | querySelectorAll('[name="foo"]') |
唯一替代不了的:getElementsByTagName 返回 实时同步 的 HTMLCollection(DOM 改了返回也跟着改)。99% 的现代代码不需要这种行为——改完重新 querySelector 就行。
const btn = document.querySelector('#submit') // 单个
const cards = document.querySelectorAll('.card') // 多个,静态 NodeList
querySelectorAll 返回 静态 NodeList,不会随 DOM 变化更新。遍历用 forEach 或 for...of:
document.querySelectorAll('.item').forEach(el => el.classList.add('active'))
插入:createElement + insertAdjacentHTML
单元素:用 createElement + 现代的 append / prepend:
const p = document.createElement('p')
p.textContent = 'Hello'
document.body.append(p) // append 比 appendChild 灵活:可传字符串 / 多个节点
HTML 片段:用 insertAdjacentHTML 替代 innerHTML += 拼接:
// 旧:先清空再重建(性能差 + 丢失子节点的事件监听)
list.innerHTML += '<li>' + item.name + '</li>'
// 新:定点插入,原有子节点不动
list.insertAdjacentHTML('beforeend', `<li>${item.name}</li>`)
insertAdjacentHTML 的 4 个位置覆盖所有插入需求:
| 位置 | 含义 |
|---|---|
beforebegin | 目标元素之前 |
afterbegin | 目标元素内部、第一个子节点之前 |
beforeend | 目标元素内部、最后一个子节点之后 |
afterend | 目标元素之后 |
批量插入:用 DocumentFragment 把多次 DOM 操作合成一次 reflow:
const frag = document.createDocumentFragment()
items.forEach(item => {
const li = document.createElement('li')
li.textContent = item.name
frag.append(li)
})
list.append(frag) // 一次性挂载,只触发 1 次 reflow
删除:el.remove() 一行搞定
旧写法要在父节点上操作:
// 旧:来回找父节点
el.parentNode.removeChild(el)
// 新:直接删自己
el.remove()
Element.remove() 是 ES2015 加的,所有现代浏览器都支持,直接干掉中间商。
替换:
// 旧:parentNode.replaceChild(new, old)
parent.replaceChild(newSpan, oldDiv)
// 新:oldDiv.replaceWith(newSpan)
oldDiv.replaceWith(newSpan)
清空所有子节点:
// 旧
while (el.firstChild) el.removeChild(el.firstChild)
// 新
el.replaceChildren() // ES2023
修改:dataset / classList / textContent 三件套
dataset:data-* 自定义属性
// HTML: <div data-user-id="123" data-role="admin">
const el = document.querySelector('[data-user-id]')
el.dataset.userId // "123"
el.dataset.role // "admin"
el.dataset.score = 95 // 自动反映到 data-score 属性
dataset 把 data-foo-bar 映射成 dataset.fooBar,省去 getAttribute / setAttribute 的字符串拼写。
classList:替代 className 字符串操作
// 旧:字符串拼接容易踩空格坑
el.className += ' active'
el.className = el.className.replace(/\bactive\b/, '')
// 新:方法调用
el.classList.add('active')
el.classList.remove('active')
el.classList.toggle('active', force) // 第二个参数强制设值
el.classList.contains('active')
textContent:替代 innerHTML 写文本
// 危险:拼接用户输入到 innerHTML = XSS
el.innerHTML = userInput // 用户输入 <img onerror=...> 直接执行
// 安全:textContent 自动转义
el.textContent = userInput
经验:除非真要插入富文本 HTML(且来源可信),永远用 textContent 不用 innerHTML。后者等于主动打开注入面。
现代 vs 老 API 一张表
| 操作 | ❌ 老 API | ✅ 现代 API |
|---|---|---|
| 单查 | getElementById('x') | querySelector('#x') |
| 多查 | getElementsByClassName | querySelectorAll('.cls') |
| 插入节点 | parent.appendChild(child) | parent.append(child) |
| 插 HTML | innerHTML += ... | insertAdjacentHTML('beforeend', ...) |
| 删自身 | parent.removeChild(self) | self.remove() |
| 替换 | parent.replaceChild(new, old) | old.replaceWith(new) |
| 清空 | while (firstChild) removeChild(...) | replaceChildren() |
| class | className += ' x' | classList.add('x') |
| data 属性 | getAttribute('data-foo') | dataset.foo |
| 文本 | innerHTML = text | textContent = text |
口诀:能 querySelector 就别 getElement,能 remove 就别 removeChild,能 textContent 就别 innerHTML。
References
- MDN Element —— 元素节点 API 索引
- MDN Node —— 通用节点 API
- DOM Living Standard —— DOM 规范源