Web 平台 API / a 元素的嵌套约束 待审核
a element · adoption agency

a 元素的嵌套约束

<a> 的 content model 是 transparent(透明),但附带一条硬约束:不得有 interactive content 后代、不得有 a element 后代、不得有带 tabindex 的后代。关键词是后代 (descendant) 而非子元素——外层 <a> 的整棵子树里只要冒出另一个 HTML 命名空间的 <a>,隔几层都违规。两个例外在 §3 交代:<template> 的内容不算后代,SVG 命名空间的 <a> 也不在这条约束的辖域内。

1 · 夹层元素决定的三种结果

内外两个 <a> 之间夹一层不同元素,Chrome 151 用 DOMParser 解析的结果分三档(核对于 2026-08):不夹或夹 <span> 时 DOM 里是两个平级的 <a>;夹 <div> 时是三个 <a>;夹 <object> 时嵌套原样保留。

图 1-1 · 四种夹层的解析结果对照。可切换夹层元素,观察 DOM 树的形状与 a 元素个数,红色标记表示该 a 的祖先链里还有另一个 a。

警示 ·<span> 与夹 <div> 结果并不相同。<a>formatting element,会进入 active formatting elements 列表;解析到内层 <a> 时算法发现「列表里到最近 marker 为止存在 a」,于是触发 adoption agency 算法<span> 不在 special 元素列表里,算法走「无 furthest block」分支:外层 <a> 连同 <span> 一起从开栈弹出并移出列表,<span> 作为空元素留在 DOM 里,内层 <a> 落到 <body> 上——两个 <a> 平级。<div> 是 special 元素,算法走 furthest block 分支:它会克隆外层 <a>、把 <div> 的子节点搬进克隆再挂回去。实测夹 <div> 时 DOM 里有三个 <a>,其中一个是 <div> 里的空克隆,两个 href="#outer" 也不平级。

警示 · <object> 夹在中间时嵌套被保留。<object>(连同 <applet> / <marquee> / <template>,以及处于表格上下文中的 <td> / <th> / <caption>)在起始时会往 active formatting elements 列表里插入一个 marker。于是解析内层 <a> 时,「到最近 marker 为止」这段里看不到外层 <a>,不触发 adoption agency,外层 <a> 也不被弹出。DOM 里于是真实出现了 aa——能渲染,但 non-conforming。注意裸写的 <td> 在 in body 模式下会被直接忽略,拿它当夹层反而得到被拆平的结果。

2 · 自由输入:任意标记的解析结果

下面的输入传入 new DOMParser().parseFromString(html, "text/html") 并渲染其 body 子树。标红的节点表示这个 <a> 的祖先链里还有另一个 <a>

图 2-1 · 任意 HTML 的解析结果。可粘贴或编辑源码,观察解析器如何补全、重排与丢弃标记。

注 · 能解析不等于合规。解析算法的宽容保证的是「任何字符流都得到一棵确定的 DOM、解析过程不中止」,而不是「不产生错误」——规范里 parse error 是正式定义的产物,近百种命名错误各有其名,只是不致命,conformance checker 必须报告它们。上面 <object> 那一档 DOM 里确实有 aaNu Html Checker 会报 The element “a” with the attribute “href” must not appear as a descendant of the “a” element.;被拆平的那几档则是解析器做了错误恢复,源码本身仍是 non-conforming 的写法,不应依赖这种被动纠正。

警示 · 本页的解析走 DOMParser,与页面加载用的是同一套解析算法,但它创建的 Document 没有浏览上下文、scripting flag 为 disabled,少数分支因此与真机不同。最典型的是 <noscript>:实测同一段 <noscript><a href="#x">y</a></noscript>DOMParser 解出 1 个 <a> 元素,而真实页面的 innerHTML 把内容当作 RAWTEXT、<a> 只是文本,querySelectorAll('a') 为 0(核对于 2026-08)。

3 · 两个不算后代的例外

<template> 的内容进入它的 content——一个由独立惰性 Document 拥有的 DocumentFragment,不在外层 <a> 的后代链上,因此 <a> 里套一个 <template>、其中再放 <a> 是合规的(Nu 实测零报错)。代价是 template 内容不渲染,解决不了「整句可点、其中某词另跳」的原始需求,它更多是后代判定的一处边界而非可用方案。

SVG 命名空间的 <a> 是另一个例外:content model 里说的 a element 指 HTML 命名空间的那一个,SVG 的 <a> 既不是它、也不属于 HTML 的 interactive content,因此 <a href="#o">L <svg><a href="#i">s</a></svg></a> 在 Nu 下同样零报错。要留意的是 querySelectorAll('a') 这类类型选择器在 HTML 文档里不区分命名空间,实测会把 SVG 的 <a> 一并选中——按命名空间判定违规时得显式比对 namespaceURI

4 · HTML 无法表达的那件事

HTML 没有「链接里再套一个指向别处的链接」这种结构。语义上确有此需要时,只能改用其他交互方式(如点击时用脚本拦截跳转分流),或重新设计信息结构:把两处跳转拆成并列的两个链接。

相关链接

  • HTML Standard · Adoption agency algorithm WHATWG 对应本页「拆开」一档:解析器遇到错误嵌套的 formatting element(如 a / b / i)时,如何重整标签栈:把外层标签弹出开栈并移出列表,必要时克隆它。active formatting elements 列表与 marker 的定义也在 parsing 章。
  • HTML Standard · The a element WHATWG 对应本页 content model:a 是 transparent,但「no interactive content descendant, no a element descendant, no descendant with tabindex」这条后代级约束的原文出处。
  • Nu Html Checker validator.w3.org W3C 官方 conformance 检查器。把本页 <object> 那个 case 输入其中,会得到「The element “a” with the attribute “href” must not appear as a descendant of the “a” element.」(该预设的 object 缺 data 属性, 另有一条无关报错)—— 与解析器的实际行为形成对照。
  • MDN · <a>: The Anchor element developer.mozilla.org 逐项说明 <a> 的技术约束(Technical summary 里的 Permitted content / Implicit ARIA role),以及「<a> 不可嵌套、不可包含交互内容」的可读版解释。
  • MDN · DOMParser developer.mozilla.org 本页解析所用接口:parseFromString(str, "text/html") 走的就是浏览器标准 HTML 解析算法, 与页面加载同源; 差别在于它创建的 Document 没有浏览上下文、scripting flag 为 disabled, <noscript> 等少数分支的结果与真机不同。