运气比较好,拿到了 2026/01/20 小米前端实习一面的机会🥹。
这场面试整体风格很“基础但不简单”:表面上是 React / TS / 网络对话,真正的杀招是 JS 作用域 + 闭包 + 异步输出题,以及 React 闭包捕获旧值这类“你写过,但你不一定解释得清”的点。
我把问题按主题拆开,一题一题讲:结论是什么、为什么是这个结论、常见坑在哪、面试官会怎么追问、我下次怎么准备。希望你看完能:
- 解决 80% 输出题套路 ✅
- 不再被 React 闭包坑到 ✅
- TS 的 unknown/any 说得专业 ✅
- SSE / fetch stream / WebSocket 选型有理有据 ✅
(如有不严谨欢迎评论区补充纠错~我也在学习中🙏)
速通地图(强收藏目录)
- A. 项目表达:自我介绍怎么“像实习生但不菜” + 组件库到底是用还是封装
- B. React 状态管理选型:客户端状态 vs 服务端状态,别把 React Query 当 Redux
- C. SSE/流式对话:EventSource vs fetch stream vs WebSocket,什么时候该用谁
- D. TS any vs unknown:unknown 是“更安全的 any”,类型守卫怎么说清楚
- E. JS 输出题 5 连击:作用域、提升、闭包、事件循环,一条链路打通
- F. React 点击输出题:为什么 setTimeout 打印旧 count?3 种修复方式
- G. 算法双指针:有序数组平方排序,40 秒口述稿直接背
- H. 反问模板:10 个能显得你很会学习的反问
逐题拆解(结论 + 为什么 + 更深一层)
1. 自我介绍
【题目原文】自我介绍
【面试官想考什么】你是否能 用 3-5 分钟把自己“定位”清楚:会什么、做过什么、适合什么岗位
【一句话先给结论】自我介绍不是“简历朗读”,而是“岗位匹配说明书”
【为什么是这样】
面试官其实在做三件事:
1)快速建模:你是什么类型候选人(基础强?项目多?学习快?)
2)挖点追问:你提到的每个关键词都可能被追打
3)判断沟通:能不能把复杂事情讲清楚(前端很看重)
2. 介绍一下 mcp,skills
【题目原文】介绍一下 mcp,skills
【面试官想考什么】你是否了解 大模型/Agent 工具链相关概念,以及你是否能把概念讲清楚
【一句话先给结论】面试里遇到缩写,先对齐语境: “我理解你说的 MCP 是……如果你指的是另一种含义,我也简单说一下”
【为什么是这样】
“MCP”在不同语境可能指不同东西。就你这场面试里同时问了 “skills”,很像在问:
- MCP(Model Context Protocol) :一种让模型连接外部工具/数据源的协议/规范思路(让模型“会用工具”)
- skills:可理解为“工具能力/技能模块”,比如:搜索、读写文件、调用接口、执行函数等(Agent 的能力单元)
你如果能做到:不硬背定义,而是用“它解决了什么问题”解释,会非常加分。
【把坑踩出来】
- ⚠️只背一句“是个协议”,但说不出解决什么
- ⚠️把 skills 当“软技能”(沟通、协作)导致跑偏
- ⚠️不确认语境,越讲越错
【延伸追问】
- MCP 解决了 LLM 的什么限制?
- 工具调用如何保证安全?怎么做权限隔离?
- skills 的设计怎么做幂等/重试/超时?
【面试可用话术】
“我理解你说的 MCP 更像是‘让模型标准化接入外部能力/数据源’的一种协议化思路:把工具和上下文用统一方式暴露给模型,让模型能稳定地调用。skills 可以理解成 Agent 的能力模块,比如搜索、调用接口、处理文件等。实际落地时我会关注:工具描述是否清晰、输入输出 schema 是否稳定、调用是否可追踪、以及失败重试/超时策略。”
“如果你说的 MCP 不是这个方向,也欢迎你纠正一下,我可以按你们业务场景再对齐理解。”
【我会怎么准备】
- 能讲清: “为什么需要工具调用” (模型不擅长实时数据/外部执行)
- 能举例:聊天流式输出、搜索增强、自动化任务
3. 了解 React 哪些状态管理库
【题目原文】了解 React 哪些状态管理库
【面试官想考什么】你是否能做 状态分层:本地 UI 状态、跨组件共享、全局、服务端缓存
【一句话先给结论】选型的关键不是“我知道多少库”,而是:我知道什么状态该用什么方式管理
【为什么是这样】
很多人把状态管理理解成“上 Redux”。但真实项目里状态至少分两类:
- ✅ 客户端状态(Client State) :UI 展开/输入框/主题/用户选择等
- ✅ 服务端状态(Server State) :来自接口的数据(列表、详情、分页),重点是缓存、同步、失效、重试
你用 Redux 管 server state 会很痛苦;你用 React Query 管 client state 也别扭。
【把坑踩出来】
- ⚠️把 Context 当“全局状态管理库”(Context 适合“依赖注入”,不适合高频更新)
- ⚠️把 React Query 当 Redux(它主要解决服务端状态)
- ⚠️只会列名词,不会讲“用在哪、为啥”
【延伸追问】
- Redux Toolkit 相比 Redux 解决了什么问题?
- Zustand/MobX 各自适合什么场景?
- Context 为什么会导致性能问题?怎么优化?
【面试可用话术】
“我会先分状态类型:UI 本地状态优先 useState/useReducer;跨层但不高频的依赖用 Context;复杂客户端全局(比如多模块共享、需要可预测数据流)用 Redux Toolkit;偏轻量、上手快且少样板代码可以用 Zustand;MobX 更偏响应式自动追踪依赖。服务端状态我更倾向 TanStack Query(React Query):它解决缓存、去重、重试、失效更新,比自己手写 loading/error/缓存要可靠。”
“选型我一般三步:1)这状态从哪来(本地/接口)2)谁要用(单组件/多组件/全局)3)更新频率与一致性要求(高频/可追溯/可回滚)。”
【我会怎么准备】
-
准备 2 个业务例子:
- 例 1:表格列表 + 分页(React Query)
- 例 2:购物车/草稿箱(Redux/Zustand)
4. 对话是 SSE 还是什么?是用 fetch 还是 EventSource?
【题目原文】对话是 SSE 还是什么?是用 fetch 还是 EventSource?
【面试官想考什么】你是否懂“流式响应”的底层:HTTP 长连接、流、重连、协议差异
【一句话先给结论】
- SSE(Server-Sent Events) :服务端单向推送,浏览器原生
EventSource很顺手 - fetch 流式读取:更灵活(可自定义 header/POST),但需要手动解析 stream
- WebSocket:双向通信必须用它(例如实时协作/游戏/双向心跳)
【为什么是这样】
把它想成三种“管道”:
- SSE:服务端往你这边“持续滴水”,你只负责接(单向)
- WebSocket:你和服务端各一根水管,双向随时聊(双向)
- fetch stream:你自己拿水桶一点点舀(更灵活,但得自己处理分片)
SSE vs WebSocket vs Long Polling(讲人话版)
- SSE:基于 HTTP,服务端推送 -> 客户端,适合通知、日志流、对话流式输出
- WebSocket:独立协议,双向,适合实时互动
- Long Polling:客户端不断请求,开销大,老方案兜底
EventSource(SSE 原生客户端)的特点
- ✅ 自动重连(你断网了它会自己再连)
- ✅ 简单:
new EventSource(url) - ⚠️ 只能 GET
- ⚠️ 自定义请求头限制(很多场景不方便带 token)
- ✅ 支持
Last-Event-ID做断线续传(服务端配合)
fetch stream 的思路(不写长代码,但讲清流程)
1)fetch(url, { method: 'POST', headers, body })
2)拿到 response.body(ReadableStream)
3)reader.read() 循环读 chunk
4)TextDecoder 把二进制转成字符串
5)自己按协议分片(比如按 \n\n 或 data:)
6)实时 append 到 UI
【把坑踩出来】
- ⚠️把 SSE 当 WebSocket(SSE 不是双向)
- ⚠️以为 EventSource 能随便加 header(很多浏览器/环境不支持)
- ⚠️流式解析没做“半包/粘包”处理(chunk 可能切在任意位置)
【延伸追问】
- 断线重连怎么做?如何避免重复数据?
- SSE 如何做鉴权?token 放哪?
- fetch stream 如何实现“边生成边渲染”?如何节流防抖?
【面试可用话术】
“如果是对话流式输出,我倾向 SSE 或 fetch stream。EventSource 用起来最省心(自动重连),但只能 GET 且 header 不灵活;如果需要 POST 或自定义鉴权 header,我会用 fetch + ReadableStream 手动读流。若场景需要客户端也持续发消息并保持低延迟双向通信,那必须 WebSocket。”
【我会怎么准备】
-
准备一张“选型决策”:
- 单向推送 + 简单:SSE/EventSource
- 需要 POST/header:fetch stream
- 双向实时:WebSocket
5. TS 中的 any 和 unknown 讲一讲
【题目原文】TS 中的 any 和 unknown 讲一讲
【面试官想考什么】你是否重视 类型安全底线,会不会正确“收窄”
【一句话先给结论】unknown 是“更安全的 any”:你可以接收一切,但使用前必须证明它是什么
【为什么是这样】
any:相当于对 TS 说“别管我了”,它会关闭类型检查,错误可能跑到运行时炸unknown:相当于“我现在不知道类型,但你用之前得先检查”,逼你做 类型守卫
✅ 记住一句话:
any 是放弃检查;unknown 是延迟检查。
【示例 1:unknown 必须收窄】
let x: unknown = "hello";
// x.toUpperCase(); // ❌ 不允许:unknown 不能直接用
if (typeof x === "string") {
x.toUpperCase(); // ✅ 收窄后可以用
}
【示例 2:用 in 做对象守卫】
function handle(v: unknown) {
if (typeof v === "object" && v !== null && "id" in v) {
// v 现在是带 id 的对象(仍可能需要更精确)
console.log((v as any).id);
}
}
【示例 3:自定义类型谓词(更专业)】
type User = { id: number; name: string };
function isUser(v: unknown): v is User {
return (
typeof v === "object" &&
v !== null &&
"id" in v &&
"name" in v
);
}
function printUser(v: unknown) {
if (isUser(v)) {
console.log(v.id, v.name); // ✅ 完全类型安全
}
}
【把坑踩出来】
- ⚠️用
as any一把梭:看似通过编译,实际是“骗过 TS” - ⚠️unknown 收窄只做 typeof,但对象结构没校验完整
- ⚠️把 unknown 当 any 用(直接点属性)
【延伸追问】
never是什么?什么时候出现?unknown和any在联合类型里有什么差异?- 你怎么处理后端返回的 unknown 数据?(zod/自定义校验)
【面试可用话术】
“any 会关闭类型检查,风险是把错误推迟到运行时;unknown 更安全,它允许接收任意值,但使用前必须通过 typeof/in/instanceof 或自定义类型谓词做收窄。实际开发中接口返回我更倾向 unknown + 校验/收窄,再进入业务逻辑。”
【我会怎么准备】
- 把“类型守卫四件套”背熟:typeof / in / instanceof / 自定义谓词
- 写 2 个 demo:解析接口返回 + 表单输入校验
6. 是直接用组件库的组件还是自己封装了一些别的
【题目原文】是直接用组件库的组件还是自己封装了一些别的
【面试官想考什么】你是否懂“工程化复用”:二次封装的边界、成本、规范
【一句话先给结论】组件库能用就用,但业务一定会逼你二次封装:统一样式、统一行为、统一接口
【为什么是这样】
组件库(Antd/Arco/MUI…)解决的是“通用 UI”,但业务会有:
- 统一主题/字号/间距/交互
- 统一表单校验、错误提示
- 统一权限/埋点/曝光
- 统一请求态(loading/empty/error)
二次封装的价值:把“重复劳动”变成“基础设施” 。
【把坑踩出来】
- ⚠️过度封装:每个组件都包一层,反而增加维护成本
- ⚠️封装只改样式不改接口:导致业务方仍然乱用
- ⚠️不做规范:props 乱命名,后期难维护
【延伸追问】
- 你封装过哪些组件?遇到过什么坑?
- 如何保证封装组件不会限制业务灵活性?
- 组件库升级如何避免大面积破坏?
【面试可用话术】
“通用组件我会优先用成熟组件库,保证效率和稳定性。但在业务里通常会做适度二次封装,比如把表格/表单/弹窗统一成带默认配置的组件,内置埋点、权限、请求态处理,减少每个页面重复写。封装的原则是:只封装高复用、高重复、规则明确的部分,避免为了封装而封装。”
【我会怎么准备】
-
准备 2 个你封装过的例子:
- 例:统一
Modal(内置确认 loading、关闭逻辑) - 例:统一
Table(分页、空态、列配置、请求态)
- 例:统一
JS 输出题专区(7~12)
✅ 记住一句话:输出题别急着猜,先走四步:看作用域 -> 看提升 -> 看闭包 -> 看事件循环
7. 代码输出题 1(var/let + 块级作用域)
【题目原文】
function main() {
{
var a = 1
let b = 2
}
console.log(a);
console.log(b);
}
main()
console.log(a);
【面试官想考什么】块级作用域、var/let 差异、访问边界
【一句话先给结论】
console.log(a)输出1console.log(b)报错:ReferenceError: b is not definedmain()外的console.log(a)报错(在模块/严格模式环境下)或在某些环境不可访问
【为什么是这样】(掰开揉碎)
{ ... }是 块级作用域。let b = 2:b只在块里有效,块外访问就是b is not defined。var a = 1:var没有块级作用域,它属于main的函数作用域,所以在main里块外还能访问到a。
那最后一行 console.log(a) 为什么?
a是main的局部变量,不在全局作用域。块里用 var 也只提升到main的作用域,不会“跑到全局”。所以main外访问a会报错。- 另外:如果你在浏览器非模块脚本里写
var a=...在全局,会挂到window.a;但这里的 a 在函数里,所以不行。
【把坑踩出来】
- ⚠️以为
var会穿透函数变全局(不会) - ⚠️把 “b is not defined” 当成
undefined(这是 ReferenceError,不是值) - ⚠️忽略 TDZ:
let在声明前访问也会报错(暂时性死区)
【延伸追问】
- TDZ 是什么?为什么存在?
const和let在作用域上有什么区别?- ES module 下顶层
var会不会挂到 window?(不会像传统脚本那样)
【面试可用话术】
“块级作用域里
let/const只在块内可见,块外访问会 ReferenceError;var没有块级作用域,会提升到函数作用域,所以在 main 内还能打印 1。但 a 是 main 的局部变量,函数外访问不到,所以最后会报错。”
【我会怎么准备】
- 练:同一段代码放在浏览器 script / module / Node 下跑,观察差异
- 把 TDZ 的解释练成一句话:let/const 在声明前不可用
8. 什么是块级作用域 / 全局作用域 / 函数作用域
【题目原文】什么是块级作用域 全局作用域 函数作用域
【面试官想考什么】你能不能把“作用域”讲成能落地的规则
【一句话先给结论】作用域就是:变量在哪儿能被访问
【为什么是这样】
- 全局作用域:最外层,哪里都可能访问到(谨慎使用)
- 函数作用域:函数内部(
var主要在这里生效) - 块级作用域:
{}内部(let/const生效)
✅ 记住一句话:
var:看函数let/const:看花括号{}
【把坑踩出来】
- ⚠️把“作用域”和“执行上下文”混了(相关但不是一回事)
- ⚠️以为 if/for 没有作用域(有块级作用域)
- ⚠️误以为闭包=作用域(闭包是“保留作用域的能力”)
【延伸追问】
- 什么是词法作用域(lexical scope)?
- 执行上下文包含哪些?
- 闭包为什么能记住变量?
【面试可用话术】
“作用域就是变量的可访问范围。JS 是词法作用域,代码写在哪决定变量能被谁访问。全局作用域在最外层;函数作用域由函数创建;块级作用域由
{}创建,let/const 生效。输出题本质就是看变量属于哪个作用域。”
【我会怎么准备】
- 画作用域链:从当前块 -> 函数 -> 全局
- 把每个输出题都按“从近到远找变量”解释
9. 代码输出题 2(for + var + setTimeout)
【题目原文】
for (var i = 0; i < 5; i++) {
setTimeout(() => {
console.log(i);
}, 100);
}
【面试官想考什么】闭包 + 事件循环 + var 共享变量
【一句话先给结论】100ms 后输出 5 5 5 5 5
【为什么是这样】
var i是 函数作用域(在这里等价于一个共享的 i)- for 循环很快跑完,i 最终变成 5
setTimeout回调是“之后再执行”,等回调执行时,读到的都是同一个 i(已经是 5)
这就是典型的:回调里读到的是“同一个变量”,不是“当时的值” 。
【把坑踩出来】
- ⚠️以为 setTimeout 会“记住当时的 i 值”(它记住的是变量引用)
- ⚠️把这个当成“异步导致乱序”,其实是闭包抓变量
- ⚠️不知道事件循环:回调在宏任务队列里
【延伸追问】
- 改成 let 会怎样?为什么?
- 还有哪些方式能输出 0~4?
- Promise.then 和 setTimeout 顺序怎么比较?
【至少 2 种改写让输出变 0~4】
✅ 方式 1:用 let(每次循环一个新绑定)
for (let i = 0; i < 5; i++) {
setTimeout(() => console.log(i), 100);
}
✅ 方式 2:IIFE 把 i 作为参数锁住
for (var i = 0; i < 5; i++) {
(function (j) {
setTimeout(() => console.log(j), 100);
})(i);
}
✅ 方式 3:setTimeout 第三个参数传参
for (var i = 0; i < 5; i++) {
setTimeout((j) => console.log(j), 100, i);
}
【面试可用话术】
“var 在 for 里是同一个 i,循环结束 i=5,setTimeout 回调晚执行,读到的都是 5。用 let 会为每次迭代创建新的块级绑定,或者用 IIFE/传参把当时的值保存下来。”
【我会怎么准备】
- 把“变量 vs 值”的区别练成一句话
- 记住三件套:let / IIFE / setTimeout 传参
10. 代码输出题 3(闭包参数保存值)
【题目原文】
for (var i = 0; i < 5; i++) {
function printText(temp) {
setTimeout(() => {
console.log(temp);
}, 100);
}
printText(i)
}
【面试官想考什么】你是否理解“把值当参数传入”就能避免共享变量
【一句话先给结论】输出 0 1 2 3 4
【为什么是这样】
虽然外层用的是 var i,但你每次调用 printText(i) 都把当时的 i 作为值传给了 temp。
temp 是函数参数,是一次调用一个值,回调闭包捕获的是 temp,不是共享的 i。
✅ 记住一句话:想要固定当时的值,就让它成为“参数/局部变量”,而不是共享外部变量。
【把坑踩出来】
- ⚠️认为 var 一定会输出 5(不一定,看你有没有把值保存)
- ⚠️把 temp 当成 “引用 i”(参数是值拷贝到局部绑定)
【延伸追问】
- temp 是怎么被闭包保存的?
- 如果 temp 是对象会怎样?(对象引用,可能被内部改)
- 这段改成箭头函数声明 printText 有差别吗?
【改写让它也输出 5 5 5 5 5(反向理解)】
如果你不传参,而是直接用外部 i:
for (var i = 0; i < 5; i++) {
setTimeout(() => console.log(i), 100);
}
【面试可用话术】
“这里虽然外层 i 是 var,但每次把 i 传进函数参数 temp,相当于把‘当时的值’存成了局部变量。setTimeout 回调闭包捕获的是 temp,所以会输出 0~4。”
【我会怎么准备】
- 专门练:同一个题写出 “5 5 5 5 5” 和 “0~4” 两个版本
- 用“捕获变量还是捕获参数”解释区别
11. 代码输出题 4(var temp 覆盖参数)
【题目原文】
for (var i = 0; i < 5; i++) {
function printText(temp) {
var temp = i
setTimeout(() => {
console.log(temp);
}, 100);
}
printText(i)
}
【面试官想考什么】变量声明覆盖、作用域、闭包捕获哪个变量
【一句话先给结论】输出 5 5 5 5 5
【为什么是这样】
你虽然传了 printText(i),但进函数后立刻写了:var temp = i
这等于:把参数 temp 的值覆盖掉,并且 temp 指向的是外部共享的 i 的“当前值”。
回调执行时 i 已经是 5,所以 temp 也会是 5。
更形象一点:
- 你本来把“拍照的 i”放进 temp ✅
- 你又把 temp 改成“实时监控的 i” ❌
所以最后大家看到的是同一个 5。
【把坑踩出来】
- ⚠️以为 “有 temp 参数就一定是 0~4”(你覆盖了它!)
- ⚠️不理解
var temp和参数 temp 是同一个标识符(同一作用域里声明会合并/覆盖语义)
【延伸追问】
- 为什么不用 var temp = i 直接 temp = i?区别?
- 如果改成 let temp = i 呢?
- 参数和 var 同名会怎样?
【改写让它输出 0~4】
✅ 方式 1:别覆盖参数
function printText(temp) {
setTimeout(() => console.log(temp), 100);
}
✅ 方式 2:用局部保存 i,但不要用共享 i(用传入的 temp)
function printText(temp) {
const value = temp
setTimeout(() => console.log(value), 100);
}
【面试可用话术】
“虽然传了 i,但函数里又用
var temp = i把参数覆盖,temp 变成读取共享 i 的结果;回调执行时 i=5,所以输出全是 5。”
【我会怎么准备】
- 专门背一句:参数是快照,但你可以把它改回实时变量
- 练:参数同名覆盖的坑题
12. 代码输出题 5(在回调内部声明 temp)
【题目原文】
for (var i = 0; i < 5; i++) {
function printText(temp) {
setTimeout(() => {
var temp = i
console.log(temp);
}, 100);
}
printText(i)
}
【面试官想考什么】回调作用域、var 提升、闭包读取外部 i
【一句话先给结论】输出 5 5 5 5 5
【为什么是这样】
这里关键点:var temp = i 写在 setTimeout 的回调里。
回调执行时,for 早就结束了,外部 i 已经是 5。
所以每次回调里重新声明的 temp 都会被赋值为 5,然后打印 5。
✅ 记住一句话:
你在回调里声明变量,只能拿到回调执行那一刻的外部状态。
【把坑踩出来】
- ⚠️以为 temp 是参数就会固定(但你没用参数 temp)
- ⚠️忽略“声明位置”:保存值要在同步阶段保存,而不是回调里
【延伸追问】
- 回调里
var temp会不会影响外层 temp? - 改成
temp = i(不声明)会怎样? - 如何让它输出 0~4?
【改写让它输出 0~4】
✅ 方式 1:把 i 作为参数传入回调(第三参)
for (var i = 0; i < 5; i++) {
setTimeout((j) => console.log(j), 100, i);
}
✅ 方式 2:在同步阶段先保存
for (var i = 0; i < 5; i++) {
const snapshot = i
setTimeout(() => console.log(snapshot), 100);
}
【面试可用话术】
“temp 在回调里才声明并赋值为 i,而回调执行时 i 已经变成 5,所以每次都打印 5。要输出 0~4,需要在同步阶段保存 i,比如 let、IIFE 或 setTimeout 传参。”
【我会怎么准备】
- 练“变量声明位置决定你拿到的是快照还是实时值”
- 手写 5 题输出题,形成肌肉记忆
React 输出题专区(13)
13. React 点击按钮后:为什么 render 1,但 setTimeout 0?
【题目原文】
export default function App() {
const [count, setCount] = useState(0)
console.log('render', count)
return (
<div className="App">
<h1>{count}</h1>
<button onClick={() => {
setCount(count + 1)
setTimeout(() => console.log('setTimeout', count), 1000)
}}>
+1
</button>
</div>
)
}
// 点击后:render 1 -> setTimeout 0
【面试官想考什么】React 渲染模型 + 闭包捕获旧值 + 异步回调
【一句话先给结论】setTimeout 打印的是 当次点击时闭包里捕获的旧 count,不是最新 state
【为什么是这样】(用“画面感”讲清楚)
把 React 的一次渲染想成“拍一张照片”:
- 每次 render,组件函数会执行一遍,产生一套新的变量(包括新的
count) - 你点击按钮时,触发的是“那一张照片里”的 onClick 回调
- 回调里
setCount(count + 1)会让 React 计划下一次 render,于是你看到render 1 - 但
setTimeout(() => console.log(count))这个回调 闭包捕获的 count 仍然来自“旧照片”(0)
所以 1 秒后它打印 0。
✅ 记住一句话:
React 里函数组件每次 render 都是新作用域,事件回调/定时器会捕获当次 render 的变量快照。
【把坑踩出来】
- ⚠️以为 setState 立刻改掉当前函数里的 count(不会,它触发下一次 render)
- ⚠️把这个当成“setTimeout 异步导致旧值”,本质是“闭包捕获”
- ⚠️连续点击时更混乱(批处理、调度、并发渲染都可能影响直觉)
【延伸追问】
- 连点两次输出会怎样?为什么?
- 怎么在 setTimeout 里拿到最新 count?
- 函数式更新
setCount(c => c + 1)为啥更安全?
【至少 3 种修复/实现方式】
✅ 方案 1:函数式更新 + 用 next 变量(你要什么就算什么)
<button onClick={() => {
setCount(c => {
const next = c + 1
setTimeout(() => console.log('setTimeout next', next), 1000)
return next
})
}}>
+1
</button>
解释:函数式更新拿到的是最新的 c,你在这里计算 next 并传给 timeout,就不会被旧闭包坑。
✅ 方案 2:useRef 同步保存最新值(“小本本记账法”)
function App() {
const [count, setCount] = useState(0)
const latestRef = useRef(count)
useEffect(() => {
latestRef.current = count
}, [count])
return (
<button onClick={() => {
setCount(count + 1)
setTimeout(() => console.log('setTimeout', latestRef.current), 1000)
}}>
+1
</button>
)
}
解释:ref 不会因为 render 而丢失,latestRef.current 永远是最新。
✅ 方案 3:useEffect 监听 count 触发副作用(“状态变了再做事”)
function App() {
const [count, setCount] = useState(0)
useEffect(() => {
const id = setTimeout(() => console.log('setTimeout', count), 1000)
return () => clearTimeout(id)
}, [count])
return <button onClick={() => setCount(c => c + 1)}>+1</button>
}
解释:副作用跟着 state 走,逻辑更 React,但要注意清理/防抖。
【面试可用话术】
“函数组件每次 render 都是新的作用域。onClick 里的 count 是当次 render 的快照,setTimeout 回调闭包捕获的是旧 count。setState 会触发下一次 render,所以你会看到 render 1,但定时器仍打印 0。解决方式可以用函数式更新拿到最新值、用 ref 保存最新值,或者用 effect 在 state 变化后再触发副作用。”
【我会怎么准备】
- 把“闭包捕获旧值”讲成一句能背的
- 手写 3 种修复方式并理解优缺点
算法题专区(14)
14. 有序数组平方后排序(双指针)
【题目原文】arr = [-4, -1, 0, 3, 5],返回平方后的排序
【面试官想考什么】你能不能识别“有序 + 绝对值最大在两端”的规律,并用双指针 O(n)
【一句话先给结论】双指针:比较两端平方,把大的从结果数组尾部填
【为什么是这样】
原数组有序,但平方会打乱顺序(负数变正)。
关键观察:平方后的最大值一定来自两端(最负或最大正)。
所以我们用两个指针 l、r 从两端向中间夹:
- 比较
arr[l]^2和arr[r]^2 - 谁大就把谁放到
result[index](从后往前填) - 指针向内移动
【朴素方案(O(n log n))】
const res = arr.map(x => x * x).sort((a, b) => a - b)
缺点:排序多了一次 log n。
【双指针方案(O(n))】
const arr = [-4, -1, 0, 3, 5]
function solution(arr) {
const len = arr.length
const result = new Array(len)
let left = 0
let right = len - 1
let index = len - 1
while (left <= right) {
const l2 = arr[left] * arr[left]
const r2 = arr[right] * arr[right]
if (l2 > r2) {
result[index] = l2
left++
} else {
result[index] = r2
right--
}
index--
}
return result
}
console.log(solution(arr))
【面试 40 秒口述稿(直接背)】
“平方会破坏有序性,但最大平方一定来自最左或最右,因为绝对值最大在两端。我用双指针 left/right 比较两端平方,谁大就放到结果数组的末尾 index,然后对应指针向内移动,index--。这样一次遍历就完成,时间 O(n),额外空间 O(n)。”
【把坑踩出来】
- ⚠️忘了从后往前填(大的先放末尾)
- ⚠️边界 left<=right 写错导致漏元素
- ⚠️重复计算平方可以先存变量避免写错
【延伸追问】
- 如果要求原地呢?(通常需要额外技巧或不要求)
- 数组很大如何优化内存?
- 类似题:两数平方和、绝对值排序
【我会怎么准备】
- 练 3 题双指针:有序数组去重、两数之和、有序平方排序
反问专区(15)
15. 反问(8~12 个模板 + 为什么专业)
① 团队与业务
1)“团队主要负责哪条业务线?前端在其中的核心挑战是什么?”
- 为什么专业:你在确认业务复杂度和成长空间
2)“这条业务目前最头疼的性能/稳定性问题是什么?”
- 为什么专业:你在对齐工程能力需求
② 技术栈与工程化
3)“目前主要技术栈是 React 哪个版本?有没有在用 React Query/Redux Toolkit 之类的方案?”
- 为什么专业:你在看技术成熟度与规范
4)“你们对组件封装、代码规范、单测/CI 有哪些要求?”
- 为什么专业:你在关注工程体系
③ 成长与导师机制
5)“实习生一般会配导师吗?代码 review 的频率大概是怎样的?”
- 为什么专业:你在争取反馈闭环
6)“实习前两周通常会安排什么类型的任务?偏熟悉业务还是直接上需求?”
- 为什么专业:你在评估上手节奏
④ 评估与转正
7)“实习阶段的评估标准更看重交付、质量还是成长速度?”
- 为什么专业:你在确认努力方向
8)“如果有转正机会,通常会看哪些指标?”
- 为什么专业:你在提前对齐目标
⑤ 当前最大挑战
9)“如果我加入,最希望我先解决什么问题?我可以提前准备哪些知识?”
- 为什么专业:你表现出主动性和可培养性
10)“团队现在最缺的是哪类能力的前端?比如工程化、性能、业务建模?”
- 为什么专业:你在确认岗位画像,也能顺势展示匹配点
结尾:复盘总结
这场面试给我最大的提醒是:基础题并不“基础” 。很多问题不是让你背结论,而是要你把机制讲清楚。尤其是:
- JS 输出题:你是否能按“作用域 -> 提升 -> 闭包 -> 事件循环”推导
- React:你是否理解“每次 render 是新作用域”,闭包为什么会拿旧值
- TS:你是否把类型安全当底线(unknown + 收窄)
- 流式对话:你是否能说清 SSE / fetch stream / WebSocket 的边界
我暴露的短板(你也可以对照自查)
- 输出题推导链路还不够“稳”(容易凭感觉)
- React 副作用与闭包解释需要更“像面试官想听的那种表达”
- SSE 细节(鉴权、重连、分片解析)需要更系统
输出题必杀口诀(建议背下来)
✅ 先看作用域(变量在哪定义)
✅ 再看提升(var/function 是否被提升)
✅ 再看闭包(回调捕获变量还是捕获值)
✅ 最后看事件循环(回调什么时候执行)