跳到主要内容

DOM 节点创建、查找、添加、移动、复制与移除:哪些 API 最常考?“追加老节点”为什么会移动?

面试速答(30 秒版 TL;DR)

  • DOM 节点操作可以按 6 组记:创建、查找、插入、移动、复制、移除
  • 最大高频点是:同一个节点同一时刻只能在文档树里出现一次,所以把旧节点再次 append / appendChild 到新位置,本质是“移动”,不是“复制”。
  • 现代 API 要会:appendprependbeforeafterreplaceWithremove;老 API 也要会:appendChildinsertBeforereplaceChildremoveChild
  • 性能上最重要的口径是:批量构建用 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 返回 静态 NodeList
  • getElementsByClassNamegetElementsByTagName 常返回 动态 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:appendChildappend 有什么区别?

  • appendChild 只能接收一个节点
  • append 可以接收多个节点,也能直接接收字符串
  • 两者都能把已有节点移动到新位置

Q3:querySelectorAllgetElementsByClassName 有什么区别?

  • querySelectorAll:静态 NodeList
  • getElementsByClassName:通常是动态集合

Q4:cloneNode(true) 会复制事件吗?

通常不会复制通过 addEventListener 绑定的监听器;它复制的是 DOM 结构和属性,不是运行时注册的 JS 逻辑。


易错点/坑

  • 以为重新插入旧节点是复制,实际是移动。
  • childNodes 里把空白文本节点也算进去,遍历时容易踩坑。
  • 复制节点后忘记处理重复 id
  • querySelectorAll 不是数组,必要时要 Array.from(...)

速记要点(可背诵)

  • 创建:createElementcreateTextNodecreateDocumentFragment
  • 查找:getElementByIdquerySelectorclosest
  • 插入:appendChildinsertBeforeappendbeforeafter
  • 复制:cloneNode
  • 移除:removeremoveChild