前端面试实战(第1期):从安全区域到计时器,16道高频题深度拆解
面试题千千万,但真正考到细节时,往往容易卡壳。这篇整理了我最近面试中遇到的一些高频题,每题都做了详细拆解,希望能帮你查漏补缺。
1. 移动端适配:如何获取安全窗口区域?
全面屏手机(iPhone X 及之后)有刘海、圆角、底部 Home Indicator,内容如果贴边会被遮挡。解决方案是 CSS 的 env() 函数:
body {
padding-bottom: env(safe-area-inset-bottom, 20px);
}
safe-area-inset-bottom是由浏览器自动提供的 CSS 环境变量,分别有 top / right / bottom / left 四个方向。- 第二个参数
20px是备用值(fallback),当浏览器不支持env()时使用。
推荐完整写法:
body {
padding:
env(safe-area-inset-top, 10px)
env(safe-area-inset-right, 0px)
env(safe-area-inset-bottom, 20px)
env(safe-area-inset-left, 0px);
}
注意:这四个值是浏览器自动提供的,不是原生开发手动注入的。如果你需要兼容更老的浏览器,可以同时保留 constant() 作为兜底(iOS 11.0-11.2 使用 constant(),iOS 11.2+ 改用 env())。
2. Monorepo 中如何给某个子包添加依赖?
使用 pnpm 的 --filter 参数:
pnpm add lodash --filter pkg-a
这会在 packages/pkg-a 下安装 lodash 并写入其 package.json。类似的,--filter 也支持运行脚本、执行命令等操作,是 monorepo 管理的核心工具。
3. Vue 3 Teleport 和组件卸载的关系?
Teleport 允许你把组件内容渲染到 DOM 的任意位置(比如 body 下),但它仍然是当前组件的子组件。生命周期钩子依然由父组件控制——父组件卸载时,Teleport 里的内容也会被销毁。它只是"传送"了 DOM 节点,没有"传送"组件树的从属关系。
4. Tailwind CSS 如何自定义常用变量?
在 tailwind.config.js 的 theme.extend 中配置:
theme: {
extend: {
fontSize: {
'1r': ['1rem', '1.5'], // text-1r === 1rem 字号
'2r': ['2rem', '1.5'], // text-2r === 2rem 字号
}
}
}
数组第一个值是字号,第二个值是行高。配置后即可在模板中使用 text-1r、text-2r。
5. CSS :has() 选择器的实战用法
如何实现:鼠标悬浮在子元素上时,父元素变色?
/* child 悬浮时 parent 变色 */
.parent:has(.child:hover) {
background-color: lightgreen;
}
:has() 被称为"父选择器",它允许你根据子元素的状态来选择父元素。这在过去只能靠 JavaScript 实现。这类面试题近年很常见,因为它是 CSS 选择器领域的重大突破,能显著减少 JS 代码量。
6. shadcn 的主题是如何实现的?
shadcn 自己实现了一套基于 data-theme 属性和 CSS 自定义属性(CSS 变量)的主题系统:
:root {
--background: 0 0% 100%; /* 明亮模式背景色(HSL) */
--foreground: 222.2 84% 4.9%; /* 明亮模式前景色 */
--card: 0 0% 100%;
--border: 0 0% 89.8%;
}
[data-theme="dark"] {
--background: 222.2 84% 4.9%;
--foreground: 0 0% 98%;
--card: 222.2 84% 4.9%;
--border: 222.2 37% 17.5%;
}
通过切换 <html> 上的 data-theme 属性,组件引用的是同一套变量名(如 var(--background)),但实际值会自动切换。核心优势:组件不需要关心当前是什么主题,它只引用变量。
7. CSS @media prefers-color-scheme 是什么?
系统级暗黑模式检测:
@media (prefers-color-scheme: dark) {
body {
background: black;
color: white;
}
}
它读取的是用户操作系统主题设置。与 shadcn 那种手动切换 data-theme 的方式不同,这种方式是被动响应式的——用户改了系统主题,页面自动变化,不需要任何 JS 交互。
8. CSS :root 是什么?
:root 是一个 CSS 伪类,代表 HTML 的根元素(即 <html> 标签),但优先级高于直接写 html {}。通常用来定义全局的 CSS 自定义属性(CSS 变量):
:root {
--primary-color: #3b82f6;
--font-size-base: 16px;
}
因为它的优先级更高,适合作为主题变量的定义入口。
9. Vue 3 defineModel API 使用感受是什么?
defineModel 是 Vue 3.4+ 引入的语法糖,大幅简化了父子组件的双向绑定:
<script setup>
// 默认 prop 是 'modelValue',事件是 'update:modelValue'
const value = defineModel()
// 也可以自定义 prop 名
// const value = defineModel('value')
</script>
<template>
<input :value="value" @input="value = $event.target.value" />
</template>
以前需要手动声明 props 和 emits,然后在 update:modelValue 里手动处理。现在一行 defineModel() 搞定——它自动帮你生成了 prop 声明和对应的事件处理。
10. React useDeferredValue Hook 怎么用?
useDeferredValue 让你延迟更新某个值的渲染,从而优先处理紧急更新(如用户输入):
const [query, setQuery] = useState('')
const deferredQuery = useDeferredValue(query)
- 当用户在输入框打字时,
query立刻更新,输入框响应迅速。 deferredQuery会滞后更新,且如果又有新的输入,旧的滞后更新会被取消。- 适合与
useMemo配合,让大数据量列表的渲染不阻塞用户交互。
简单理解:在非紧急渲染时使用延迟值,其他时候返回旧值,避免性能问题。
11. React.memo 的参数列表是什么?
function React.memo<Props>(
Component: React.ComponentType<Props>,
arePropsEqual?: (prevProps: Props, nextProps: Props) => boolean
): React.MemoExoticComponent<React.ComponentType<Props>>
- 第一个参数:要包裹的组件。
- 第二个参数(可选):自定义比较函数,默认使用
Object.is逐 prop 比较。
React.memo 只针对 props 变化做优化。如果组件内部有 useState / useContext / useReducer 等,即使 props 没变,这些状态变化仍然会触发重渲染,memo 管不了。
12. Object.is 怎么工作的?和 === 有什么不同?
Object.is 用于判断两个值是否严格相等,在大多数情况下和 === 相同,但有两个关键差异:
console.log(0 === -0); // true
console.log(Object.is(0, -0)); // false(能区分 +0 和 -0!)
console.log(NaN === NaN); // false
console.log(Object.is(NaN, NaN)); // true(NaN 等于自身!)
React.memo 的默认比较函数就是 Object.is,所以它能正确处理这些边界情况。
13. TypeScript never 类型的用途是什么?
never 表示"永远不可能出现的值"。常见场景:
- 函数抛错:
function throwError(): never { throw new Error() } - 无限循环:
function infiniteLoop(): never { while(true) {} } - 类型收窄排除分支:
switch的default分支中,利用never做穷举检查
type Shape = 'circle' | 'square' | 'triangle'
function area(shape: Shape) {
switch (shape) {
case 'circle': return ...
case 'square': return ...
default:
const _exhaustive: never = shape // 如果上面漏了某个类型,这里会报错
return _exhaustive
}
}
与 void 的区别:void 表示函数没有返回值或返回 undefined,但函数正常执行结束;never 表示函数永远不会执行结束(抛错或死循环)。
14. 元组如何获取每一项的元素类型?
type MyTuple = [string, number, boolean]
// 获取所有项的类型,变成联合类型
type TupleValues = MyTuple[number] // string | number | boolean
T[number] 是 TypeScript 中索引访问类型的常用技巧,它取出元组所有可能索引对应的类型,合并为联合类型。
15. 工程化中图片要如何处理?
主要从三个维度优化:
① 压缩与格式转换
将大图压缩,优先使用 WebP 格式(同等质量下体积比 JPEG 小 25-35%)。
② 懒加载
<img src="image.jpg" loading="lazy" alt="延迟加载图片" />
loading="lazy" 是浏览器原生支持的懒加载,不需要引入任何第三方库。
③ CDN 加速
静态资源走 CDN,降低服务器压力,利用边缘节点加速用户访问。
进阶方案:基于 <picture> 的自动格式选择
<picture>
<source media="(min-width: 1200px)" srcset="large-image.webp" type="image/webp">
<source media="(min-width: 1200px)" srcset="large-image.jpg" type="image/jpeg">
<source srcset="small-image.webp" type="image/webp">
<source srcset="small-image.jpg" type="image/jpeg">
<img src="small-image.jpg" alt="响应式图片" width="300" height="200">
</picture>
浏览器从前往后匹配,优先加载 WebP(如果支持),否则回退到 JPEG,最后由 <img> 兜底。这是一种标准的"图片格式自动选择 + 优雅降级"方案。
16. JavaScript 中的计时器能精确计时吗?
答案:不能,且永远无法做到"精确"。
原因有三层:
硬件层面:计算机没有原子钟。主板上有一个石英晶振,通电后产生脉冲信号,CPU 靠这些脉冲有节奏地工作。但石英晶振会受温度、压力影响,存在漂移。
持久计时:关机后靠主板上由纽扣电池供电的 RTC(Real-time Clock)芯片 维持时间。
校准机制:计算机会通过网络定期访问 NTP(Network Time Protocol)时间服务器 来同步校准。
最重要的是事件循环的制约:JavaScript 是单线程的,计时器的回调函数只能在主线程空闲时运行。即使你设了 setTimeout(fn, 0),如果主线程被同步任务阻塞,回调也会被推迟:
setTimeout(() => console.log('延迟'), 0)
while(true) {} // 主线程被阻塞,计时器永远不会执行
所以实际场景中:
setTimeout(fn, 1000)表示"至少 1000ms 后执行",而不是"1000ms 时准时执行"。- 如果是倒计时、动画等对精度有要求的场景,应该通过 rAF(requestAnimationFrame) 配合时间差来计算,而不是依赖计时器的回调次数来推算。
以上 16 道题涵盖了移动端适配、CSS 新特性、框架(Vue/React)细节、TypeScript 类型体操和工程化实践。每个知识点背后都有值得深挖的细节,面试时能展开讲到这个深度,基本就能过关了。
后续会继续整理第 2 期,覆盖网络、构建工具和性能优化等方向。