0%

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: 1nodeName: 标签名大写(如 P);nodeValue: null(元素节点无此属性)
属性节点 元素节点的属性(如 titleidclass),依附于元素节点存在 title="remark" 属性 nodeType: 2nodeName: 属性名(如 title);nodeValue: 属性值(如 remark
文本节点 元素节点内的文本内容(如标签中的文字、空格、换行),是元素节点的子节点 <p> 标签内的 “备注” 文本 nodeType: 3nodeName: #textnodeValue: 文本内容(如 备注
关键补充:
  • 除上述三种外,DOM 还有注释节点(nodeType: 8)、文档节点(documentnodeType: 9)等,但日常开发中以 “元素 / 属性 / 文本节点” 为主;
  • 节点关系:元素节点可包含子节点(文本节点、其他元素节点),属性节点是元素节点的 “附属”,不参与文档树的层级关系。

2. 文档树结构

HTML 文档被解析后,节点按 “父子、兄弟” 关系组成树形结构,例如:

1
2
3
4
5
6
7
8
9
10
11
<html>
<head>
<title>DOM 示例</title>
</head>
<body>
<div id="city">
<li name="bj">北京</li>
<li name="sh">上海</li>
</div>
</body>
</html>

对应的文档树简化结构:

1
2
3
4
5
6
7
8
9
10
11
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

1
2
3
4
5
6
7
8
9
10
<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 节点全部解析完成后再执行,这是最简单且高效的方式:

1
2
3
4
5
6
7
8
9
10
11
<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 文档(包括图片、样式表等资源)全部加载完成后,才会执行事件回调函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<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 事件(执行时机更早):

1
2
3
4
5
// 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 可调用此方法(元素节点无法调用)。
1
2
3
4
5
window.onload = function() {
// 获取 id 为 "city" 的元素节点
var cityNode = document.getElementById("city");
console.log(cityNode); // <div id="city">...</div>
};
(2)通过标签名获取:getElementsByTagName(tagName)
  • 作用:根据标签名(如 lidiv)获取元素节点的集合HTMLCollection,类似数组但非数组);
  • 返回值:无论是否找到,均返回集合(空集合长度为 0);
  • 灵活调用document 调用时获取全文档节点,元素节点调用时仅获取该元素的子节点
1
2
3
4
5
6
7
8
9
10
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 可调用此方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!-- 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.classli[name="bj"])获取第一个匹配的元素节点
  • document.querySelectorAll(selector):根据 CSS 选择器获取所有匹配的元素节点集合NodeList)。
1
2
3
4
5
6
7
8
9
10
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)直接读写内置属性(如 idvaluesrc

元素节点的内置属性(HTML 标准属性)可通过 “. 语法” 直接读写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<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() 修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<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) 可直接获取属性节点,但日常开发中很少使用(不如直接读写属性值高效):

1
2
3
4
5
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:获取元素节点的所有子节点集合(包括文本节点、元素节点、注释节点)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<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() 等方法添加到文档中。
1
2
3
4
5
// 创建 <li> 元素节点
var liNode = document.createElement("li");
// 给元素节点设置属性
liNode.setAttribute("name", "hk");
liNode.textContent = "香港"; // 设置文本内容
(2)创建文本节点:document.createTextNode(text)
  • 作用:创建包含指定文本的文本节点;
  • 注意:通常无需单独创建文本节点,可通过 textContent 直接给元素节点设置文本(更简洁)。
1
2
3
4
// 创建文本节点 "香港"
var textNode = document.createTextNode("香港");
// 将文本节点添加到 <li> 元素节点中
liNode.appendChild(textNode);

2. 添加节点到文档树

创建的节点需添加到现有文档树中才能显示在页面上,常用方法:

  • parentNode.appendChild(childNode):将子节点添加到父节点的 “最后一个子节点之后”;
  • parentNode.insertBefore(newNode, referenceNode):将新节点插入到 “参考节点之前”。
示例:给列表添加新项
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<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> 列表会新增 “香港” 项,位置可通过 appendChildinsertBefore 控制。

DOM 核心操作 3:删除与替换节点

1. 删除节点:parentNode.removeChild(childNode)

  • 作用:通过父节点删除指定的子节点;
  • 注意:必须通过父节点删除子节点,无法直接删除自身;若子节点不存在,会抛出错误。
示例:删除列表中的指定项
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<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)

  • 作用:用新节点替换父节点中的旧节点;
  • 效果:旧节点会从文档树中移除,新节点插入到旧节点的位置。
1
2
3
4
5
6
7
8
9
10
11
12
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 是 “动态集合”—— 文档树变化时,集合会自动更新,可能导致遍历异常:

1
2
3
4
5
var liNodes = document.getElementsByTagName("li");
// 错误:遍历过程中删除节点,集合长度动态变化,导致漏删
for (var i = 0; i < liNodes.length; i++) {
liNodes[i].parentNode.removeChild(liNodes[i]);
}

解决方案

  • 从后往前遍历(避免集合长度变化影响索引);
  • 将动态集合转为静态数组(如 Array.from(liNodes))。
1
2
3
4
5
6
7
8
9
10
11
12
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 中的空格、换行会被解析为文本节点,导致 firstChildchildNodes 包含空白文本节点:

1
2
3
4
5
6
7
8
9
<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 属性(仅包含元素子节点,忽略文本 / 注释节点)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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),影响性能:

1
2
3
4
5
6
7
// 低效:循环中每次都操作文档树
var cityNode = document.getElementById("city");
for (var i = 0; i < 100; i++) {
var liNode = document.createElement("li");
liNode.textContent = "城市" + i;
cityNode.appendChild(liNode); // 每次循环都触发重排
}

解决方案:使用 DocumentFragment(文档片段)批量处理节点,仅最后一次操作文档树:

1
2
3
4
5
6
7
8
9
10
11
// 高效:先将节点添加到文档片段,最后一次性添加到文档树
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. 实战建议

  1. 减少 DOM 操作次数:批量处理节点(如 DocumentFragment),避免频繁重排;
  2. 缓存节点引用:避免重复调用 getElementById 等方法(如将节点保存在变量中);
  3. 优先操作 “离线节点”:创建的节点先设置好属性和文本,再添加到文档树;
  4. 兼容处理:若需支持旧浏览器(如 IE),注意 textContent 需替换为 innerTextArray.from 需替换为 [].slice.call()

欢迎关注我的其它发布渠道