DOM 节点创建、查找、添加、移动、复制与移除:哪些 API 最常考?“追加老节点”为什么会移动?
面试速答(30 秒版 TL;DR)
- DOM 节点操作可以按 6 组记:创建、查找、插入、移动、复制、移除。
- 最大高频点是:同一个节点同一时刻只能在文档树里出现一次,所以把旧节点再次
append/appendChild到新位置,本质是“移动”,不是“复制”。 - 现代 API 要会:
append、prepend、before、after、replaceWith、remove;老 API 也要会:appendChild、insertBefore、replaceChild、removeChild。 - 性能上最重要的口径是:批量构建用
DocumentFragment,少做无意义的 DOM 读写交替。
心智模型:DOM 是一棵“唯一节点树”
很多 DOM 面试题本质都在问同一件事:
- DOM 节点不是值,而是对象实体
- 这个实体在树上只能有一个位置
- 所以“插入已存在节点”会发生摘除再挂载
这也是为什么:
- 追加老节点会移动
- 真正想要两份,要用
cloneNode
一、创建节点
最常用的创建 API:
| API | 作用 |
|---|---|
document.createElement(tagName) | 创建元素节点 |
document.createTextNode(text) | 创建文本节点 |
document.createDocumentFragment() | 创建文档片段 |
document.createComment(data) | 创建注释节点 |
const li = document.createElement("li");
li.textContent = "JavaScript";
const text = document.createTextNode("hello");
const frag = document.createDocumentFragment();
为什么常提 DocumentFragment
因为它是一个“离线容器”:
- 先在内存里把子树拼好
- 最后一次性插入文档
这能减少中间态的 DOM 变更。
二、查找节点
1) 最常见选择器 API
| API | 返回值 | 特点 |
|---|---|---|
getElementById(id) | Element | null | 精准按 id 找 |
querySelector(selector) | 第一个匹配元素 / null | 支持 CSS 选择器 |
querySelectorAll(selector) | 静态 NodeList | 支持 CSS 选择器 |
document.getElementById("app");
document.querySelector(".card");
document.querySelectorAll("li.active");
2) DOM 关系查找
| API/属性 | 作用 |
|---|---|
parentNode / parentElement | 父节点 / 父元素 |
children | 仅元素子节点集合 |
childNodes | 所有子节点,包括文本/注释 |
firstChild / firstElementChild | 第一个子节点 / 子元素 |
nextSibling / nextElementSibling | 下一个兄弟节点 / 元素 |
closest(selector) | 向上找最近匹配祖先 |
3) 静态集合和动态集合
面试常考:
querySelectorAll返回 静态NodeListgetElementsByClassName、getElementsByTagName常返回 动态 HTMLCollection
也就是说,后者会随 DOM 变化实时反映。
三、添加与插入
1) 传统 API
| API | 作用 |
|---|---|
parent.appendChild(child) | 尾部插入一个节点 |
parent.insertBefore(newNode, referenceNode) | 在参考节点前插入 |
parent.replaceChild(newNode, oldNode) | 用新节点替换旧节点 |
2) 现代 API
| API | 作用 |
|---|---|
parent.append(...nodesOrStrings) | 尾部插入,可传多个节点或字符串 |
parent.prepend(...nodesOrStrings) | 头部插入 |
node.before(...nodesOrStrings) | 在当前节点前插入 |
node.after(...nodesOrStrings) | 在当前节点后插入 |
node.replaceWith(...nodesOrStrings) | 替换当前节点 |
list.append(li1, li2);
title.before(document.createElement("hr"));
title.after("说明文本");
四、移动节点:为什么不是复制
const a = document.getElementById("a");
const b = document.getElementById("b");
b.appendChild(a);
结果:
a不会同时出现在两个地方- 它会从旧位置移除,再挂到
b下面
面试标准答法:
- DOM 节点在树中具有唯一性
- 重新插入现有节点时,本质是移动节点引用,不会自动复制
五、复制节点
const shallow = node.cloneNode();
const deep = node.cloneNode(true);
| 写法 | 含义 |
|---|---|
cloneNode(false) 或省略参数 | 浅拷贝,只复制当前节点本身 |
cloneNode(true) | 深拷贝,连子树一起复制 |
高频追问:
cloneNode会复制元素属性和子树结构- 不会自动复制通过
addEventListener绑定的 JS 事件监听器
复制节点时为什么要小心 id
因为:
- DOM 属性也会被复制
- 页面里可能出现重复
id - 这会影响
getElementById和样式/脚本定位
六、移除节点
| API | 作用 |
|---|---|
node.remove() | 直接移除当前节点 |
parent.removeChild(node) | 旧式移除,需要父节点调用 |
element.replaceChildren(...nodesOrStrings) | 用新内容替换所有子节点 |
node.remove();
parent.removeChild(node);
container.replaceChildren();
七、批量插入为什么优先 DocumentFragment
const frag = document.createDocumentFragment();
for (let i = 0; i < 3; i++) {
const li = document.createElement("li");
li.textContent = `item-${i}`;
frag.appendChild(li);
}
ul.appendChild(frag);
面试口径:
DocumentFragment不直接在页面里渲染- 先把节点离线组装好,再一次性挂载
- 能减少中间状态下的 DOM 操作成本
要补一句:
- 现代浏览器优化已经很多,不是说它永远“质变更快”
- 但它仍然是表达“批量构建后再挂载”的好语义
八、节点和元素别混
这是基础题,但很爱被追问。
Node是更大的基类Element是元素节点- 文本、注释也属于 Node,但不是 Element
所以:
childNodes包含文本节点children只包含元素节点
九、实战建议:减少布局抖动
DOM 操作不只是“会不会”,还会问“怎么写更稳”。
常见建议:
- 先集中读布局信息
- 再集中写样式/结构
- 不要在循环里反复“读布局 -> 写样式 -> 再读布局”
- 高频渲染时优先改
transform/opacity
典型题 & 标准答法
Q1:appendChild 一个旧节点时发生了什么?
- 不是复制
- 是从原父节点摘下来,再挂到新父节点
- 因为 DOM 树中的同一节点只能存在一次
Q2:appendChild 和 append 有什么区别?
appendChild只能接收一个节点append可以接收多个节点,也能直接接收字符串- 两者都能把已有节点移动到新位置
Q3:querySelectorAll 和 getElementsByClassName 有什么区别?
querySelectorAll:静态NodeListgetElementsByClassName:通常是动态集合
Q4:cloneNode(true) 会复制事件吗?
通常不会复制通过 addEventListener 绑定的监听器;它复制的是 DOM 结构和属性,不是运行时注册的 JS 逻辑。
易错点/坑
- 以为重新插入旧节点是复制,实际是移动。
childNodes里把空白文本节点也算进去,遍历时容易踩坑。- 复制节点后忘记处理重复
id。 querySelectorAll不是数组,必要时要Array.from(...)。
速记要点(可背诵)
- 创建:
createElement、createTextNode、createDocumentFragment - 查找:
getElementById、querySelector、closest - 插入:
appendChild、insertBefore、append、before、after - 复制:
cloneNode - 移除:
remove、removeChild