Skip to content

前端工程师完整培养方案 · 参考答案 ​

配套《前端工程师完整培养方案》使用。用法:先自己做,再对照;答案给到"达标线"层面,不是唯一正确解。标注 ★ 的是导师抽问的追问方向。


一、原理 12 问参考答案(六域各 2 问) ​

语言域 ​

Q1:事件循环中宏任务与微任务的执行顺序?浏览器渲染发生在哪个环节?

达标回答:同步代码执行完 → 清空全部微任务(Promise.then、queueMicrotask、MutationObserver)→ 取一个宏任务(setTimeout、I/O、事件回调)→ 执行 → 再清微任务 → 渲染检查点(requestAnimationFrame 回调在本次渲染前执行)→ 如此循环。

★ 追问三层:①为什么 await 后面的代码是微任务?(等价于 then 回调)②requestAnimationFrame 和 setTimeout(fn, 0) 谁先?(一轮循环里 rAF 在渲染前、属于帧回调,不与宏队列同序)③ SSR/Node 环境里渲染环节去哪了?(不存在,循环只到宏微任务层,process.nextTick 优先级高于 Promise)。

Q2:闭包为什么会导致内存泄漏?怎么定位?

达标回答:闭包让内层函数持有外层作用域引用,外层函数执行完后其变量无法回收;若闭包被全局/长生命周期对象(定时器、事件监听、缓存 Map)持有,整条引用链都不可回收。

★ 定位链路:Performance 录制确认存在增长 → Memory 拍 heap snapshot(多次对比)→ 看 retained size 排序 → 沿保留树找到引用源头 → 修复(removeEventListener、clearInterval、WeakMap/WeakRef)。

TS 域 ​

Q3:any、unknown、never 的区别与工程取舍?

达标回答:any 放弃检查且会沿赋值链传染;unknown 是"安全的 any"——可以接收任何值但使用前必须收窄(typeof/instanceof/类型守卫);never 是空类型,表示"不可能",用于穷举检查(switch default 赋值给 never)和函数永不返回的标注。

★ 工程取舍:边界输入用 unknown + 收窄,禁止 any 出现在业务代码;never 配合穷举检查是重构期"漏改分支会编译报错"的利器。追问:never 和 void 区别?(void 是"可能有值但不用",never 是"不可能有值")。

Q4:TS 的类型擦除是什么?为什么装饰器元数据要额外插件?

达标回答:TS 编译后所有类型标注都被删除,运行时不存在类型信息。所以"运行时校验"(zod)必须独立于类型层。emitDecoratorMetadata 需要插件是因为它要在编译期把"被擦除的类型"重新以元数据形式发射到运行时(reflect-metadata),这是超出标准类型系统的行为。

★ 追问:那 satisfies 解决什么?(在不改变推导类型的前提下校验字面量,兼得"窄化推导"和"约束检查")。

浏览器域 ​

Q5:渲染流水线中"重排(reflow)"和"重绘(repaint)"的区别?怎么减少?

达标回答:重排改变几何信息(宽高、位置),触发布局→绘制→合成的完整链路;重绘只改外观(颜色、阴影),跳过布局;两者之后都进合成。减少手段:批量改 style 或用 class、transform/opacity 走合成层(不触发重排重绘)、读写分离(避免布局抖动 layout thrashing)、content-visibility / contain 隔离。

★ 追问:为什么 transform 便宜?(合成器线程处理,不回主线程;对应 will-change 的使用与滥用)。

Q6:页面白屏,给出系统的排查路径。

达标回答(路径完整即可):

  1. 先分场景:全量白屏 / 部分白屏 / 偶发白屏,是否与发版相关;
  2. 控制台与监控:JS 报错、资源 404/超时、CSP 拦截;
  3. 网络层:HTML 是否 200、关键资源加载时序、CDN 节点、缓存策略错误;
  4. 渲染层:根节点未挂载(JS 未执行/执行失败)、路由匹配失败、鉴权重定向死循环;
  5. 环境差异:浏览器版本、灰度流量、Cookie/本地存储兼容;
  6. 复盘:补监控告警(JS 错误率、资源成功率、白屏检测指标)。

