前言
大家好,我是木斯佳。
相信很多人都感受到了,在AI浪潮的席卷之下,前端领域的门槛在变高,纯粹的"增删改查"岗位正在肉眼可见地减少。曾经热闹非凡的面经分享,如今也沉寂了许多。但我们都知道,市场的潮水退去,留下的才是真正在踏实准备、努力沉淀的人。学习的需求,从未消失,只是变得更加务实和深入。
这个专栏的初衷很简单:拒绝过时的、流水线式的PDF引流贴,专注于收集和整理当下最新、最真实的前端面试资料。我会在每一份面经和八股文的基础上,尝试从面试官的角度去拆解问题背后的逻辑,而不仅仅是提供一份静态的背诵答案。无论你是校招还是社招,目标是中大厂还是新兴团队,只要是真实发生、有价值的面试经历,我都会在这个专栏里为你沉淀下来。专栏快速地址

温馨提示:市面上的面经鱼龙混杂,甄别真伪、把握时效,是我们对抗内卷最有效的武器。
面经原文内容
📍面试公司:正泰电气
🕐面试时间:近期
💻面试岗位:前端实习一面
❓面试问题:
一、八股相关
- 常用 HTML5 标签
- 标签语义化
- SEO优化怎么做
- TypeScript防御性编程会怎么去做
- interface 与 type 的区别
- 如何获取函数入参类型(二次封装):Parameters<T>
- 事件委托原理
- React 组件内 onClick 与 document.addEventListener 谁先触发?
- useEffect 与 useLayoutEffect 区别?
- React 任务调度与优先级
二、项目相关
- 瀑布流布局如何实现
- 长列表渲染优化方案
- 大图 / 大量计算如何避免阻塞主线程(Web Worker、WebAssembly)
- Zustand 与 Redux 区别
三、场景题
- 全局异常捕获与上报设计思路(框架解耦的通用 SDK 设计)
来源:牛客网eGgo3
💡 木木有话说(刷前先看)
工程化思维,是AI时代前端必须掌握的一个能力。很多面试官习惯从项目中问或者架空场景问,也是考量候选人综合解决问题的能力,这篇面经,适合已经有一定项目经验、准备冲击中大厂实习的同学。
📝 正泰电气前端实习一面·深度解析
🎯 面试整体画像
| 维度 | 特征 |
|---|---|
| 面试风格 | 基础+进阶混合型 + 工程实战型 + 场景设计型 |
| 难度评级 | ⭐⭐⭐⭐(四星,TS高级类型、React调度、异常捕获SDK较深) |
| 考察重心 | HTML语义化/SEO、TS类型编程、React事件机制、性能优化、错误监控 |
| 特殊之处 | 场景题"全局异常捕获SDK"非常实战,考察工程化设计能力 |
🔍 逐题深度解析
一、常用 HTML5 标签
回答思路:列举新增的语义化标签和功能标签。
语义化标签 :<header>、<nav>、<main>、<article>、<section>、<aside>、<footer>
多媒体标签 :<video>、<audio>、<canvas>、<svg>
表单增强 :<input type="email/number/date/range">、<datalist>、<progress>、<meter>
其他 :<details>/<summary>(折叠面板)、<dialog>(对话框)
二、标签语义化
定义 :使用具有明确含义的HTML标签,而非通用<div>/<span>。
作用:
- SEO:搜索引擎更准确识别内容结构,提升排名
- 可访问性:屏幕阅读器能更好地理解页面,辅助残障人士
- 可维护性:代码结构清晰,团队协作效率高
- 浏览器兼容:部分标签有默认样式和行为
三、SEO优化怎么做
回答思路:从标签、内容、性能、技术四个维度说明。
标签层面:
- 设置
<title>(关键词前置)、<meta name="description">(吸引点击) - 使用语义化标签(
<h1>~<h6>层级清晰) - 图片加
alt属性,链接加rel="nofollow"(外部链接)
内容层面:
- 关键词密度合理(2%-8%)
- 内链策略:相关页面互相链接
- 内容原创、定期更新
性能层面:
- 首屏加载速度(LCP < 2.5s)
- 移动端适配(响应式设计)
技术层面:
- 服务端渲染(SSR)或预渲染
- 生成
sitemap.xml和robots.txt - 使用结构化数据(JSON-LD)
html
<!-- 结构化数据示例 -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Article",
"headline": "文章标题",
"description": "文章描述"
}
</script>
四、TypeScript防御性编程
回答思路:防御性编程是"假设最坏情况,提前防范错误"。
TypeScript中的实践:
- 严格模式 :
strict: true,启用所有严格类型检查 - 使用
unknown替代any:强制类型守卫后才使用 - 可选链(
?.)和空值合并(??):安全访问嵌套属性 - 类型守卫 :
is关键字、instanceof、typeof - 联合类型和穷尽性检查 :
never类型确保所有分支处理
typescript
// 防御性函数设计
function processInput(input: unknown): string {
// 类型守卫
if (typeof input !== 'string') {
throw new Error('输入必须是字符串')
}
if (input.length === 0) {
return '默认值'
}
return input.trim()
}
// 穷尽性检查
type Status = 'pending' | 'success' | 'error'
function handleStatus(status: Status) {
switch (status) {
case 'pending': return '加载中'
case 'success': return '成功'
case 'error': return '失败'
default: {
const _exhaustiveCheck: never = status
throw new Error(`未处理的状态: ${_exhaustiveCheck}`)
}
}
}
五、interface 与 type 的区别
| 维度 | interface | type |
|---|---|---|
| 声明合并 | ✅ 可以(同名自动合并) | ❌ 不可以(会报错) |
| 扩展方式 | extends |
&(交叉类型) |
| 实现类 | ✅ 可以用implements |
❌ 不支持 |
| 声明类型 | 对象、函数、类 | 所有类型(联合、元组、原始类型) |
| 计算属性 | ❌ 不支持 | ✅ 支持(`type Keys = 'a' |
typescript
// 声明合并
interface User { name: string }
interface User { age: number } // 合并为 { name: string; age: number }
// type适用场景
type ID = string | number // 联合类型
type Point = [number, number] // 元组
type Keys = 'name' | 'age' // 字面量联合
建议 :优先使用interface(可扩展、性能好),需要联合类型或元组时用type。
六、如何获取函数入参类型:Parameters<T>
回答思路 :Parameters<T>是TypeScript内置的工具类型,用于提取函数类型的参数类型。
typescript
// 定义
type Parameters<T extends (...args: any) => any> =
T extends (...args: infer P) => any ? P : never
// 使用示例
function greet(name: string, age: number): void {
console.log(`${name}, ${age}`)
}
type GreetParams = Parameters<typeof greet> // [string, number]
// 二次封装场景:包装函数,保持参数类型一致
function wrapFunction<T extends (...args: any[]) => any>(fn: T) {
return function(...args: Parameters<T>): ReturnType<T> {
console.log('调用前', args)
const result = fn(...args)
console.log('调用后', result)
return result
}
}
const wrappedGreet = wrapFunction(greet)
wrappedGreet('Tom', 18) // 类型安全
七、事件委托原理
定义:利用事件冒泡机制,将子元素的事件监听委托给父元素统一处理。
原理:
- 事件触发后,从目标元素向上冒泡到父元素
- 父元素监听器通过
event.target判断实际触发元素 - 根据
target匹配选择器,执行对应回调
优点:
- 减少内存占用(少绑定监听器)
- 动态添加的子元素无需重新绑定
javascript
// 事件委托示例
document.getElementById('list').addEventListener('click', (e) => {
if (e.target.matches('li.item')) {
console.log('点击了', e.target.textContent)
}
})
八、React组件内onClick与document.addEventListener谁先触发
答案 :document.addEventListener先触发(在冒泡阶段)。
原因:
- React的合成事件是在
document(React 16)或root节点(React 17+)上通过事件委托实现的 - 原生事件(
document.addEventListener)在React合成事件之前执行(冒泡阶段) - 执行顺序:原生捕获 → 目标阶段 → 原生冒泡 → React合成事件冒泡
javascript
// 验证示例
document.addEventListener('click', () => console.log('原生事件'))
function App() {
return <button onClick={() => console.log('React合成事件')}>点击</button>
}
// 点击输出:原生事件 → React合成事件
注意 :React 17+事件委托到root节点,但原生事件仍然先于合成事件。
九、useEffect 与 useLayoutEffect 区别
| 维度 | useEffect | useLayoutEffect |
|---|---|---|
| 执行时机 | 浏览器绘制后(异步) | 浏览器绘制前(同步) |
| 阻塞渲染 | 否 | 是 |
| 使用场景 | 多数副作用(数据获取、订阅、日志) | 需要测量DOM、同步修改样式 |
| SSR支持 | 完全支持 | 有警告(需跳过) |
javascript
// useLayoutEffect典型场景:测量DOM
useLayoutEffect(() => {
const height = divRef.current.offsetHeight
setHeight(height) // 在绘制前更新,避免闪烁
}, [])
// useEffect:数据获取
useEffect(() => {
fetchData().then(setData)
}, [])
原则 :优先使用useEffect,只有当需要在绘制前同步操作 (如测量DOM、防止闪烁)时才用useLayoutEffect。
十、React任务调度与优先级
回答思路:React的调度器(Scheduler)实现了基于优先级的任务调度。
优先级分类(从高到低):
- Immediate:同步任务,立即执行(如用户输入)
- UserBlocking:用户交互(点击、按键)
- Normal:普通更新(setState)
- Low:低优先级(数据预加载)
- Idle:空闲任务(日志上报)
调度机制:
- 任务根据优先级进入不同队列
- 每帧(16.6ms)执行任务,剩余时间用
requestIdleCallback执行低优先级任务 - 高优先级任务可中断低优先级任务
javascript
// 简化理解
function workLoop(deadline) {
while (有任务 && deadline.timeRemaining() > 0) {
执行任务()
}
if (有任务) requestIdleCallback(workLoop)
}
十一、瀑布流布局实现
回答思路:多列布局,每项高度不同,依次放入最短列。
实现方案:
- CSS Multi-column :
column-count: 3,简单但无法精确控制顺序 - 绝对定位 + JS:计算每列高度,动态定位每个元素
javascript
// JS瀑布流
function waterfall(container, items, colCount = 3) {
const colHeights = new Array(colCount).fill(0)
items.forEach(item => {
// 找到高度最小的列
const minCol = colHeights.indexOf(Math.min(...colHeights))
item.style.position = 'absolute'
item.style.left = `${minCol * (item.offsetWidth + 10)}px`
item.style.top = `${colHeights[minCol]}px`
colHeights[minCol] += item.offsetHeight + 10
})
container.style.height = `${Math.max(...colHeights)}px`
}
优化 :使用IntersectionObserver实现图片懒加载,避免一次性加载所有图片。
十二、长列表渲染优化方案
回答思路:参考之前面经。
核心方案:
- 虚拟滚动:只渲染可视区域(react-window、vue-virtual-scroller)
- 分页/无限滚动:滚动触底加载更多
- 时间分片 :
requestIdleCallback分批渲染 - 复用DOM :
React.memo、v-memo避免重渲染
十三、大图/大量计算如何避免阻塞主线程
回答思路:将耗时任务移到主线程之外。
方案:
| 方案 | 适用场景 | 原理 |
|---|---|---|
| Web Worker | 大量计算(数据处理、图像处理) | 独立线程,通过postMessage通信 |
| OffscreenCanvas | 图像处理 | 在Worker中操作Canvas |
| WebAssembly | 高性能计算(视频/音频编解码) | 接近原生的执行速度 |
| 任务分片 | 需要与UI交替执行 | setTimeout/requestIdleCallback拆分 |
javascript
// Web Worker示例
// worker.js
self.onmessage = (e) => {
const result = heavyCalculation(e.data)
self.postMessage(result)
}
// 主线程
const worker = new Worker('worker.js')
worker.postMessage(largeData)
worker.onmessage = (e) => setResult(e.data)
十四、Zustand 与 Redux 区别
| 维度 | Redux | Zustand |
|---|---|---|
| 学习曲线 | 陡峭(action、reducer、store) | 平缓(create函数) |
| 代码量 | 多(需定义action type、reducer) | 少(直接修改state) |
| 中间件 | 丰富(redux-thunk、redux-saga) | 简单(subscribe) |
| 性能 | 需手动优化(reselect) | 自动优化(selector) |
| 体积 | 较大 | 小(~1kb) |
| DevTools | 完善 | 支持(需配置) |
javascript
// Zustand示例
import { create } from 'zustand'
const useStore = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 }))
}))
// Redux示例
const counterSlice = createSlice({
name: 'counter',
initialState: { count: 0 },
reducers: { increment: (state) => { state.count++ } }
})
选择建议:中小型项目用Zustand,大型项目/团队标准化用Redux Toolkit。
十五、全局异常捕获与上报SDK设计
回答思路:设计一个框架无关的错误监控SDK。
核心功能:
- 异常捕获 :
window.onerror、unhandledrejection、try/catch包装 - 数据聚合:收集错误信息(消息、堆栈、URL、用户行为、时间)
- 上报策略 :批量上报、
sendBeacon、避免影响主线程 - 去重:相同错误在短时间内只上报一次
- 上下文扩展:支持自定义标签(用户ID、版本号)
javascript
class ErrorMonitor {
constructor(options = {}) {
this.appId = options.appId
this.reportUrl = options.reportUrl
this.queue = []
this.init()
}
init() {
// 捕获JS错误
window.onerror = (message, source, lineno, colno, error) => {
this.capture('js_error', { message, source, lineno, colno, stack: error?.stack })
}
// 捕获未处理的Promise拒绝
window.addEventListener('unhandledrejection', (event) => {
this.capture('promise_error', { reason: event.reason?.stack || event.reason })
})
// 监听页面关闭,发送剩余队列
window.addEventListener('beforeunload', () => this.flush())
}
capture(type, data) {
this.queue.push({
type,
data,
timestamp: Date.now(),
url: location.href,
userAgent: navigator.userAgent,
appId: this.appId
})
// 批量上报:队列满或定时发送
if (this.queue.length >= 10) this.flush()
if (!this.timer) this.timer = setTimeout(() => this.flush(), 3000)
}
flush() {
if (this.queue.length === 0) return
const data = [...this.queue]
this.queue = []
// 使用sendBeacon保证页面关闭时也能发送
navigator.sendBeacon(this.reportUrl, JSON.stringify(data))
if (this.timer) clearTimeout(this.timer)
this.timer = null
}
// 手动上报
report(error, extra = {}) {
this.capture('custom_error', { message: error.message, stack: error.stack, ...extra })
}
}
// 框架集成示例(Vue)
app.config.errorHandler = (err, vm, info) => {
monitor.capture('vue_error', { message: err.message, stack: err.stack, info })
}
设计要点:
- 解耦:不依赖任何框架,提供插件机制(Vue/React集成)
- 可配置:采样率、上报地址、自定义字段
- 性能:批量上报、异步发送、避免阻塞
- 隐私:脱敏处理(不上报敏感信息)
📚 知识点速查表
| 知识点 | 核心要点 |
|---|---|
| HTML5语义化 | header/nav/main/article/section/footer,SEO+可访问性 |
| SEO优化 | 标题/描述、语义化标签、性能、sitemap、结构化数据 |
| TS防御性编程 | 严格模式、unknown替代any、可选链、穷尽性检查 |
| interface vs type | interface可合并、可扩展;type适用联合/元组 |
| Parameters | 提取函数参数类型,用于二次封装 |
| 事件委托 | 利用冒泡,父元素监听,减少内存 |
| React事件顺序 | 原生事件先于合成事件 |
| useEffect vs useLayoutEffect | 绘制后/前,大多数用useEffect |
| React调度 | 优先级队列,高优先级可中断低优先级 |
| 瀑布流 | 最短列动态定位,CSS多列或JS计算 |
| 长列表优化 | 虚拟滚动、分页、时间分片 |
| Web Worker | 独立线程处理耗时任务 |
| Zustand vs Redux | Zustand轻量,Redux规范适合大型项目 |
| 异常监控SDK | 捕获error/unhandledrejection,批量上报,框架解耦 |
📌 最后一句:
正泰电气这场实习一面,是一份"进阶版"的基础面经。从TS高级类型、React调度机制,到瀑布流、Web Worker,再到异常监控SDK设计,难度层层递进。面试官显然不满足于"会不会用",而是考察是否理解原理、能否设计解决方案 。能从容应对这套题,说明你不仅会写业务代码,更具备工程化思维和系统设计能力。这样的能力,正是大厂实习面试中拉开差距的关键。