01DOM 是什么与节点树

浏览器拿到你写的 HTML,是怎么把它变成一个可以被 JS 操控的模型的? 答案就是 DOM(Document Object Model)——一棵"倒过来的树", 每一个标签、每一段文本、每一条注释都变成了对象,可以读、查、改、增、删。 这一课让你把这棵树看清、把入口握紧、把节点关系一次钉死。

约 16 分钟 6 个动手实验 JS 真改页面

一、DOM 是什么:JS 与 HTML 的桥梁

浏览器读到 <html> 时并不会把标签当作「文字」——它会立刻把整页解析成一棵 对象树,每个标签、每段文本、每条注释都是树上的一个「节点(Node)」, 每个节点都有属性、方法,还能被 JS 取出来随意操作。这棵对象树,就是 DOM。

JS 通过 DOM API(一组浏览器内置的对象与方法)来读写这棵树: querySelector() 查节点、textContent 改文字、 createElement() 新建节点、addEventListener() 绑定事件。 学完整个 DOM 章节,你会反复用到这几个动作。

DOM 与 HTML 区别:本质 / 谁生成 / 谁控制
维度HTML(源代码)DOM(内存对象)由谁生成
本质写在文件里的字符串文本浏览器内存里的一棵对象树你写 / 浏览器建
动态性写完就定型JS 可以随时增删改—
谁能改改文件 → 重新加载el.remove()、el.innerHTML = ...JS 任意改
常见观察F12 → Sources 面板看原文F12 → Elements 面板看树—
记住一条铁律:浏览器渲染的是 DOM,不是 HTML 原文。 在 F12 → Elements 面板看到的才是当前 DOM 树,如果你 JS 改了什么, 那里也会立刻同步——而 Sources 面板里的 HTML 永远保持原样。

二、document 是入口

整棵 DOM 树的「树根」,叫 document。它实际上是浏览器全局对象 window 上的一个属性:window.document === document。 在普通脚本里直接写 document 就行,不必带 window.。

document 身上挂着一堆常用属性,可以一键读出整页状态:

document 常用属性:秒读整页信息
属性含义典型取值
document.title页签标题(<title> 内容)"前端自习室"
document.URL当前页完整网址"https://..."
document.body<body> 元素节点<body>
document.head<head> 元素节点<head>
document.forms页内所有表单集合HTMLCollection
document.images页内所有图片集合HTMLCollection
document.documentElement<html> 元素节点<html>

点运行查看这些属性的实时取值——改完 document.title,浏览器页签的文字也会跟着变。

点运行看一组 document 属性的实时值:把 document.title = "..." 那行改成你想看的标题,回头看浏览器页签
document 自身就是一个 节点(Node), 类型是 Node.DOCUMENT_NODE(值为 9)。所以它身上也有 nodeType / nodeName 这些通用属性,下一节看。

三、节点树:DOM 是树形结构

把浏览器解析后的 DOM 画出来,就是一棵倒过来的树: 根在顶端 document,向下分叉出 <html>, 再分叉出 <head> / <body>,如此类推。 每一个圈圈都是一个节点(Node)。

#document
↑|↓
<html>
↓
<head> — <body>
↓ ↓
<title> "前端自习室" <h1> <!-- 注释也算节点 -->
文档节点 (9) 元素节点 (1) 文本节点 (3) 注释节点 (8)

每个节点都有三种「身份证」属性,任何时候取出来都一致: nodeType(类型编号)、nodeName(名字)、nodeValue(值)。

nodeType 四种常用常量:节点身份一览
常量数值对应节点nodeNamenodeValue
Node.ELEMENT_NODE1HTML 元素(<div> 等)标签名大写 "DIV"null
Node.TEXT_NODE3文字、空白、换行"#text"文本内容
Node.COMMENT_NODE8注释 <!-- -->"#comment"注释内容
Node.DOCUMENT_NODE9document 自身"#document"null

用 DOMParser 解析一段 HTML 字符串,就能立刻得到一棵完整的节点树——这是后面所有 DOM 操作的「原材料」。

点运行解析一段含元素 / 文本 / 注释的 HTML 字符串,逐节点打印 nodeType / nodeName / nodeValue——改字符串再跑,节点信息会变
nodeValue 不是「子节点文本」。 文本节点(type=3)的 nodeValue 才有意义, 元素节点(type=1)的 nodeValue 永远是 null—— 元素里的文字要用 textContent,下一课细讲。

四、节点关系:parent / child / sibling

节点之间有三类关系: 父节点(parent)、 子节点(child)、 兄弟节点(sibling)。 DOM 给你 6 个属性把这三种关系打通:parentNode 找爹、 childNodes / firstChild / lastChild 找孩子、 previousSibling / nextSibling 找邻居。

节点关系 6 属性速查:从一个节点走到任意节点
属性含义备注
parentNode父节点每个节点只有一个爹
childNodes所有直接子节点列表含空白文本节点
firstChild第一个子节点可能是空白文本
lastChild最后一个子节点可能是空白文本
previousSibling上一个兄弟可能是空白文本
nextSibling下一个兄弟可能是空白文本

一个最容易被坑的细节:HTML 源码里的换行和缩进,全部都是文本节点。 所以 <div> 里只有一行 <p>, 它的 childNodes.length 也可能是 2(前后各一个空文本节点)。

