CSS 的层叠、继承和初始值是怎么共同决定最终样式的?
简化版
浏览器先收集所有能命中元素的声明,按来源与重要性、层叠层、选择器优先级、作用域距离和源码顺序逐级决胜,再处理继承与属性初始值,最后得到计算值。inherit、initial、unset 和 revert 是主动控制这套流程的工具,含义并不相同。
详细版
CSS 最终值不是只靠“选择器权重”决定。对同一元素的同一属性,浏览器先比较声明来源和重要性,例如用户的 !important、作者的 !important、作者普通样式;同一来源内还会考虑 @layer、选择器优先级、作用域距离与书写顺序。
如果元素没有胜出的指定值,可继承属性通常从父元素取得计算值,不可继承属性则使用自身初始值。inherit 强制继承,initial 强制使用规范初始值,unset 对可继承属性等同 inherit、对其他属性等同 initial,revert 则回退到更低来源或浏览器默认样式。
工程上应优先靠清晰的层级、低特异性选择器和明确的组件边界控制样式,避免用不断叠加选择器和 !important 解决冲突。
完整版教学
一、先区分指定值、计算值和使用值
开发者写下的声明只是候选项,不一定成为元素的最终样式。浏览器先通过层叠选出“指定值”,再解析相对单位、变量和继承得到“计算值”,布局阶段才可能得到像素化的“使用值”。例如父元素字号为 20px,子元素写 line-height: 1.5,其计算结果保留为无单位倍率,实际行高才是 20 × 1.5 = 30px。
候选声明 → 层叠后的指定值 → 继承/变量/相对值计算 → 布局使用值 → 实际值
这个区分能解释为什么 getComputedStyle() 返回的结果有时和源码不同,也能解释百分比为什么要等到布局阶段才能完全确定。面试时只说“权重高的生效”会漏掉大半条流水线。
二、层叠算法是一组按顺序执行的裁判
浏览器不会把所有因素揉成一个总分,而是逐级淘汰。前一层已经分出胜负时,后一层没有机会翻盘;所以低优先来源里再复杂的选择器,也无法击败更高优先来源的声明。
| 比较顺序 | 关注点 | 常见例子 |
|---|---|---|
| 1 | 来源与重要性 | 浏览器、用户、作者,普通与 !important |
| 2 | 层叠层 | @layer reset, base, components |
| 3 | 选择器优先级 | ID、类/属性/伪类、元素/伪元素 |
| 4 | 作用域接近度 | @scope 内离作用域根更近者 |
| 5 | 源码顺序 | 前面全相同时,后写覆盖先写 |
例如 .card .title 的特异性高于 .title,但如果前者在低优先级层、后者在更高优先级层,仍然是后者胜出。层叠层把“谁有权覆盖谁”从选择器技巧提升成了架构规则。
三、选择器优先级应按三列比较
常用记法是 (a,b,c):a 为 ID 数量,b 为类、属性和伪类数量,c 为元素和伪元素数量。三列从左到右比较,不是把数字拼成十进制,也不存在“十个类等于一个 ID”。
#app .card h2 /* (1,1,1) */
.page .card.active /* (0,3,0) */
:where(#app .card) /* (0,0,0),:where() 自身及参数都不增加权重 */
:is(#app, .card) h2 /* (1,0,1),取参数中最高权重 */
第一条即使只有三个组成部分,也会击败拥有一百个类的选择器,因为 ID 列先决胜。:where() 很适合写可覆盖的基础样式,:is()、:not() 和 :has() 则把参数列表中最高的特异性带入计算。
四、继承只发生在规定可继承的属性上
文字相关属性如 color、font-family、line-height 通常可继承,盒模型属性如 margin、padding、border 通常不可继承。这是合理的:一段文字保持统一字体很有用,但父盒子的 20px 外边距若自动传给每个后代,会让布局迅速失控。
<section class="article"><p><em>文字</em></p></section>
.article { color: #334155; border: 2px solid; }
p 和 em 会继承颜色,但不会各自得到一圈边框。继承的是父元素的计算值,不是父元素原始声明文本;这点在 em、自定义属性和无单位行高上尤其重要。
五、四个全局关键字解决不同的回退需求
inherit 不管属性默认是否继承,都强制拿父元素的计算值;initial 使用 CSS 规范为该属性规定的初始值,未必等于浏览器默认样式。比如 display 的初始值是 inline,但浏览器样式表通常给 div 设置 display: block。
| 关键字 | 行为 | 适合场景 |
|---|---|---|
inherit | 强制继承父元素 | 让边框颜色等非继承属性跟随父级 |
initial | 回到属性规范初始值 | 清除当前元素上的作者样式 |
unset | 可继承则继承,否则取初始值 | 做通用属性重置 |
revert | 回退到较低来源/默认规则 | 恢复按钮、列表等原生表现 |
all: unset 会让组件接近“白纸”,也可能一并清掉 display 等关键属性;all: revert 更适合希望恢复浏览器可用默认值的场景。使用前必须知道自己要回到规范初始态,还是用户代理样式态。
六、用可维护的层叠架构代替权重军备竞赛
假设一个按钮依次经历基础、主题、组件和工具类四类样式,与其写 .page #app .toolbar button.primary,不如显式定义层叠层。低特异性选择器让局部覆盖可预测,也降低删除旧样式时的风险。
@layer reset, base, components, utilities;
@layer components { :where(.button) { color: white; } }
@layer utilities { .text-dark { color: #111827; } }
这里工具层声明即使选择器很简单,也能覆盖组件层。假设团队每次冲突多加两级选择器,五轮修补后可能累积十级嵌套;层叠层把这个增长压回固定的架构顺序。
记忆钩子:先问“哪一层有资格参赛”,再算选择器权重;继承只负责“没有指定值以后怎么办”。
七、常见误区与追问
- 误区:
!important会无条件击败所有声明。 用户重要声明、动画与来源顺序仍会影响结果,且相同重要性下还要继续比较层叠规则。 - 误区:内联样式永远无法覆盖。 作者
!important可以击败普通内联样式,脚本也能直接改内联声明。 - 误区:
initial就是恢复浏览器默认样式。 它回到属性规范初始值;恢复用户代理样式通常要考虑revert。 - 追问:自定义属性会继承吗? 普通
--token默认继承,并在使用处通过var()参与计算;可用@property改变其继承行为。 - 追问:动画为何能覆盖普通样式? 运行中的 CSS 动画和过渡在层叠来源中有专门位置,过渡值甚至能暂时压过重要声明。
- 追问:如何降低第三方 CSS 的覆盖风险? 把第三方样式放入较低优先级的
@layer,再用本地组件层和工具层形成明确边界。
八、加强记忆
把最终样式想成一次分阶段选拔:来源与重要性先发入场券,层叠层决定队伍顺序,选择器优先级、作用域距离和源码位置依次决胜。胜出后才进入值处理阶段;没有指定值时,可继承属性找父级,不可继承属性取初始值。需要人工改道时,inherit 找父级、initial 找规范起点、unset 按属性天性二选一、revert 找较低来源。遇到冲突先定位处于哪一阶段,而不是立刻增加选择器或 !important。