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 文档被解析后,节点按 “父子、兄弟” 关系组成树形结构,例如:
1 | <html> |
对应的文档树简化结构:
1 | document(文档节点) |
DOM 操作本质就是 “遍历文档树” 或 “修改文档树结构”(如添加 / 删除节点)。
文档加载顺序与 DOM 操作时机
“JS 写在 <head> 中无法获取 <body> 节点” 是 DOM 开发的常见问题,核心原因是 “HTML 文档按自上而下顺序加载,JS 执行时目标节点尚未解析”。
1. 问题根源
若 JS 代码写在 <head> 中且未做处理,执行时 <body> 及内部节点还未被浏览器解析,此时调用 document.getElementById() 会返回 null:
1 | <head> |
2. 两种解决方案
方案 1:将 JS 代码放在 <body> 末尾
让 JS 在 HTML 节点全部解析完成后再执行,这是最简单且高效的方式:
1 | <body> |
优势:无需额外代码,执行效率高;
适用场景:简单页面或无需提前加载的 JS 逻辑。
方案 2:使用 window.onload 事件
window.onload 是浏览器提供的 “文档加载完成” 事件,当 HTML 文档(包括图片、样式表等资源)全部加载完成后,才会执行事件回调函数:
1 | <head> |
优势:JS 可放在 <head> 中,代码结构更清晰;
注意:window.onload 会等待所有资源(如大图片)加载完成,若只需等待 HTML 解析完成,可使用 DOMContentLoaded 事件(执行时机更早):
1 | // HTML 解析完成后立即执行(无需等待图片、样式表) |
DOM 核心操作 1:获取节点
获取节点是 DOM 操作的 “第一步”,需根据节点的 id、标签名、属性名等特征选择合适的方法。
1. 获取元素节点(最常用)
(1)通过 id 获取:document.getElementById(id)
- 作用:根据元素的
id属性获取唯一的元素节点(HTML 规定id必须唯一); - 返回值:找到则返回元素节点,未找到则返回
null; - 注意:仅
document可调用此方法(元素节点无法调用)。
1 | window.onload = function() { |
(2)通过标签名获取:getElementsByTagName(tagName)
- 作用:根据标签名(如
li、div)获取元素节点的集合(HTMLCollection,类似数组但非数组); - 返回值:无论是否找到,均返回集合(空集合长度为 0);
- 灵活调用:
document调用时获取全文档节点,元素节点调用时仅获取该元素的子节点。
1 | window.onload = function() { |
(3)通过 name 属性获取:document.getElementsByName(name)
- 作用:根据元素的
name属性获取元素节点的集合(NodeList,类似数组); - 适用场景:表单元素(如单选框、复选框),因同一表单的多个元素可共享相同
name; - 注意:仅
document可调用此方法。
1 | <!-- HTML 表单 --> |
(4)补充:现代 DOM 方法(ES5+)
除上述传统方法外,ES5 新增了更灵活的获取方法(推荐在现代项目中使用):
document.querySelector(selector):根据 CSS 选择器(如#city、.class、li[name="bj"])获取第一个匹配的元素节点;document.querySelectorAll(selector):根据 CSS 选择器获取所有匹配的元素节点集合(NodeList)。
1 | window.onload = function() { |
2. 获取属性节点
属性节点依附于元素节点,通常无需直接获取属性节点,而是通过元素节点间接读写属性值(更高效):
(1)直接读写内置属性(如 id、value、src)
元素节点的内置属性(HTML 标准属性)可通过 “. 语法” 直接读写:
1 | <input type="text" id="username" value="张三"> |
(2)读写自定义属性:getAttribute() 与 setAttribute()
对于自定义属性(非 HTML 标准属性,如 data-id),需使用 getAttribute() 读取、setAttribute() 修改:
1 | <div id="user" data-id="123" data-name="张三"></div> |
(3)直接获取属性节点(不推荐)
通过 getAttributeNode(name) 可直接获取属性节点,但日常开发中很少使用(不如直接读写属性值高效):
1 | var userNode = document.getElementById("user"); |
3. 获取文本节点
文本节点是元素节点的子节点,需先获取元素节点,再通过 “子节点相关属性” 获取文本节点:
firstChild:获取元素节点的第一个子节点(可能是文本节点或其他元素节点);lastChild:获取元素节点的最后一个子节点;childNodes:获取元素节点的所有子节点集合(包括文本节点、元素节点、注释节点)。
1 | <li id="bj" name="bj">北京</li> |
推荐:使用 textContent(或 innerText,兼容性稍差)直接读写元素节点的文本内容,无需单独获取文本节点。
DOM 核心操作 2:创建与添加节点
动态创建节点是实现 “动态渲染内容” 的核心(如列表添加新项、表单动态增加字段),流程为 “创建节点 → 添加到文档树”。
1. 创建节点
(1)创建元素节点:document.createElement(tagName)
- 作用:创建指定标签名的元素节点(如
<li>、<div>); - 注意:创建的节点默认 “游离” 在文档树外,需通过
appendChild()等方法添加到文档中。
1 | // 创建 <li> 元素节点 |
(2)创建文本节点:document.createTextNode(text)
- 作用:创建包含指定文本的文本节点;
- 注意:通常无需单独创建文本节点,可通过
textContent直接给元素节点设置文本(更简洁)。
1 | // 创建文本节点 "香港" |
2. 添加节点到文档树
创建的节点需添加到现有文档树中才能显示在页面上,常用方法:
parentNode.appendChild(childNode):将子节点添加到父节点的 “最后一个子节点之后”;parentNode.insertBefore(newNode, referenceNode):将新节点插入到 “参考节点之前”。
示例:给列表添加新项
1 | <ul id="city"> |
效果:页面中的 <ul> 列表会新增 “香港” 项,位置可通过 appendChild 或 insertBefore 控制。
DOM 核心操作 3:删除与替换节点
1. 删除节点:parentNode.removeChild(childNode)
- 作用:通过父节点删除指定的子节点;
- 注意:必须通过父节点删除子节点,无法直接删除自身;若子节点不存在,会抛出错误。
示例:删除列表中的指定项
1 | <ul id="city"> |
2. 替换节点:parentNode.replaceChild(newNode, oldNode)
- 作用:用新节点替换父节点中的旧节点;
- 效果:旧节点会从文档树中移除,新节点插入到旧节点的位置。
1 | window.onload = function() { |
DOM 操作常见问题与解决方案
1. 遍历节点集合时的 “动态变化” 问题
通过 getElementsByTagName() 获取的 HTMLCollection 是 “动态集合”—— 文档树变化时,集合会自动更新,可能导致遍历异常:
1 | var liNodes = document.getElementsByTagName("li"); |
解决方案:
- 从后往前遍历(避免集合长度变化影响索引);
- 将动态集合转为静态数组(如
Array.from(liNodes))。
1 | var liNodes = document.getElementsByTagName("li"); |
2. 文本节点包含 “空白字符” 问题
HTML 中的空格、换行会被解析为文本节点,导致 firstChild 或 childNodes 包含空白文本节点:
1 | <div id="test"> |
解决方案:
- 遍历子节点时过滤空白文本节点;
- 使用
children属性(仅包含元素子节点,忽略文本 / 注释节点)。
1 | var testNode = document.getElementById("test"); |
3. DOM 操作性能问题
频繁的 DOM 操作(如循环创建并添加节点)会导致浏览器频繁重绘(Repaint)和重排(Reflow),影响性能:
1 | // 低效:循环中每次都操作文档树 |
解决方案:使用 DocumentFragment(文档片段)批量处理节点,仅最后一次操作文档树:
1 | // 高效:先将节点添加到文档片段,最后一次性添加到文档树 |
总结与实战建议
1. 核心总结
- DOM 本质:HTML 文档的树形结构,节点是构成文档的基本单位(元素 / 属性 / 文本节点为主);
- 操作流程:获取节点 → 读写属性 / 文本 → 创建 / 添加 / 删除节点;
- 关键时机:确保 DOM 加载完成后再执行操作(
window.onload或 JS 放<body>末尾); - 现代方法:优先使用
querySelector/querySelectorAll替代传统方法,使用textContent读写文本。
2. 实战建议
- 减少 DOM 操作次数:批量处理节点(如
DocumentFragment),避免频繁重排; - 缓存节点引用:避免重复调用
getElementById等方法(如将节点保存在变量中); - 优先操作 “离线节点”:创建的节点先设置好属性和文本,再添加到文档树;
- 兼容处理:若需支持旧浏览器(如 IE),注意
textContent需替换为innerText,Array.from需替换为[].slice.call()