HTML解析器在词法阶段即合并空白字符,将U+0020、U+0009、U+000A、U+000D、U+000C统一转为U+0020并压缩连续空格;U+00A0( )因非规范空白字符而绕过该处理,保留语义与宽度。

HTML解析器在词法阶段就合并空白字符
浏览器不是等到渲染时才“压缩空格”,而是在构建DOM树前的词法分析阶段,就把所有空白字符(U+0020空格、U+0009制表符、U+000A换行、U+000D回车、U+000C换页)统一转成标准空格 U+0020,再把连续多个 U+0020 合并为一个。
这意味着:哪怕你写的是 <p>hello\t\n\n world</p>,DOM中对应的文本节点内容已经是 "hello world" —— 中间只保留一个空格,原始换行/缩进已丢失。
- 这个过程不可逆,JavaScript 读取
textContent拿到的就是已压缩后的字符串 -
innerText还会进一步受CSS影响(比如display: none的文本不计入),但空白压缩早已完成 - DOM树里那些看似“空”的文本节点(如两个
<div></div>之间的换行),其实是独立的nodeType === 3文本节点,值为"\n"或" ",它们也会被参与布局(例如影响 inline 元素间距)
为什么 能绕过空白合并
是 Unicode 字符 U+00A0(NO-BREAK SPACE),它不是 HTML 规范定义的“空白字符”,因此不会被词法分析器识别为待压缩对象。浏览器把它当作普通字符处理,和 a 或 1 一样保留在文本节点中。
关键点在于: 不是“空格的替代品”,而是“一个有语义的字符”——它的语义就是“此处不可换行且不可被折叠”。
立即学习“前端免费学习笔记(深入)”;
- 误写成
& n b s p ;(含空格)或 (缺分号)会导致原样输出字面量,DOM中变成纯文本"& n b s p ;" - 它宽度≈普通空格,但行为完全不同:连续多个
会显示为三个独立空格,且不会在该位置断行 - 它无法通过 CSS 的
white-space控制其折叠逻辑——因为根本没进入折叠流程
white-space 改变的是渲染阶段的空白解释逻辑
white-space 不影响 DOM 构建,只作用于渲染引擎如何解释已存在的文本节点内容。它决定的是“已压缩过的空格”或“预存的换行符”要不要被再次处理。
比如给一个包含 "a b" 的 <div> 设置 white-space: pre,结果仍是 "a b"( 不变),但其中的普通空格和换行会被保留;而设为 normal,普通空格仍被压缩, 依然不变。
-
pre:保留所有空白符(空格、制表、换行),不自动换行 -
pre-wrap:保留所有空白符,但允许软换行(最常用,兼顾代码块与可读性) -
pre-line:合并连续空格,但保留换行符(适合日志类带换行的纯文本) - 注意:这些值对
、、等实体完全无效——它们始终原样显示
DOM中“看不见的空格节点”常被忽略
HTML源码里元素间的换行和缩进,在解析后会生成真实的 Text 节点(nodeType === 3)。例如:
<div>A</div> <div>B</div>
实际 DOM 结构是:div → 文本节点 "\n" → div。这个 "\n" 节点虽不可见,但在 display: inline 或 inline-block 布局下,会表现为元素间的间隙。
- 用
document.getElementById('test').nextSibling取到的往往不是下一个元素,而是这个换行文本节点 - 遍历
childNodes时必须过滤掉nodeType !== 1的节点,否则会误操作空格节点 - 服务端模板(如 Nunjucks、Django)若开启自动转义,可能把
当作普通文本转义,需显式标记为安全



