网络域 ​

Q7:HTTPS 握手全流程?为什么混合加密?

达标回答:TCP 握手 → ClientHello(随机数 + 支持的密码套件)→ ServerHello + 证书(含公钥)→ 客户端验证证书链与域名 → 双方用证书公钥交换预主密钥(TLS1.3 改为 DH 密钥协商,1-RTT)→ 派生对称会话密钥 → 之后全部对称加密通信。混合加密的原因:非对称安全但慢,对称快但分发密钥难,所以"非对称换密钥、对称传数据"。

★ 追问:中间人攻击卡在哪一环?(证书校验,CA 信任链与域名匹配;HSTS 强制 HTTPS)。

Q8:强缓存与协商缓存的区别与组合用法?

达标回答:强缓存由浏览器本地判断(Cache-Control: max-age / Expires),不发请求;协商缓存发请求验证(ETag / Last-Modified),未变更返回 304。组合用法:HTML 用协商缓存(no-cache),带 hash 的静态资源用一年强缓存——版本变化即 URL 变化,天然失效。

★ 追问:ETag 和 Last-Modified 谁更准?(ETag 能感知内容级变化,Last-Modified 只到秒且受文件时间干扰)。

框架域 ​

Q9:Fiber 架构下 setState 为什么是"异步"的?

达标回答:state 更新不是立即赋值,而是生成一次更新(update)挂到 Fiber 链表上,由调度器按优先级在渲染阶段批量处理——所以"读不到最新值"。并发渲染(并发特性)下渲染可以被中断、恢复、丢弃,配合 useTransition/startTransition 把更新标记为可中断的低优先级。

★ 追问:怎么立刻拿到最新值?(函数式更新 setX(x => x + 1);或在 useEffect 里读)。

Q10:Hooks 为什么不能放在条件语句里?

达标回答:Hooks 的状态存在 Fiber 节点的链表里,按调用顺序索引。条件调用会改变链表顺序,下次渲染的 hooks 对不上号,状态错位。依赖数组同理是"顺序对齐"机制——React 只做浅比较决定是否重执行。

★ 追问:闭包陷阱的本质?(effect 捕获的是当次渲染的变量快照;解法:依赖数组写全、函数式更新、useRef 存最新值)。

工程域 ​

Q11:Monorepo 依赖提升(hoisting)导致什么问题?幽灵依赖怎么治?

达标回答:包管理器把子包依赖提升到根 node_modules,导致代码能 import 到"自己没声明过的依赖"(幽灵依赖)——换环境/换管理器后报错;版本冲突也可能被错误提升。治理:pnpm 的符号链接 + 按包隔离结构(默认不提升)、workspace: 协议声明包间依赖、CI 里加"幽灵依赖检测"。

Q12:为什么有的库摇不动(tree-shaking)?

达标回答:tree-shaking 依赖 ESM 的静态结构(import/export 可分析)。CommonJS 动态 require 无法静态分析;包的 package.json 缺 sideEffects: false 或有未标注的副作用文件会被保守保留;babel 转译回 CJS 也会破坏。修复:优先 ESM 包、正确标注 sideEffects、避免 barrel file 全量导出。


二、月 1 JS 任务集参考答案(核心 4 题) ​

1. 手写防抖与节流 ​

js
// 防抖:停止触发 n 毫秒后才执行(最后一次有效)
function debounce(fn, wait) {
  let timer = null;
  return function (...args) {
    clearTimeout(timer);
    timer = setTimeout(() => fn.apply(this, args), wait);
  };
}
// 节流:n 毫秒内最多执行一次(第一次有效)
function throttle(fn, interval) {
  let last = 0;
  return function (...args) {
    const now = Date.now();
    if (now - last >= interval) {
      last = now;
      fn.apply(this, args);
    }
  };
}

