DOM操作
JavaScript DOM 操作详解:节点操作与文档交互实战指南
DOM(Document Object Model,文档对象模型)是浏览器将 HTML 文档解析后形成的树形结构,JavaScript 通过 DOM 操作可以动态修改网页的内容、结构和样式。从 “DOM 基础概念→核心操作流程→实战场景→常见问题” 四个维度,系统讲解 DOM 操作的底层逻辑与实用技巧,帮你掌握动态网页开发的核心能力。
DOM 核心概念:节点与文档树
在进行 DOM 操作前,需先理解 “节点” 和 “文档树” 的基本概念 ——HTML 文档中所有内容(标签、属性、文本)都被抽象为 “节点”,这些节点按层级关系组成 “文档树”。
1. 三种核心节点类型
HTML 文档中的节点主要分为三类,每种节点的特性和用途不同:
| 节点类型 | 描述 | 示例(基于 <p title="remark">备注</p>) |
核心属性(nodeType/nodeName/nodeValue) |
|---|---|---|---|
| 元素节点 | HTML 标签(如 <p>、<div>、<li>),是文档树的 “骨架”,可包含子节点 |
<p> 标签本身 |
nodeType: 1;nodeName: 标签名大写(如 P);nodeValue: null(元素节点无此属性) |
| 属性节点 | 元素节点的属性(如 title、id、class),依附于元素节点存在 |
title="remark" 属性 |
nodeType: 2;nodeName: 属性名(如 title);nodeValue: 属性值(如 remark) |
| 文本节点 | 元素节点内的文本内容(如标签中的文字、空格、换行),是元素节点的子节点 | <p> 标签内的 “备注” 文本 |
nodeType: 3;nodeName: #text;nodeValue: 文本内容(如 备注) |
关键补充:
- 除上述三种外,DOM 还有注释节点(
nodeType: 8)、文档节点(document,nodeType: 9)等,但日常开发中以 “元素 / 属性 / 文本节点” 为主; - 节点关系:元素节点可包含子节点(文本节点、其他元素节点),属性节点是元素节点的 “附属”,不参与文档树的层级关系。
2. 文档树结构
HTML 文档被解析后,节点按 “父子、兄弟” 关系组成树形结构,例如:
<html>
<head>
<title>DOM 示例</title>
</head>
<body>
<div id="city">
<li name="bj">北京</li>
<li name="sh">上海</li>
</div>
</body>
</html>
对应的文档树简化结构:
document(文档节点)
└── html(元素节点)
├── head(元素节点)
│ └── title(元素节点)
│ └── #text(文本节点:"DOM 示例")
└── body(元素节点)
└── div(元素节点,id="city")
├── li(元素节点,name="bj")
│ └── #text(文本节点:"北京")
└── li(元素节点,name="sh")
└── #text(文本节点:"上海")
DOM 操作本质就是 “遍历文档树” 或 “修改文档树结构”(如添加 / 删除节点)。
文档加载顺序与 DOM 操作时机
“JS 写在 <head> 中无法获取 <body> 节点” 是 DOM 开发的常见问题,核心原因是 “HTML 文档按自上而下顺序加载,JS 执行时目标节点尚未解析”。
1. 问题根源
若 JS 代码写在 <head> 中且未做处理,执行时 <body> 及内部节点还未被浏览器解析,此时调用 document.getElementById() 会返回 null:
<head>
<script>
// 错误:此时 <body> 尚未加载,无法获取 id="city" 的节点
var city = document.getElementById("city");
console.log(city); // 输出:null
</script>
</head>
<body>
<div id="city">...</div>
</body>
2. 两种解决方案
方案 1:将 JS 代码放在 <body> 末尾
让 JS 在 HTML 节点全部解析完成后再执行,这是最简单且高效的方式:
<body>
<div id="city">
<li name="bj">北京</li>
</div>
<!-- JS 放在 </body> 前,确保所有节点已解析 -->
<script>
var city = document.getElementById("city");
console.log(city); // 输出:<div id="city">...</div>(正常获取节点)
</script>
</body>
优势:无需额外代码,执行效率高;
适用场景:简单页面或无需提前加载的 JS 逻辑。
方案 2:使用 window.onload 事件
window.onload 是浏览器提供的 “文档加载完成” 事件,当 HTML 文档(包括图片、样式表等资源)全部加载完成后,才会执行事件回调函数:
<head>
<script>
// 文档加载完成后执行回调
window.onload = function() {
var city = document.getElementById("city");
console.log(city); // 输出:<div id="city">...</div>(正常获取节点)
};
</script>
</head>
<body>
<div id="city">
<li name="bj">北京</li>
</div>
</body>
优势:JS 可放在 <head> 中,代码结构更清晰;
注意:window.onload 会等待所有资源(如大图片)加载完成,若只需等待 HTML 解析完成,可使用 DOMContentLoaded 事件(执行时机更早):
// HTML 解析完成后立即执行(无需等待图片、样式表)
document.addEventListener("DOMContentLoaded", function() {
var city = document.getElementById("city");
console.log(city);
});
DOM 核心操作 1:获取节点
获取节点是 DOM 操作的 “第一步”,需根据节点的 id、标签名、属性名等特征选择合适的方法。
1. 获取元素节点(最常用)
(1)通过 id 获取:document.getElementById(id)
- 作用:根据元素的
id属性获取唯一的元素节点(HTML 规定id必须唯一); - 返回值:找到则返回元素节点,未找到则返回
null; - 注意:仅
document可调用此方法(元素节点无法调用)。
window.onload = function() {
// 获取 id 为 "city" 的元素节点
var cityNode = document.getElementById("city");
console.log(cityNode); // <div id="city">...</div>
};
(2)通过标签名获取:getElementsByTagName(tagName)
- 作用:根据标签名(如
li、div)获取元素节点的集合(HTMLCollection,类似数组但非数组); - 返回值:无论是否找到,均返回集合(空集合长度为 0);
- 灵活调用:
document调用时获取全文档节点,元素节点调用时仅获取该元素的子节点。
window.onload = function() {
// 1. document 调用:获取全文档的所有 <li> 节点
var allLiNodes = document.getElementsByTagName("li");
console.log(allLiNodes.length); // 输出:2(假设文档中有 2 个 <li>)
// 2. 元素节点调用:仅获取 id="city" 下的 <li> 节点
var cityNode = document.getElementById("city");
var cityLiNodes = cityNode.getElementsByTagName("li");
console.log(cityLiNodes.length); // 输出:2(仅 <city> 下的 <li>)
};
(3)通过 name 属性获取:document.getElementsByName(name)
- 作用:根据元素的
name属性获取元素节点的集合(NodeList,类似数组); - 适用场景:表单元素(如单选框、复选框),因同一表单的多个元素可共享相同
name; - 注意:仅
document可调用此方法。
<!-- HTML 表单 -->
<input type="radio" name="gender" value="male"> 男
<input type="radio" name="gender" value="female"> 女
<script>
window.onload = function() {
// 获取 name 为 "gender" 的所有单选框
var genderNodes = document.getElementsByName("gender");
console.log(genderNodes.length); // 输出:2
// 遍历集合,获取选中的值
for (var i = 0; i < genderNodes.length; i++) {
if (genderNodes[i].checked) {
console.log("选中的性别:", genderNodes[i].value);
}
}
};
</script>
(4)补充:现代 DOM 方法(ES5+)
除上述传统方法外,ES5 新增了更灵活的获取方法(推荐在现代项目中使用):
document.querySelector(selector):根据 CSS 选择器(如#city、.class、li[name="bj"])获取第一个匹配的元素节点;document.querySelectorAll(selector):根据 CSS 选择器获取所有匹配的元素节点集合(NodeList)。
window.onload = function() {
// 1. 获取 id 为 "city" 的第一个元素(等同于 getElementById)
var cityNode = document.querySelector("#city");
// 2. 获取 name 为 "bj" 的第一个 <li>(CSS 选择器语法)
var bjNode = document.querySelector("li[name='bj']");
// 3. 获取所有 class 为 "item" 的元素(集合)
var itemNodes = document.querySelectorAll(".item");
};
2. 获取属性节点
属性节点依附于元素节点,通常无需直接获取属性节点,而是通过元素节点间接读写属性值(更高效):
(1)直接读写内置属性(如 id、value、src)
元素节点的内置属性(HTML 标准属性)可通过 “. 语法” 直接读写:
<input type="text" id="username" value="张三">
<script>
window.onload = function() {
var usernameNode = document.getElementById("username");
// 1. 读取属性值
console.log(usernameNode.id); // 输出:"username"(id 属性)
console.log(usernameNode.value); // 输出:"张三"(value 属性)
// 2. 修改属性值
usernameNode.value = "李四"; // 输入框内容变为 "李四"
usernameNode.id = "new-username"; // id 改为 "new-username"
};
</script>
(2)读写自定义属性:getAttribute() 与 setAttribute()
对于自定义属性(非 HTML 标准属性,如 data-id),需使用 getAttribute() 读取、setAttribute() 修改:
<div id="user" data-id="123" data-name="张三"></div>
<script>
window.onload = function() {
var userNode = document.getElementById("user");
// 1. 读取自定义属性
console.log(userNode.getAttribute("data-id")); // 输出:"123"
console.log(userNode.getAttribute("data-name")); // 输出:"张三"
// 2. 修改自定义属性
userNode.setAttribute("data-name", "李四");
console.log(userNode.getAttribute("data-name")); // 输出:"李四"
// 3. 删除自定义属性
userNode.removeAttribute("data-id");
};
</script>
(3)直接获取属性节点(不推荐)
通过 getAttributeNode(name) 可直接获取属性节点,但日常开发中很少使用(不如直接读写属性值高效):
var userNode = document.getElementById("user");
// 获取 data-id 属性节点
var dataIdNode = userNode.getAttributeNode("data-id");
console.log(dataIdNode.nodeType); // 输出:2(属性节点类型)
console.log(dataIdNode.nodeValue); // 输出:"123"(属性值)
3. 获取文本节点
文本节点是元素节点的子节点,需先获取元素节点,再通过 “子节点相关属性” 获取文本节点:
firstChild:获取元素节点的第一个子节点(可能是文本节点或其他元素节点);lastChild:获取元素节点的最后一个子节点;childNodes:获取元素节点的所有子节点集合(包括文本节点、元素节点、注释节点)。
<li id="bj" name="bj">北京</li>
<script>
window.onload = function() {
var bjNode = document.getElementById("bj");
// 1. 获取第一个子节点(即文本节点 "北京")
var textNode = bjNode.firstChild;
console.log(textNode.nodeType); // 输出:3(文本节点类型)
console.log(textNode.nodeValue); // 输出:"北京"(文本内容)
// 2. 修改文本内容
textNode.nodeValue = "北京市"; // <li> 标签内容变为 "北京市"
// 3. 简化写法:直接修改元素节点的 textContent(推荐)
bjNode.textContent = "北京"; // 等同于修改文本节点的 nodeValue
};
</script>
推荐:使用 textContent(或 innerText,兼容性稍差)直接读写元素节点的文本内容,无需单独获取文本节点。
DOM 核心操作 2:创建与添加节点
动态创建节点是实现 “动态渲染内容” 的核心(如列表添加新项、表单动态增加字段),流程为 “创建节点 → 添加到文档树”。
1. 创建节点
(1)创建元素节点:document.createElement(tagName)
- 作用:创建指定标签名的元素节点(如
<li>、<div>); - 注意:创建的节点默认 “游离” 在文档树外,需通过
appendChild()等方法添加到文档中。
// 创建 <li> 元素节点
var liNode = document.createElement("li");
// 给元素节点设置属性
liNode.setAttribute("name", "hk");
liNode.textContent = "香港"; // 设置文本内容
(2)创建文本节点:document.createTextNode(text)
- 作用:创建包含指定文本的文本节点;
- 注意:通常无需单独创建文本节点,可通过
textContent直接给元素节点设置文本(更简洁)。
// 创建文本节点 "香港"
var textNode = document.createTextNode("香港");
// 将文本节点添加到 <li> 元素节点中
liNode.appendChild(textNode);
2. 添加节点到文档树
创建的节点需添加到现有文档树中才能显示在页面上,常用方法:
parentNode.appendChild(childNode):将子节点添加到父节点的 “最后一个子节点之后”;parentNode.insertBefore(newNode, referenceNode):将新节点插入到 “参考节点之前”。
示例:给列表添加新项
<ul id="city">
<li name="bj">北京</li>
<li name="sh">上海</li>
</ul>
<script>
window.onload = function() {
// 步骤1:创建新节点(<li> 元素节点 + 文本内容)
var liNode = document.createElement("li");
liNode.name = "hk"; // 设置 name 属性
liNode.textContent = "香港"; // 设置文本内容
// 步骤2:获取父节点(<ul id="city">)
var cityNode = document.getElementById("city");
// 步骤3:添加新节点到父节点中
cityNode.appendChild(liNode); // 添加到列表末尾
// (可选)插入到指定位置(如第一个 <li> 之前)
var firstLiNode = cityNode.firstChild; // 获取第一个子节点
cityNode.insertBefore(liNode, firstLiNode); // 插入到第一个 <li> 之前
};
</script>
效果:页面中的 <ul> 列表会新增 “香港” 项,位置可通过 appendChild 或 insertBefore 控制。
DOM 核心操作 3:删除与替换节点
1. 删除节点:parentNode.removeChild(childNode)
- 作用:通过父节点删除指定的子节点;
- 注意:必须通过父节点删除子节点,无法直接删除自身;若子节点不存在,会抛出错误。
示例:删除列表中的指定项
<ul id="city">
<li name="bj">北京</li>
<li name="sh">上海</li>
</ul>
<script>
window.onload = function() {
// 步骤1:获取父节点和要删除的子节点
var cityNode = document.getElementById("city");
var bjNode = document.querySelector("li[name='bj']"); // 获取 "北京" 节点
// 步骤2:通过父节点删除子节点
cityNode.removeChild(bjNode);
// (可选)删除父节点自身(需先获取父节点的父节点)
var bodyNode = cityNode.parentNode; // 获取 <body>(cityNode 的父节点)
bodyNode.removeChild(cityNode); // 删除 <ul id="city"> 节点
};
</script>
2. 替换节点:parentNode.replaceChild(newNode, oldNode)
- 作用:用新节点替换父节点中的旧节点;
- 效果:旧节点会从文档树中移除,新节点插入到旧节点的位置。
window.onload = function() {
// 步骤1:创建新节点
var newLiNode = document.createElement("li");
newLiNode.textContent = "重庆";
// 步骤2:获取父节点和旧节点
var cityNode = document.getElementById("city");
var shNode = document.querySelector("li[name='sh']"); // 要替换的 "上海" 节点
// 步骤3:替换节点
cityNode.replaceChild(newLiNode, shNode);
};
DOM 操作常见问题与解决方案
1. 遍历节点集合时的 “动态变化” 问题
通过 getElementsByTagName() 获取的 HTMLCollection 是 “动态集合”—— 文档树变化时,集合会自动更新,可能导致遍历异常:
var liNodes = document.getElementsByTagName("li");
// 错误:遍历过程中删除节点,集合长度动态变化,导致漏删
for (var i = 0; i < liNodes.length; i++) {
liNodes[i].parentNode.removeChild(liNodes[i]);
}
解决方案:
- 从后往前遍历(避免集合长度变化影响索引);
- 将动态集合转为静态数组(如
Array.from(liNodes))。
var liNodes = document.getElementsByTagName("li");
// 方案1:从后往前遍历
for (var i = liNodes.length - 1; i >= 0; i--) {
liNodes[i].parentNode.removeChild(liNodes[i]);
}
// 方案2:转为静态数组
var liArray = Array.from(liNodes); // 现代浏览器
// 或 var liArray = [].slice.call(liNodes);(兼容旧浏览器)
liArray.forEach(function(li) {
li.parentNode.removeChild(li);
});
2. 文本节点包含 “空白字符” 问题
HTML 中的空格、换行会被解析为文本节点,导致 firstChild 或 childNodes 包含空白文本节点:
<div id="test">
<span>文本</span>
</div>
<script>
var testNode = document.getElementById("test");
console.log(testNode.firstChild.nodeType); // 输出:3(空白文本节点,因 <div> 后有换行)
console.log(testNode.childNodes.length); // 输出:3(空白文本节点 + <span> + 空白文本节点)
</script>
解决方案:
- 遍历子节点时过滤空白文本节点;
- 使用
children属性(仅包含元素子节点,忽略文本 / 注释节点)。
var testNode = document.getElementById("test");
// 方案1:过滤空白文本节点
var childNodes = testNode.childNodes;
for (var i = 0; i < childNodes.length; i++) {
var node = childNodes[i];
// 只处理元素节点(nodeType === 1)
if (node.nodeType === 1) {
console.log(node); // 输出:<span> 节点
}
}
// 方案2:使用 children 属性(推荐)
var elementChildren = testNode.children;
console.log(elementChildren[0]); // 输出:<span> 节点(直接获取元素子节点)
3. DOM 操作性能问题
频繁的 DOM 操作(如循环创建并添加节点)会导致浏览器频繁重绘(Repaint)和重排(Reflow),影响性能:
// 低效:循环中每次都操作文档树
var cityNode = document.getElementById("city");
for (var i = 0; i < 100; i++) {
var liNode = document.createElement("li");
liNode.textContent = "城市" + i;
cityNode.appendChild(liNode); // 每次循环都触发重排
}
解决方案:使用 DocumentFragment(文档片段)批量处理节点,仅最后一次操作文档树:
// 高效:先将节点添加到文档片段,最后一次性添加到文档树
var cityNode = document.getElementById("city");
var fragment = document.createDocumentFragment(); // 创建文档片段
for (var i = 0; i < 100; i++) {
var liNode = document.createElement("li");
liNode.textContent = "城市" + i;
fragment.appendChild(liNode); // 操作片段,不触发重排
}
cityNode.appendChild(fragment); // 仅一次操作文档树,触发一次重排
总结与实战建议
1. 核心总结
- DOM 本质:HTML 文档的树形结构,节点是构成文档的基本单位(元素 / 属性 / 文本节点为主);
- 操作流程:获取节点 → 读写属性 / 文本 → 创建 / 添加 / 删除节点;
- 关键时机:确保 DOM 加载完成后再执行操作(
window.onload或 JS 放<body>末尾); - 现代方法:优先使用
querySelector/querySelectorAll替代传统方法,使用textContent读写文本。
2. 实战建议
- 减少 DOM 操作次数:批量处理节点(如
DocumentFragment),避免频繁重排; - 缓存节点引用:避免重复调用
getElementById等方法(如将节点保存在变量中); - 优先操作 “离线节点”:创建的节点先设置好属性和文本,再添加到文档树;
- 兼容处理:若需支持旧浏览器(如 IE),注意
textContent需替换为innerText,Array.from需替换为[].slice.call()