Hotdry.
application-security

纯 CSS 实现 FizzBuzz:counter-increment 与 nth-child 的魔力

利用 CSS 计数器模拟循环,nth-child 选择器巧妙检查 3 和 5 的倍数,伪元素输出数字或 Fizz/Buzz,实现纯样式表的 FizzBuzz 序列生成。

在编程面试中,FizzBuzz 是一个经典的入门问题,要求输出从 1 到 100 的序列:能被 3 整除输出 “Fizz”、被 5 整除输出 “Buzz”、同时被 3 和 5 整除输出 “FizzBuzz”,否则输出数字本身。传统上,这需要 JavaScript 或其他脚本语言,但 Susam Pal 在其博客中展示了一个惊人的纯 CSS 实现,仅用几行样式规则就完成了这个任务。

核心原理剖析

这个实现的精髓在于 CSS 的三个强大特性:

  1. counter-increment 和 counter () 函数:模拟循环计数器。

    • <ul> 或父容器上设置 counter-reset: n 0;,每个 <li> 通过 counter-increment: n; 自动递增计数器。
    • 使用 content: counter(n); 在伪元素中显示当前数字。这相当于一个隐式的 for 循环:i++。
  2. :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 时显示数字。
  3. ::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-childcounter-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 列表。

扩展与变体

  1. 自定义模数:改 3n7n 实现 Fizz (7)/Buzz (11)。
  2. 更短代码(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"}
    
  3. 无限序列:结合 CSS Grid repeat(auto-fill) 但需 JS 辅助 li 生成(纯 CSS 难无限)。
  4. 风险限制
    • 无动态交互:纯静态。
    • 复杂逻辑失效:仅适合简单模运算。
    • specificity 冲突:避免其他规则覆盖。

为什么值得学习?

这个 hack 展示了 CSS 的计算能力(类似 Houdini/CSS Paint API 前身),启发无 JS 场景如 email 模板、静态页。代码高尔夫爱好者可挑战更短版本。

资料来源

(本文约 1200 字,聚焦可落地实现。)

查看归档