点运行:用 DOMParser 构造 5 层嵌套 HTML,沿着 parentNode 一路向上爬,把每一层的「自己 / 父节点」打印出来
想偷懒地用「递归函数」遍历整棵树?记住一个万能模板: node.childNodes.forEach(c => walk(c)),再判断 node.nodeType === 1 才处理元素——可以避免对注释和空白文本做多余工作。

五、元素的特殊属性

上一节的 6 个「通用属性」对任何节点都适用。 但日常 80% 的操作都在元素节点(nodeType === 1)上, DOM 给元素单独开了一套「跳过空白」的属性,找孩子更干净。

元素专用 vs Node 通用:通用版含空白,元素版已过滤
需求Node 通用版(含空白)Element 专用版(仅元素)
当前节点的标签名nodeNametagName(始终大写)
所有直接子childNodeschildren
子节点数量childNodes.lengthchildElementCount
第一个子firstChildfirstElementChild
最后一个子lastChildlastElementChild
上一兄弟previousSiblingpreviousElementSibling
下一兄弟nextSiblingnextElementSibling

关键差异:children 只算元素子节点,自动跳过空白文本—— 这是日常开发应该默认用「Element 版」的根本原因,少踩一半坑。

点运行对比同一段 HTML:childNodes 把换行 / 缩进也算进去(数量多),children 只算真元素(数量少)
口诀:遇到元素就用元素版。 写 el.children / el.firstElementChild 而不是 el.childNodes / el.firstChild,少写一堆空值判断。

六、DOM 操作的核心四件套(先讲「读」)

所有 DOM 操作可以归为 4 类: 读(query / get)、 改(textContent / innerHTML)、 增(createElement + append)、 删(remove)。 这一课只讲「读」:把节点查出来,最经典的 4 个 API。

DOM 四件套:读 / 改 / 增 / 删 全景
动作核心 API返回本节是否讲
读(query)querySelector / querySelectorAll单个元素 / NodeList✓ 本节
改(update)textContent / innerHTML / setAttribute字符串第 02 课
增(create)createElement + append / prepend新元素第 03 课
删(remove)element.remove()—第 03 课

「读」这一步的黄金组合:document.querySelector(css选择器) 返回单个元素,document.querySelectorAll(...) 返回多个元素。 它们通用、灵活、跟 CSS 选择器完全一样,是现代 DOM 首选。

点运行查看「读」的实时演示:document.title / .URL / body 元素数 / h1 当前文字 —— JS 把这些真实值打了出来

七、DOMContentLoaded vs load

浏览器读 HTML 是一行一行往下走的:读到什么标签,就立刻解析对应的节点。 当HTML 整张解析完、DOM 树构建好那一刻,会触发 DOMContentLoaded 事件;等所有图片 / 样式表 / 脚本都加载完, 才触发 load 事件。

两个加载事件:DOMContentLoaded 快 / load 全
事件触发时机DOM 可访问?资源全好?典型场景
DOMContentLoadedHTML 解析完成✓✗(图片等可能还在)尽早初始化 JS 逻辑
load所有资源加载完毕✓✓统计埋点 / 启动特效

在 <head> 里写的 JS 会在 HTML 还没解析完时就执行—— 此时 下面的元素还不存在,直接 document.querySelector(...) 会拿到 null。 解决办法:要么把 JS 放最末尾(最常用),要么监听 DOMContentLoaded,要么给 <script> 加 defer 属性。

点运行模拟「提前执行」vs「等 DOMContentLoaded 后执行」:把 document.querySelector("#box") 提前读会拿到 null
关于脚本执行时机:本课的 JS lab 由 learn.js 注入到 <script> 里并以 defer 加载,等到 DOM 树构建完才执行,所以 lab 里大多数 querySelector 都能拿到结果。真实项目里, 优先用 <script defer> 或把脚本放 </body> 前, 不要依赖 DOMContentLoaded 兜底——它是个保险,不是首要方案。

八、本章小结

  • DOM = 浏览器把 HTML 解成的一棵对象树,JS 通过 DOM API 读写它——和 HTML 原文是两个东西。
  • document 是入口(nodeType === 9);document.title / .URL / .body / .head 是一组秒读页面的属性。
  • 每个节点都有 nodeType / nodeName / nodeValue 三件套;常见类型:元素 1、文本 3、注释 8、文档 9。
  • 节点关系 6 属性:parentNode / childNodes / firstChild / lastChild / previousSibling / nextSibling——含空白文本节点。
  • 元素专属 6 属性跳过空白:tagName / children / childElementCount / firstElementChild / lastElementChild / previousElementSibling / nextElementSibling。
  • DOM 四件套:读 / 改 / 增 / 删;本课仅讲「读」querySelector / querySelectorAll。
  • DOMContentLoaded = HTML 解析完;load = 全资源完;脚本最好用 <script defer> 或放底部。
动手练习: ① 在控制台输入 document.title = "改成你的名字" 看浏览器页签; ② 写一段含 <h1>、一段文字、一条注释的 HTML 字符串,用 DOMParser 解析,遍历输出每个节点的 nodeType / nodeName / nodeValue; ③ 把同一段 HTML 拿去比较:childNodes.length 与 children.length 的差,看看哪个算上了空白文本; ④ 在一个嵌套 4 层的元素上连写: e.parentNode.parentNode.parentNode.parentNode,看一路爬回哪里; ⑤ F12 → Elements 面板随便点一个元素,在控制台输入 $0.nodeType、$0.tagName、$0.children.length, 把上面学到的属性现场验证一次。