自测点:this 和参数要正确传递(用 apply(this, args));追问版:支持"第一次立即执行 + 停止后补一次"的组合版。

2. 深拷贝(处理循环引用) ​

js
function deepClone(obj, map = new WeakMap()) {
  if (obj === null || typeof obj !== 'object') return obj;
  if (obj instanceof Date) return new Date(obj);
  if (obj instanceof RegExp) return new RegExp(obj);
  if (map.has(obj)) return map.get(obj);          // 循环引用:直接返回缓存
  const clone = Array.isArray(obj) ? [] : {};
  map.set(obj, clone);
  for (const key of Reflect.ownKeys(obj)) {       // 含 Symbol 键
    clone[key] = deepClone(obj[key], map);
  }
  return clone;
}

自测点:循环引用不爆栈(WeakMap 缓存);追问版:函数/Map/Set 怎么处理?JSON.parse(JSON.stringify) 丢了什么?(undefined、函数、Symbol、Date 变字符串)。

3. Promise 并发控制(最多同时 3 个) ​

js
function limitConcurrency(tasks, limit) {
  return new Promise((resolve) => {
    const results = [];
    let index = 0, done = 0;
    const next = () => {
      if (done === tasks.length) return resolve(results);
      if (index >= tasks.length) return;
      const i = index++;
      tasks[i]().then((res) => {
        results[i] = res;
        done++;
        next();
      });
    };
    for (let i = 0; i < Math.min(limit, tasks.length); i++) next();
  });
}

自测点:结果按原顺序(按索引存);失败场景——一个失败要不要中断?给两种策略(fail-fast 用 reject;全量收集用 Promise.allSettled 思路)。

4. 事件循环输出题 ​

js
console.log('A');
setTimeout(() => console.log('B'), 0);
Promise.resolve().then(() => console.log('C'));
(async () => { await console.log('D'); console.log('E'); })();
console.log('F');
// 输出:A D F C E B

解析:同步 A → D(同步执行,await 的值非 thenable 不产生等待)→ F → 微任务 C →(async 函数体)E → 宏任务 B。变体题:把 setTimeout 嵌套两层考察每层宏任务间都清微任务。


三、月 2 TS 任务集参考答案 ​

1. 标注取舍判断(示例) ​

ts
const [user, setUser] = useState<User | null>(null);   // 必须标:可能为 null,别让 User 撒谎
const [count, setCount] = useState(0);                 // 不用标:初始值可推导
const raw: unknown = JSON.parse(text);                 // 必须标:外部输入,unknown 而非 any
function getId(u: User) { return u.id; }               // 必须标:跨边界入参
function getLen(s: string) { return s.length; }        // 返回值不用标:可推导

2. 布尔堆重构 ​

ts
// 重构前:3 个布尔,8 种组合里 5 种非法
interface State { loading: boolean; error: boolean; empty: boolean; }
// 重构后:互斥状态显式建模
type State =
  | { status: 'loading' }
  | { status: 'error'; message: string }
  | { status: 'empty' }
  | { status: 'success'; data: User[] };
// 验收:success 分支之外访问不到 data;新增状态时 switch 缺分支会编译报错(配合 never 穷举)

3. 泛型 Hook ​

ts
function useList<T extends { id: string }>(fetcher: () => Promise<T[]>) {
  const [items, setItems] = useState<T[]>([]);
  const [state, setState] = useState<{ status: 'loading' | 'done' | 'error' }>({ status: 'loading' });
  // ...加载后 setItems(data)
  return { items, state, remove: (id: string) => setItems((xs) => xs.filter((x) => x.id !== id)) };
}

自测点:extends { id: string } 同时表达约束和意图;调用方传入类型后 items 自动获得元素类型。

4. type-challenges 代表题 ​

