Skip to main content

DOM 节点增删改查:现代 API 写法

· 4 min read

DOM 增删改查只记现代 API,老的 getElement 系列 + innerHTML 拼接可以忘了。

  1. 查询:querySelector / querySelectorAll 一对,覆盖 6 个老方法
  2. 插入:createElement + insertAdjacentHTML,比 appendChild 简洁
  3. 删除:el.remove() 一行替代 parentNode.removeChild 来回找
  4. 修改:dataset / classList / textContent,避免 innerHTML 拼接 XSS
  5. 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 变化更新。遍历用 forEachfor...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 属性

datasetdata-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')
多查getElementsByClassNamequerySelectorAll('.cls')
插入节点parent.appendChild(child)parent.append(child)
插 HTMLinnerHTML += ...insertAdjacentHTML('beforeend', ...)
删自身parent.removeChild(self)self.remove()
替换parent.replaceChild(new, old)old.replaceWith(new)
清空while (firstChild) removeChild(...)replaceChildren()
classclassName += ' x'classList.add('x')
data 属性getAttribute('data-foo')dataset.foo
文本innerHTML = texttextContent = text

口诀:能 querySelector 就别 getElement,能 remove 就别 removeChild,能 textContent 就别 innerHTML

References

  1. MDN Element —— 元素节点 API 索引
  2. MDN Node —— 通用节点 API
  3. DOM Living Standard —— DOM 规范源