
昨天工作群里又有人问了一个我听过不下二十遍的问题“同一个元素我 class 写了 10 个为什么颜色还是被一个 id 样式压着”我当时没直接回答案而是把权重计算过程贴了一遍。这个问题的本质就是 CSS 选择器权重值如何计算以及!important在这套规则里到底处在什么位置。很多前端写了一两年能背出“id 大于 class、class 大于标签”但一到现场调试就被各种诡异现象带偏。这篇就当作【H5 前端开发笔记】第 24 期把 CSS 层叠样式表里最绕人的这块彻底理清楚。1. 为什么“高权重”能压死人先看懂层叠的三层裁决1.1 层叠到底在比什么CSS 的全称是 Cascading Style Sheets重点就在“Cascading”这个词上也就是层叠。所谓层叠指的是当同一个元素的同一个属性出现了多个不同来源的声明时浏览器需要从中选出一个“最终生效”的值。很多人以为只比选择器权重其实不对真正的裁决顺序是三段式的先比声明的重要性有没有!important以及声明来自哪里重要性相同再比选择器的权重权重也相同最后比源码顺序后出现的覆盖先出现的。这个顺序我建议直接刻在脑子里因为绝大多数让人困惑的覆盖问题都是这三步走中某一步被跳过导致的。比如有人加了!important还是被压下去多半就是漏了“重要性相同之后还要回头比选择器权重”这一条。1.2 来源、权重、顺序这三者什么时候适用“来源”听起来很官方其实就是指样式来自哪里浏览器默认样式user agent、页面作者写的样式、还是用户自定义样式。对普通前端开发来说90% 的时间只需要考虑作者样式和你看到的浏览器默认样式之间的冲突。浏览器默认样式优先级最低作者普通声明优先于浏览器默认样式带!important的作者声明优先于所有作者普通声明。一旦比较范围落在“同重要性、同来源”内才轮到选择器权重出场。权重也相同才会看谁在源码里更靠后。这就像一场三局两胜的比赛第一局重要性就分出胜负后面根本不需要打第一局打平才进第二局比权重。1.3 内联样式为什么能单独占一个“高位”很多人困惑的一点是stylecolor: red这种写在 HTML 标签上的内联样式并没有选择器为什么它总能把外部样式表里的规则压下去原因在于内联样式在权重计算里单独占据最高位。后面讲权重时你会看到权重的第一位就是留给内联样式的。换句话说内联样式等于默认自带了一层“外挂”它不是靠选择器赢的而是靠规则本身的设计赢的。真正能正面压过内联普通样式的只有作者声明的!important规则这一点我会在第四节展开。2. 选择器权重值怎么算四位“数位”拆开看2.1 四个位代表什么从内联到伪元素官方规范里选择器权重用四个数位表示常见写法是(a, b, c, d)每一位代表不同的选择器类别从高到低分别是a 位内联样式style这是最高位b 位id 选择器比如#app、#headerc 位类选择器、属性选择器、伪类比如.btn、[typetext]、:hoverd 位元素选择器和伪元素比如div、span、::before。比较规则是从高位到低位逐位比较a位有差异就直接结束a位相同再看b位b位相同再看c位最后才是d位。任何低位的数值都不可能“累计”到高位去超越一位之差。2.2 常见选择器归位对照表很多初学者记不住某类选择器到底归到哪一位我整理了一张对照表实测下来比死记公式好用得多选择器类型示例归属位内联样式stylecolor:reda 位id 选择器#main、#sidebarb 位类选择器.button、.activec 位属性选择器[typetext]、[disabled]c 位伪类:hover、:focus、:first-childc 位元素选择器div、p、spand 位伪元素::before、::after、::selectiond 位通配符*不计入权重为 0组合器、、~、空格不计入这里最容易被带偏的是伪元素和伪类的区分。:hover、:focus是伪类归 c 位::before、::after是伪元素归 d 位。别小看这一位之差在特定场景下就是压倒对手的关键。2.3 为什么不能把权重当十进制求和网上很多教程会把权重写成id100、class10、元素1然后直接求和比大小。这个说法能帮助理解大方向但严格来说是错的而且会推导出反例。规范里的权重是“逐位比较”不是“加权求和”。举个例子十个类选择器写在一起按求和算法权重是 100和一个 id 选择器的 100 打平但按规范的正确算法十类的权重是(0, 0, 10, 0)id 是(0, 1, 0, 0)第二位1直接大于0不管 c 位有多少id 都赢。这个细节没有搞清楚后面遇到类似场景就会怀疑人生。值得强调的是浏览器引擎比较权重时也是逐位比较的不存在“把 10 个类累加成 id”这种换算。2.4 新选择器 :is / :not / :where 的权重陷阱现代 CSS 里:is()、:not()、:where()越来越常用它们对权重的影响也有明确规则:not()本身不计入权重但它参数列表里选择器的权重照常计入:is()的权重取参数列表中权重最高的那一个:where()的权重永远恒定为 0是一个“权重清零器”。举个例子div:not(.active)的权重是(0, 0, 1, 1)因为:not()参数里的.active照算。而:where(#app) .card的权重只是(0, 0, 1, 0)因为:where()把参数里的#app清零了。这个特性在实际工作中非常有用比如你想让某个基础组件样式“低权重化”方便使用者覆盖:where()就比普通的类选择器更好。3. 现场推演几个最容易算错的实际案例3.1 十个类选择器对抗一个 id 的经典实验先把刚才的理论落到代码上。下面这段p里的文字最终显示什么颜色style .a.b.c.d.e.f.g.h.i.j { color: blue; } #target { color: red; } /style p idtarget classa b c d e f g h i j文字颜色/p答案是红色。前一条规则权重(0, 0, 10, 0)后一条(0, 1, 0, 0)。比较时先看 b 位1和0之间已经有了胜负后面的 c 位根本没有机会参与。这个实验我让实习生亲手跑过之后他才真正扔掉“100 分等于 100 分”的错误直觉。3.2 后代选择器里的复合权重累加复合选择器的权重计算方式是按“组成部分逐项累加”组合器本身不计入。比如#box .item span { color: green; }权重拆解#box占 b 位 1.item占 c 位 1span占 d 位 1结果为(0, 1, 1, 1)。再对比一条#box .item .child { color: blue; }这条是(0, 1, 2, 0)。虽然 d 位少了一个元素但 c 位两个类压过一个元素所以最终蓝色生效。累加时建议老老实实写下四元组眼睛看一遍容易漏。3.3 属性选择器和伪类到底算哪个位属性选择器和伪类都归 c 位这一点很多人会忽略。比如[typetext] { border: 1px solid #ccc; } input:focus { border-color: #00f; }这两条的权重分别是(0, 0, 1, 1)和(0, 0, 2, 1)后者多一个伪类所以聚焦边框色能正常覆盖默认边框。如果把input:focus写成div:focusd 位仍然是 1不影响结论但如果你把伪元素::placeholder误当伪类和属性选择器打架时就算错了位数。3.4 继承来的样式为什么总是输继承属性的优先级有个特殊地位它连最低的 0 权重都没有。什么意思你给body设置color: red里面的p标签“继承”到了红色但如果此时有一条* { color: blue; }通配符权重为(0, 0, 0, 0)即使它数值为 0也会赢过继承值。因为“继承”根本不在权重比较的体系里它是所有样式指定值都缺席时才会启用的兜底方案。基于这个机制实际开发里经常出现的坑是想给某个子元素“取消”父级继承的颜色只写一个空声明是不够的必须显式指定值否则继承值依然存在。你写color: initial才能把它拉回初始状态。3.5 import 加载顺序带来的假象样式顺序影响胜负而import在顺序上有个隐蔽行为它必须写在样式文件的最前面并且它引入的内容相当于被插入到当前样式的“那一个位置”。你在这个样式文件里先import了一个第三方库再写自己的规则那么自己的规则因为靠后在同权重下会赢。但如果你把import放在另一个文件里而这个文件在整个页面的link顺序中比较靠前那第三方样式大概率会输给后面link引入的样式。很多人排查半天以为是权重问题最后发现是引入顺序问题。需要特别留意的是import会阻塞渲染生产环境一般不建议用但如果你在维护老项目遇到“明明后写的样式却不生效”这类现象时先去看看import到底把样式插到了哪个位置。4. !important 优先级真相它改变的是声明层级不是权重值4.1 带 !important 意味着什么!important加在声明值的末尾例如color: red !important;。它修改的并不是选择器的权重数值而是这条声明的“重要性层级”。前面说过层叠比较先看重要性而!important让一条作者声明直接跳出普通作者声明的层级进入到一个更高的分组里。所以结论是作者!important声明的优先级高于所有作者普通普通声明哪怕普通声明来自内联样式如果两个声明都带!important就比较选择器权重如果权重还一样再比较源码顺序。这才是完整的规则。只记住“!important 最大”是不对的因为它只在“对比双方一方有、一方没有”时成立。4.2 两个都带 !important回落到权重和顺序很多人不知道!important并不是“绝对免死金牌”两个都带的时候还是要进入常规比较流程。比如#content .title { color: red !important; } .page .title { color: blue !important; }此时权重分别是(0, 1, 1, 0)和(0, 0, 2, 0)b 位1 0红色生效。反过来如果双方权重完全一样比如.title { color: red !important; } h2 { color: blue !important; }假设元素本身就是h2 classtitle前一条权重(0, 0, 1, 0)后一条(0, 0, 0, 1)依然是 title 赢因为 c 位压过 d 位。真正走到“同为重要、同权重”时才是后写的赢。4.3 真实案例覆盖框架默认样式的完整排查链路我在项目里遇到过一件印象很深的事第三方 UI 库给某个按钮写了一条约 0.3 个权重的默认样式我用普通选择器怎么都覆盖不掉最后加了!important才算成功。本来以为事情到此结束结果过了两周业务方要换主题色我在另一个样式文件里同样写了带!important的规则却发现无论如何都压不过原来的库样式。当时排查链路如下我先打开浏览器 DevTools 的 Elements 面板选中那个按钮看右侧 Styles 区域哪条规则划线了新版 Chrome 会直接显示“specificity: (0, 1, 0, 0)”这类提示确认两条规则都带!important然后对比两条规则的选择器权重发现库规则用的是#app .btn-primary而我的业务规则只是.btn-custom权重输了!important数量再多也没用最终解法不是继续堆!important而是把业务规则的选择器改成.theme-dark .btn-custom把权重抬到“至少相同或更高”。这个过程里最重要的一步是意识到两条规则都带!important之后比较逻辑重新回到了“权重比大小”。很多人在这一步卡住是因为脑中始终只有“important 赢 important 输”这种二元判断。4.4 什么情况下别用 !important我在代码评审里经常看到滥用!important的情况。它的正确用途是处理你无法修改的第三方样式、临时热修、或者动态脚本注入的内联普通样式。在这些场景外我建议默认别用它。因为!important会破坏层叠的自然顺序让样式表变得难以维护。你今天用一条!important赢了明天别人不知道这条规则他的!important只要权重比你高、或者顺序比你靠后就能把你压下去。与其互相堆人质不如从一开始就想清楚到底是权重选得不对还是结构设计有问题。5. 同权重规则之间的胜负源码顺序和其他隐形变量5.1 后写的赢link、style、内联的顺序博弈权重完全相同时裁决权交给源码顺序规则很简单同一声明级别下后出现的覆盖先出现的。这里要注意“源码顺序”不是只指同一个 CSS 文件里的行数而是指浏览器解析到这条规则的先后顺序。比如页面先引入a.css再引入b.css两者里都写了.box { padding: 8px; }最终b.css里的值生效。同理HTML 里的style标签和link标签也是按它们在文档中的出现顺序来算的style写在link后面那style内同权重规则获胜。这是一个很简单但经常被忽略的机制。有一次同时加载了两个版本的组件样式就是因为link顺序写反了新版的东西全被旧版覆盖掉排查时第一反应是查缓存后面才发现是顺序问题。5.2 工程化后的顺序问题合并、压缩、按需加载随着项目进入工程化源码顺序变得更难直观判断。打包工具会把多个 CSS 文件合并成一个合并顺序取决于 import 的顺序按需加载场景下不同路由会异步注入样式后进入页面的模块样式很有可能“后来居上”。在实际开发中我遇到过的最离奇的一个问题是本地开发一切正常生产环境某个组件样式突然不对。后来发现是打包出来的 CSS 文件里基础样式被放在了组件样式后面同权重规则下基础样式反超了组件样式。这个案例想说明的是工程化工具可以帮你处理模块依赖但不会替你理解层叠顺序。在关键样式上要么明确权重差异要么就不要依赖源码顺序来分胜负。我的习惯是基础类样式尽量只占一层低权重组件类样式保持同一层级、不要互相裸写冲突把顺序因素带来的波动降到最低。5.3 养成让顺序规则变“可控”的编码习惯说完工程化的坑再说说如何从编码端减少这类问题组件样式尽量限定在组件根节点下避免全局裸类名互相覆盖公共样式与业务样式分离业务样式放在后面统一加载同一层级的“状态类”用专门的命名比如.is-active、.is-disabled避免和结构类样式搅在一起需要被外部覆盖的组件用:where()把基础权重清零把“覆盖权”主动让给使用者。这些习惯并不会直接让权重消失但能让权重和顺序在团队协作中变得可预期。写 CSS 和写逻辑代码一样最怕的就是“不可控的隐式规则”。6. 权重计算的冷门边界与自测题6.1 通配符 *、继承值与“零权重”比赛通配符*的权重是(0, 0, 0, 0)但它仍然比“继承值”强这一点前面提过。以实际场景来说* { box-sizing: border-box; }这条规则会作用于所有元素因为每个元素都匹配*即便权重为 0它也是“指定值”。而如果是body { color: red; } p {}你期望p的文字颜色是什么呢答案还是红色因为它从body继承而来但这里生效的原因不是p的样式命中而是p自身没有任何color声明继承值兜底了。这两者之间存在一个微妙的差别继承值的来源不是选择器权重而是没有声明时的回退逻辑。6.2 动态伪类和表单状态选择器的权重表单场景下状态选择器经常被用来做交互反馈。:checked、:disabled、:focus都属于伪类归 c 位[checked]、[disabled]属于属性选择器也归 c 位。所以input:checked label { color: blue; } input[typeradio]:checked label { color: green; }前一条权重(0, 0, 2, 2)后一条因为多了一个属性选择器权重变成(0, 0, 3, 2)所以最终绿色生效。这里如果只看选择器长度可能觉得第一条写得更短、更简洁但权重就是比不过没有道理可讲只能接受规则。6.3 自测五道题看看你是否真的会算我留了五道经典题大家可以自己先算一遍再看答案#app .header .title span的权重是多少ul li.item.active的权重是多少div:not(.hidden)的权重是多少:where(#app) .btn的权重是多少input[typetext]:focus的权重是多少答案分别是(0, 1, 2, 1)一个 id、两个类、一个元素(0, 0, 2, 2)两个类、两个元素(0, 0, 1, 1)类的权重照算伪类不计只算参数里的类(0, 0, 1, 0):where()把#app清零了(0, 0, 2, 1)属性选择器加伪类共两个 c 位。如果你能全部算对说明这一套规则已经建立起来了。如果哪一题算错了回看第二节的对照表和逐位比较逻辑大概率能找到遗漏。最后再说一个我自己的调试习惯。遇到样式冲突我先打开 DevTools 的 Computed 面板点一下那个属性值浏览器会直接告诉我最终是哪条规则赢了并提供跳转链接。原因查清楚之前我不轻易上!important因为大部分所谓“诡异覆盖”其实都是权重或顺序没看清楚而已。把这一整套层叠规则内化以后写 CSS 的时候会踏实很多至少不会再被“为什么改了不生效”这种问题反复折磨。