返回文章列表

前端面试实战(第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.jstheme.extend 中配置:

theme: {
  extend: {
    fontSize: {
      '1r': ['1rem', '1.5'],  // text-1r === 1rem 字号
      '2r': ['2rem', '1.5'],  // text-2r === 2rem 字号
    }
  }
}

数组第一个值是字号,第二个值是行高。配置后即可在模板中使用 text-1rtext-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>

以前需要手动声明 propsemits,然后在 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 表示"永远不可能出现的值"。常见场景:

  1. 函数抛错function throwError(): never { throw new Error() }
  2. 无限循环function infiniteLoop(): never { while(true) {} }
  3. 类型收窄排除分支switchdefault 分支中,利用 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 期,覆盖网络、构建工具和性能优化等方向。