ts
// Easy Pick:不使用内置 Pick 实现
type MyPick<T, K extends keyof T> = { [P in K]: T[P] };
// Easy Readonly
type MyReadonly<T> = { readonly [P in keyof T]: T[P] };
// Medium GetReturnType
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

四、月 3 浏览器/网络任务参考答案 ​

1. 白屏排查演练 ​

按 Q6 路径实操:故意注入一个资源 404 和一个路由死循环,用 DevTools 定位两者;产出物是记录了每步证据的排查笔记(截图 + 时间线)。

2. 带缓存的 fetch 封装 ​

ts
const cache = new Map<string, { data: unknown; expires: number }>();
async function cachedFetch(url: string, ttl = 60_000): Promise<unknown> {
  const hit = cache.get(url);
  if (hit && hit.expires > Date.now()) return hit.data;        // 强缓存:命中直接返回
  const res = await fetch(url);
  if (!res.ok) throw new Error(`HTTP ${res.status}`);          // 边界:非 2xx
  const data = await res.json();
  cache.set(url, { data, expires: Date.now() + ttl });
  return data;
}

自测点:并发同 URL 请求会打几次?(会重复打——进阶版缓存 Promise 本身而非结果,即请求去重)。


五、项目验收要点参考 ​

项目一(中后台):无 any 逃生门的验收标准 ​

  • 全局搜索 : any / as any / @ts-ignore,业务代码中为 0(声明文件与第三方补丁除外,需注释说明);
  • 所有接口响应先过类型守卫或 zod,再进业务;
  • 表单校验错误用联合类型建模,不出现 error: boolean 布尔堆。

项目二(BFF):类型贯通的标准 ​

  • 前端 DTO 与 BFF 出入参共享同一份类型定义(openapi 生成或共享包),改动接口类型时前端编译期即报错;
  • 错误码枚举统一类型化;鉴权中间件有类型安全的上下文扩展。

项目三(AI Agent):任务链的验收标准 ​

  • 工具调用有 TS 接口定义(工具名、入参 Schema、返回类型),模型输出先过校验再执行;
  • 流式 UI 支持:打字机渲染、用户中断(AbortController)、失败重试、超时降级(提示"换个问法"而非白屏);
  • 能口述完整链路:用户输入 → 提示词组装 → 模型决策 → 工具执行 → 结果回填 → 流式输出。

六、结业答辩常见追问参考 ​

追问回答要点
你的架构方案里,为什么不直接上微前端?先给判断标准:团队规模与发布耦合度;独立部署、技术栈共存才值得付隔离成本;给出"什么时候该拆"的触发条件
类型系统为什么值得投入这么深?用一个真实 bug 案例:类型层拦住的编译期错误 vs 上线后的事故成本;类型是"可执行的需求文档"
AI Agent 幻觉怎么兜底?分层:输出结构化校验(Schema)→ 工具执行前权限与参数校验 → 失败降级话术;不给模型直接写库权限
你怎么保证性能优化不是"感觉变快了"?指标先行(LCP/TTI 基线)、实验室与真实用户数据结合、A/B 或灰度对比,优化前后同一口径
三层模型里视图模型会不会变成新负担?承认成本:只在"多个页面共用派生逻辑"时建视图层,页面私有的留在组件内;规则随项目演进,写进文档
模块负责期间最大的风险是什么?举例:排期风险/依赖方延期/灰度问题;讲清暴露时机(方案评审而非上线前)和应对(备选方案、回滚预案)

七、给导师的阅卷提示 ​

  • 参考答案给的是"达标线",学员答出更好的方案(哪怕是参考答案没写的)应给更高评价;
  • 重点是追问三层的抗性:第一层答对只能算 P5,第二层(为什么)达标,第三层(边界与反例)才是资深档信号;
  • 答案与学员实际项目结合的部分(自己项目的数字、自己踩的坑)权重高于背诵标准答案。