写这篇之前先说明一下:这是“CSS面试必考八股文”系列的第二篇。上一篇把CSS选择器、盒模型、层叠规则、BFC这些底层概念过了一遍,这次接着聊更容易在面试中被追问、也更容易暴露水平的几块内容:Flex/Grid布局原理、定位与层叠上下文、动画与性能,以及移动端那些坑。如果你正在准备前端面试,或者带人复盘基础,这篇应该能帮你省不少时间。
1. Flex布局:面试里最常被追问的细节
1.1 flex-grow、flex-shrink、flex-basis的分配逻辑
Flex布局是面试必问,但大部分候选人只背了“水平垂直居中”那一套。真正拉开差距的,是问他 flex-grow / flex-shrink / flex-basis 这三个属性的分配逻辑。
先说 flex-basis。它决定了项目在主轴上的初始尺寸,优先级高于 width(在没有被 flex-grow/shrink 影响的情况下)。默认值是 auto,意思是“看项目自身的 width 属性”;如果设置成具体数值,比如 flex-basis: 200px,那项目的主轴尺寸就是 200px。关键考点是:flex-basis 设置的是 content-box 还是 border-box?答案取决于 box-sizing,面试官问到这里基本是在考察你对盒模型的理解是否贯通。
然后是 flex-grow,它解决的是“空间有剩余时怎么分配”。假设容器主轴剩余空间是 300px,三个项目 flex-grow 分别是 1、2、3,那剩余空间会按比例 1:2:3 分配,也就是 50px、100px、150px。这里有个细节:flex-grow 分配的是剩余空间,不是最终尺寸的比例。很多候选人搞混这一点,会答成“按自身宽度的比例放大”,这是错的。
flex-shrink 则是“空间不足时怎么压缩”。容器总宽 600px,三个项目 flex-basis 都是 300px(总 900px),溢出 300px。如果 flex-shrink 都是默认值 1,那每个项目都压缩 100px,最终每个 200px。但如果某个项目设置 flex-shrink: 0,它就不会被压缩,其他两个项目要承担全部 300px 的压缩量,各变成 150px。
面试加分回答:flex-shrink 的压缩不是简单按 shrink 值比例均摊,而是“先按 flex-shrink 比例、再乘以其 flex-basis 权重”来计算。场景是各项目 flex-basis 不同,较大的项目会被压缩更多。这里如果能把公式讲出来,我一般直接让他过。
1.2 主轴与交叉轴:justify-content和align-items的易混点
另一个高频考点是主轴(main axis)和交叉轴(cross axis)的区分。flex-direction 默认是 row,主轴水平从左到右;改成 column 后主轴变成垂直从上到下。justify-content 永远作用于主轴,align-items 作用于交叉轴,这个别搞反。
实际面试中一种常见问法是:flex-direction: column 时,如何实现水平居中?不少候选人下意识回答“justify-content: center”,然后被面试官反问“主轴不是垂直了吗”,才反应过来应该是 align-items: center。这就是对主轴变换不敏感。
还有一个进阶坑:align-content 和 align-items 的区别。align-content 只在“多行/多列”时生效,它控制的是整个 flex 行集合在交叉轴上的分布方式;align-items 控制的是单行内各项目在交叉轴上的对齐方式。如果容器只有一行,align-content 不起作用。这个点能讲清楚,说明你是真用过,不是背的。
1.3 面试加分:flex:1到底等于什么
很多前端常用 flex: 1 来实现“剩余空间占满”,但问他 flex: 1 实际展开成什么,经常答不上来。答案是 flex: 1 1 0%,即 flex-grow: 1、flex-shrink: 1、flex-basis: 0%。
这里有个关键区别:flex-basis: 0% 和 flex-basis: auto 在分配空间时的行为完全不同。base 是 0%,则所有项目从 0 开始按 grow 比例分配容器宽度,比如三个项目都 flex: 1,则三等分;如果 base 是 auto,项目会先按内容宽度占据空间,剩余空间再按 grow 分配,导致内容多的项目最终更宽。这种细节笔试中出现过很多次,值得留意。
常见的简写对照:
| 简写 | 展开 | 实际效果 |
|---|---|---|
| flex: 1 | flex: 1 1 0% | 等分剩余空间,按比例伸缩 |
| flex: auto | flex: 1 1 auto | 按内容尺寸为基础,再分配剩余空间 |
| flex: none | flex: 0 0 auto | 不伸缩,保持内容尺寸 |
| flex: 0 1 auto | 默认值 | 可收缩但不可放大 |
实操心得:移动端适配里,底部导航栏的中间按钮经常需要“绝对居中但两侧空间不相等”,用 flex: 1 + justify-content 不好处理时,我习惯给左右各一个 flex: 1 的空元素占位,再给中间按钮固定尺寸,简单可靠,比 position 计算偏移量省事太多。
2. Grid布局:从“能看懂”到“能讲明白”
2.1 核心属性速记:容器与项目的分工
CSS Grid 在面试中出现的频率这几年明显上升,尤其是涉及复杂页面布局的岗位。Grid 的核心考点在于“容器属性”和“项目属性”的分工。
容器属性包括:grid-template-columns / grid-template-rows(划分行列)、grid-template-areas(区域命名)、grid-gap(间距)、justify-items / align-items(单元格内对齐)、justify-content / align-content(网格整体对齐)。项目属性包括:grid-column / grid-row(合并与定位)、grid-area(区域简写)、justify-self / align-self(自身对齐)。
面试官经常问一个类比题:“Grid 和 Flex 的区别是什么?”比较准确的回答是:Flex 是一维布局模型,适合处理“一行或一列”内的对齐与分配;Grid 是二维布局模型,同时管理行和列。Flex 关注内容沿主轴的流动,Grid 关注网格单元格的放置。实际操作中两者常结合用——页面骨架用 Grid,组件内部用 Flex。
2.2 fr单位与auto的差别
grid-template-columns: 1fr 1fr 和 grid-template-columns: auto auto 在实际呈现上差别很大。fr 是“剩余空间份额”,会把容器可用空间扣除固定尺寸后按比例分配;auto 是“内容自适应”,列宽由该列最宽项目的内容决定。
举个例子:容器宽 900px,grid-template-columns: 200px 1fr 2fr,那么第一列固定 200px,剩下 700px 按 1:2 分配给第二、三列,即约 233px 和 467px。如果是 auto 1fr,auto 列会被内容撑开,剩余的再给 1fr。这个区别在响应式布局中很常用,比如侧边栏固定宽度、主内容自适应,就可以用 grid-template-columns: 240px 1fr。
还有 minmax() 函数,它是 Grid 里非常经典的考点。grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)) 可以实现“列数自适应”的卡片布局——容器宽就多排几列,窄就少排几列,但每列不小于 200px。这个写法的好处是无需媒体查询就能完成一定程度的响应式,值得在项目中实践。
2.3 实战中的Grid场景
场景一:经典双栏布局,左侧 240px 固定,右侧自适应。用 Grid 就是一行代码:grid-template-columns: 240px 1fr。
场景二:圣杯布局(header + 三列 + footer),Flex 时代要套三层容器,Grid 时代一行搞定:
.layout { display: grid; grid-template-columns: 200px 1fr 200px; grid-template-areas: "header header header" "left main right" "footer footer footer"; grid-template-rows: auto 1fr auto; min-height: 100vh; }配合 grid-area: header 等命名,代码可读性直接上升一个档次。面试时如果能主动写出 grid-template-areas 方案,会比只会 flex 的候选人更有亮点。
场景三:商品卡片网格,图、标题、价格、按钮左对齐,用 Grid 的 align-items 控制每行高度一致,同时项目内部用 flex-direction: column 让底部按钮始终贴底。Grid 管“外部对齐”,Flex 管“内部排布”,两者配合是真实项目中最常用的组合。
注意:Grid 的兼容性在 2024 年之后基本无需担心,现代浏览器全面支持。但如果你维护的是老项目(比如嵌在低版本 WebView 里),建议先用 display: grid 做个基础降级,再配合 @supports 做增强,不要一上来就全量替换。
3. 定位与层叠:看似简单实则高频翻车
3.1 relative、absolute、fixed的细节考点
定位相关的面试题表面简单,但陷阱很多。面试官最爱问:relative 的定位参考点是什么?答案是“元素自身原本的位置”;absolute 的定位参考点是什么?是“最近的已定位祖先(position 不为 static)”,如果找不到,就是初始包含块(通常是视口)。这个“最近的已定位祖先”就是关键考点。
还有一个细节:absolute 元素如果祖先设置了 transform 属性,即使它的 position 是 static,也依然会成为 absolute 的定位参考。因为 transform 会生成“包含块”。这是很多候选人答不出来的点,也是开发中遇到“明明没有定位祖先,元素却跑了”的常见原因。
fixed 和 absolute 的区别也要能讲清楚:fixed 默认相对于视口定位,但如果祖先有 transform、filter、perspective 等属性,fixed 会变成相对于该祖先定位。这个特性在“弹窗被裁剪”的问题中是核心原因。面试如果聊到弹窗组件,一定要主动提到:给弹窗的 DOM 树里任何一个父节点加 transform,都会破坏 fixed 的视口定位。
3.2 sticky的“粘性定位”有哪些隐藏陷阱
position: sticky 是新面试题里的常客。它结合了 relative 和 fixed 的特点:元素在滚动过程中,先按正常文档流排列,到达指定阈值后“粘”在视口的某个位置。常见写法是 top: 0,表示元素顶部距离视口顶部 0 时开始吸顶。
但 sticky 有非常多的触发条件,稍不注意就不生效。最核心的:父容器高度不够、或父容器 overflow: hidden / auto / scroll,都会导致 sticky 失效。原理是 sticky 的作用范围被限制在父元素内,父元素 overflow 不为 visible 时会建立一个“滚动上下文”,sticky 在这个上下文内无法相对视口计算位置。面试时回答出“sticky 的偏移计算受最近滚动祖先影响”这个层面,基本就能过关。
另一个实操建议:吸顶头部和吸底按钮组合时,sticky 会比 fixed 更合适。因为 sticky 元素不影响文档流,不需要给内容区额外加 padding 来补偿头部高度,滚动到页面底部时按钮也不会遮挡内容。
3.3 z-index和层叠上下文:为什么设置了z-index还是不生效
z-index 不是越大越靠前,这个错误认知在面试和工作中都很常见。z-index 只在“同一层叠上下文”内比较大小。如果两个元素分别位于不同的层叠上下文中,它们的 z-index 数值大小没有直接可比性,实际层级由祖先层叠上下文的层级决定。
哪些情况会创建层叠上下文?常见的有:position 非 static 且 z-index 不是 auto;opacity 小于 1;transform 非 none;filter 非 none;will-change 指定了上述属性;flex/grid 子项且 z-index 非 auto;display: flow-root。
所以,“z-index: 99999 还是被盖住”的排查思路应该是:沿 DOM 树往上找,看元素的父级、祖父级是否创建了层叠上下文,然后把 z-index 往上层加,而不是无限放大当前元素。这里是“八股文”和“实战能力”的分水岭。
独家技巧:排查层叠问题时,我一般在 DevTools 里给目标元素临时加一个 outline: 3px solid red,确认它渲染在了正确位置,再用“元素面板 -> 层叠上下文树”来看各层级的包含关系。Chrome 的 Layers 面板特别适合这类问题。
4. 动画与transform:视觉题也能问出深度
4.1 transform中rotate3d、translateZ等考点
CSS 动画面试题现在越来越偏向视觉还原类。rotate3d 是其中一个典型考点。
rotate3d(x, y, z, angle) 的参数含义是绕一个三维向量旋转。比如 rotate3d(1, 0, 0, 45deg) 是绕 X 轴旋转 45 度,元素像翻书页一样翻转;rotate3d(0, 1, 0, 45deg) 绕 Y 轴旋转,像旋转门;rotate3d(0, 0, 1, 45deg) 等价于 rotate(45deg),绕 Z 轴平面旋转。
transform-origin 是另一个考点,它决定变换的基准点。默认是 50% 50%,即元素中心。如果做“开关门”效果,应将 transform-origin 设置为左边或右边(比如 left center),否则看起来像中心旋转而不是门轴旋转。
深一层的问题是:为什么 transform: translateZ(0) 能触发 GPU 加速?原因是它把元素提升为一个合成层(composited layer),由 GPU 直接合成,避免重新触发布局和绘制。面试回答到这里,顺带提一句“但不要滥用,否则合成层过多反而导致内存压力”,会显得你有真实项目经验。
4.2 动画性能:为什么用transform/opacity而不是left/top
这是前端性能面试中几乎必问的考点。在动画过程中,修改 left/top 会触发布局(layout)和绘制(paint),每一帧都要重新计算几何信息,性能开销很大;而修改 transform 和 opacity 只触发合成(composite),由 GPU 处理,开销小得多。
背后的原理是浏览器渲染流水线:JavaScript -> Style -> Layout -> Paint -> Composite。如果动画修改 left,从 Style 阶段开始就要全面重算 Layout,一帧内容变化大;如果修改 transform,浏览器可以直接在合成阶段复用之前的纹理,做位移动画,开销接近“免费”。
实操上的建议是:能用 transform 实现的动画就不用 left/top;能用 opacity 做的淡入淡出就别用 display: none 硬切。另外 will-change: transform 可以提前告诉浏览器“这个元素将要变换”,让它提前建合成层,但它不是万能的,用得太多反而会消耗内存,建议只对动画中的元素使用。
4.3 字体渐变、涟漪光圈等视觉效果的实现原理
字体渐变在热搜词里出现频率很高。实现方式其实不复杂,核心是利用 background-clip: text 把背景裁切到文字形状上,再配一个渐变背景:
.gradient-text { background: linear-gradient(90deg, #ff6a00, #ee0979); -webkit-background-clip: text; background-clip: text; color: transparent; }这里有个值得在面试中提一句的点:color 必须设为 transparent,不然文字默认颜色会盖住背景。另外 background-clip: text 的浏览器兼容性已经很好,但某些 WebView 上仍需要 -webkit- 前缀。
涟漪光圈扩散效果(ripple)的实现思路是:一个圆形元素,不断放大并降低透明度。核心是利用 transform: scale 和 opacity 的动画组合实现,生成多个层叠的波纹时,每个波纹用不同的 animation-delay 错开时间,效果就能连续起来。这类“视觉题”的考点往往不是效果本身,而是你能不能拆解成 transform/opacity 的合理组合,并回答出性能差异。
5. 移动端与设备兼容:面试官最爱问的工程化问题
5.1 1px问题的成因与几种解法
移动端 1px 问题几乎人手一份答案。成因是:设备像素比(devicePixelRatio)大于 1 时,CSS 的 1px 在物理屏幕上实际占据多个物理像素,导致线条看起来“偏粗”。常见解法有四种:
方案一:transform: scaleY(0.5)。给元素加伪元素,高度为 1px,再垂直缩放 0.5:
.hairline { position: relative; } .hairline::after { content: ""; position: absolute; left: 0; top: 0; width: 200%; height: 1px; transform-origin: left top; transform: scaleY(0.5); }方案二:使用 0.5px 单位。border: 0.5px solid #eee。但部分安卓机不支持 0.5px。
方案三:使用线性渐变模拟。background: linear-gradient(to top, #eee 50%, transparent 50%),实际上是绘制半像素高度的渐变,视觉上更像细线。
方案四:postcss-write-svg、postcss-border-1px 等工具方案,原理还是 transform 缩放。
面试如果提到 1px,我建议主动说出方案一和方案四的取舍:方案一的伪元素方案最稳,兼容性最好;工具方案适合批量处理,但需要配合构建流程。
5.2 苹果底部安全区与CSS的兼容方案
热搜词里“小程序苹果底部兼容css”指向的就是 iOS 全面屏底部安全区问题(Home Indicator 区域)。
方案核心是 env(safe-area-inset-bottom)。iOS 11 及以上支持,配合 viewport-fit=cover 才能生效。在网页中需要设置:
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />然后给底部固定元素加:
.bottom-bar { padding-bottom: env(safe-area-inset-bottom); }如果担心低版本 iOS 不支持 env,可以加一层 fallback:
.bottom-bar { padding-bottom: 15px; padding-bottom: constant(safe-area-inset-bottom); /* iOS 11.0-11.2 */ padding-bottom: env(safe-area-inset-bottom); /* iOS 11.2+ */ }小程序里的写法类似,但要注意小程序环境对 env 的支持要配合官方提供的兼容方案;常见思路是在页面的容器上设置 padding-bottom,或者用占位元素撑开底部安全区。另一个容易踩的坑:如果页面本身有滚动区域,在滚动容器上直接加 padding-bottom 可能无效,要在滚动内容的内部容器上加。
踩坑记录:我遇到过“底部按钮被 Home Indicator 遮挡”的问题,当时只设置了 padding-bottom: constant(...),但 Android 端不认 constant,导致安卓上底部留白过大。最终方案是配合 @supports (padding-bottom: env(safe-area-inset-bottom)) 做特性检测,只有支持 environ 的机型才加安全区 padding。
5.3 文本位置调整的常见场景与坑
热搜词里“怎么调整css容器里的文本位置”其实覆盖了非常多面试题:单行文本垂直居中(line-height = height)、多行文本垂直居中(flex + align-items: center)、文本水平居中(text-align: center)、文字与图标对齐(vertical-align: middle 或 flex 布局)等。
面试里有一个经典陷阱:line-height 实现垂直居中,对单行文本有效,但文本换行后 line-height 仍然生效,多行之间行距过大、内容超过容器高度,就会出现视觉偏移。此时应该用 flex 或 grid 的对齐属性,而不是依赖 line-height。
另一个关于 div 底部对齐的问题也很高频。想让元素固定在父容器底部,最简单的是 flex + margin-top: auto,或父级设置 flex-direction: column,子级用 margin-top: auto 也可以实现。Grid 方案则是 grid-template-rows: 1fr auto,下面那行自动贴底。相对比 position: absolute + bottom: 0 更方便的地方在于:它不脱离文档流,不会遮挡其他内容。
6. 高频CSS面试题速查表
整理一个表格,方便考前快速过一遍:
| 问题 | 考察点 | 推荐回答要点 |
|---|---|---|
| flex: 1 展开是什么? | flex 简写 | flex: 1 1 0% |
| flex-shrink 的压缩逻辑 | 空间分配 | 按 shrink 比例与 basis 权重综合计算 |
| Grid 和 Flex 区别 | 布局模型 | 一维 vs 二维 |
| fr 和 auto 的区别 | Grid 单位 | fr 是剩余空间份额;auto 由内容决定 |
| sticky 不生效原因 | 定位限制 | 父容器高度不够或 overflow 非 visible |
| z-index 失效原因 | 层叠上下文 | 不在同一个层叠上下文内比较 |
| transform 为什么比 left 动画流畅 | 渲染流水线 | 只触发合成,不触发布局与绘制 |
| background-clip: text 的作用 | 视觉效果 | 背景裁切到文字,配合 color: transparent |
| 1px 问题原因 | 设备像素比 | dpr > 1 导致物理像素不足 |
| env(safe-area-inset-bottom) | iOS 安全区 | viewport-fit=cover + env |
这张表适合在面试前一小时快速扫一遍。每一条被追问深一层时,都能回到前面章节的原理解释里去,能答到那个程度,CSS 这关基本就稳了。
关于 CSS 面试准备,我个人的体会是:不要只背结论,要把每个属性放在“浏览器渲染流程、层叠上下文、布局模型、性能开销”这几条主线上去理解。面试官追问的往往是结论背后的为什么,只要能把为什么讲清楚,结论本身是不是一字不差反而不太重要。这也是这个系列想要传达的:八股文不是背出来的,是理解出来的。