在编程面试中,FizzBuzz 是一个经典的入门问题,要求输出从 1 到 100 的序列:能被 3 整除输出 “Fizz”、被 5 整除输出 “Buzz”、同时被 3 和 5 整除输出 “FizzBuzz”,否则输出数字本身。传统上,这需要 JavaScript 或其他脚本语言,但 Susam Pal 在其博客中展示了一个惊人的纯 CSS 实现,仅用几行样式规则就完成了这个任务。
核心原理剖析
这个实现的精髓在于 CSS 的三个强大特性:
-
counter-increment 和 counter () 函数:模拟循环计数器。
- 在
<ul>或父容器上设置counter-reset: n 0;,每个<li>通过counter-increment: n;自动递增计数器。 - 使用
content: counter(n);在伪元素中显示当前数字。这相当于一个隐式的 for 循环:i++。
- 在
-
:nth-child (n) 选择器:实现模运算检查。
:nth-child(3n)选中第 3、6、9... 个元素,即 i % 3 === 0。:nth-child(5n)选中第 5、10、15... 个,即 i % 5 === 0。:not(:nth-child(5n))排除 5 的倍数,避免在 Buzz 时显示数字。
-
::before 和 ::after 伪元素:字符串输出与叠加。
- 默认在
::before显示数字。 - 3 的倍数覆盖
::before为 “Fizz”。 - 5 的倍数在
::after添加 “Buzz”,这样 15 的倍数就是 “FizzBuzz”。
- 默认在
这种设计充分利用了 CSS 选择器的优先级和伪元素的多内容叠加,无需任何 JavaScript 或 HTML 中的静态文本。
完整可运行代码
以下是完整 HTML + CSS 示例,直接复制到本地文件即可运行(建议在现代浏览器如 Chrome/Firefox 测试):
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>CSS FizzBuzz</title>
<style>
body { font-family: monospace; }
ul {
counter-reset: n 0;
list-style: none;
padding: 0;
display: flex;
flex-wrap: wrap;
max-width: 800px;
}
li {
counter-increment: n;
width: 80px;
height: 40px;
margin: 2px;
background: #f0f0f0;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid #ccc;
}
li:not(:nth-child(5n))::before {
content: counter(n);
}
li:nth-child(3n)::before {
content: "Fizz";
}
li:nth-child(5n)::after {
content: "Buzz";
}
</style>
</head>
<body>
<h1>CSS Pure FizzBuzz (1-100)</h1>
<ul>
<li></li><li></li><li></li><li></li><li></li>
<li></li><li></li><li></li><li></li><li></li>
<li></li><li></li><li></li><li></li><li></li>
<li></li><li></li><li></li><li></li><li></li>
<li></li><!-- 重复 li 到 100 个,此处省略,可用 JS 生成或手动 -->
</ul>
</body>
</html>
注意:为简洁,上例只列 20 个 <li>,实际需 100 个。可用工具生成,或在 devtools 中快速复制。
输出示例(前 15 项): 1 2 Fizz 4 Buzz Fizz 7 8 Fizz 11 Fizz 13 14 FizzBuzz
选择器优先级与逻辑执行顺序
CSS 规则按优先级应用:
- 基础:
li:not(:nth-child(5n))::before { content: counter(n); }—— 非 5 倍数显示数字。 - 高优先:
li:nth-child(3n)::before { content: "Fizz"; }—— 3 倍数覆盖前者为 Fizz(包括 15)。 - 独立:
li:nth-child(5n)::after { content: "Buzz"; }—— 5 倍数附加 Buzz。
对于 i=15:
- :nth-child (3n) 匹配 → ::before = "Fizz"
- :nth-child (5n) 匹配 → ::after = "Buzz"
- not (5n) 不匹配,但已被覆盖。
完美处理 FizzBuzz 优先级。
浏览器兼容性与参数优化
-
支持度:Chrome 1+, Firefox 3.5+, Safari 3.1+, Edge 12+。
nth-child和counter-increment均为成熟特性(CanIUse 确认)。 -
性能参数:
参数 推荐值 说明 li 数量 ≤500 过多导致 DOM 膨胀,渲染慢 counter-reset n 0 从 1 开始计数 display flex/grid 布局优化,避免 float content 字符串 / 计数器 长度 <50 字符,避免溢出 -
监控要点:
- DevTools Elements 检查 computed styles。
- Performance 面板观察 selector 匹配时间(nth-child O (n))。
- 回滚:若不支持,fallback 到纯 HTML 列表。
扩展与变体
- 自定义模数:改
3n为7n实现 Fizz (7)/Buzz (11)。 - 更短代码(152 字符 minified):
li{counter-increment:n}li:not(:nth-child(5n))::before{content:counter(n)}li:nth-child(3n)::before{content:"Fizz"}li:nth-child(5n)::after{content:"Buzz"} - 无限序列:结合 CSS Grid
repeat(auto-fill)但需 JS 辅助 li 生成(纯 CSS 难无限)。 - 风险限制:
- 无动态交互:纯静态。
- 复杂逻辑失效:仅适合简单模运算。
- specificity 冲突:避免其他规则覆盖。
为什么值得学习?
这个 hack 展示了 CSS 的计算能力(类似 Houdini/CSS Paint API 前身),启发无 JS 场景如 email 模板、静态页。代码高尔夫爱好者可挑战更短版本。
资料来源:
- Susam Pal 博客:展示 4 行核心 CSS,“li:nth-child (3n)::before { content: "Fizz" }”。
- Demo:https://susam.net/css-fizz-buzz.html(验证输出)。
(本文约 1200 字,聚焦可落地实现。)