Typescript 面试题

共 61 道 Typescript 面试题。答案默认折叠,便于先自行作答。 1. 如何管理和优化 tsconfig? 难度:3 · 类型:QA 题目要点 tsconfig 应被视为工程规范而非临时配置;通过分层与继承降低维护成本;类型严格性需要渐进式收紧而非一次性激进开启;合理控制编译范围以优化性能;在复杂工程中利用 tsconfig 约束依赖边界,使类型系统服务于长期可维护性。 参考答案 tsconfig 的管理与优化,本质上是在类型安全、开发体验与工程效率之间做长期平衡。它并不是一次性配置文件,而是随着项目规模、团队协作方式和构建体系不断演进的工程资产。 首先需要明确 tsconfig 在工程中的角色定位。它既是 TypeScript 编译器的输入约束,也是 IDE 类型分析与提示能力的基础配置。如果把 tsconfig 仅当作“让项目能跑起来的必要文件”,往往会在项目中后期暴露出类型失控、编译缓慢或不同环境行为不一致的问题。因此,治理 tsconfig 的第一步,是将其视为工程规范的一部分,而不是单纯的工具配置。 在实际管理中,通常会通过“分层”的方式来降低复杂度。将通用且稳定的配置抽离为基础 tsconfig,由不同运行环境或子项目通过 extends 继承,再在局部覆盖差异化选项。这种方式可以避免在多个 tsconfig 中重复维护同一组 compilerOptions,也能清晰表达哪些约束是全局共识,哪些是特定场景的取舍。例如,类型严格性、模块解析策略往往属于全局约束,而是否生成声明文件、是否开启 sourceMap 则更偏向构建阶段需求。 在优化层面,类型严格性的管理尤为关键。一次性开启所有严格选项,在历史项目中通常不可行,反而会导致大量噪音,削弱团队对类型系统的信任度。更可持续的做法,是以 strict 为目标方向,但通过逐步引入单项严格规则,让类型质量随时间提升,而不是通过一次激进配置制造阻力。tsconfig 在这里承担的是“收紧边界”的角色,而不是制造阻断。 编译性能同样是 tsconfig 优化中不可忽视的一环。随着项目体量增大,合理控制 include、exclude 范围,避免将无关文件纳入类型分析,是最直接也最有效的手段。同时,明确区分“类型检查”与“代码转译”的职责,可以避免在开发阶段承担不必要的编译成本。在一些工程中,会通过不同 tsconfig 分别服务于 IDE 类型检查与构建流程,从而兼顾体验与效率。 在 Monorepo 或组件库场景下,tsconfig 还承担着依赖边界约束的职责。通过 project references 明确包之间的依赖关系,不仅可以加速增量编译,也能在类型层面阻止不合法的跨包引用。这类配置一旦稳定下来,往往比 lint 规则更可靠,因为它直接作用于编译阶段。 长期来看,tsconfig 的优化不是“调参数”,而是通过持续审视哪些约束是必要的、哪些已经不再适用,让类型系统始终贴合真实的工程状态。当 tsconfig 能够稳定表达团队的工程共识时,它的价值才真正体现出来。 2. Boolean 和 boolean 有什么区别? 难度:3 · 类型:QA ...

Vue.js 面试题

共 123 道 Vue.js 面试题。答案默认折叠,便于先自行作答。 1. 为何setup()中直接解构props会丢失响应性? 难度:2 · 类型:QA 题目要点 setup 中直接解构 props 会丢失响应性,是因为解构会把属性值拷贝为普通变量,打断了与 props Proxy 的引用关系,导致 Vue 无法进行依赖收集。正确做法是直接使用 props.xxx,或通过 toRefs / toRef 将属性转换为保持响应性的 ref。 参考答案 在 Vue 3 中,setup() 里直接解构 props 会丢失响应性,根本原因在于:响应式是依赖引用关系实现的,而解构会打断这种引用关系。 下面从原理、示例和正确做法三个层面说明。 一、根本原因:解构会“拷贝值”,不再是响应式引用 在 setup(props) 中: props 本身是一个 浅只读的响应式对象(Proxy) Vue 的响应式系统是 基于 getter / setter(Proxy 拦截)+ 依赖收集 实现的 解构会把属性的当前值“取出来”,赋值给一个普通变量 一旦解构: const { title } = props; 此时: title 是一个 普通变量 它不再通过 props.title 访问 Vue 无法拦截 title 的读取 依赖收集链路断裂 → 响应性丢失 二、对比示例:为什么一个会更新,一个不会 ❌ 错误示例(丢失响应性) export default { props: { title: String }, setup(props) { const { title } = props; return { title }; } }; 父组件更新 title 时: ...

前端安全 面试题

共 35 道 前端安全 面试题。答案默认折叠,便于先自行作答。 1. 如果发现JWT Token泄露,如何快速响应? 难度:3 · 类型:QA 题目要点 定位:确认泄露的是单个用户 Token 还是全局密钥。 拉黑:立即将泄露的 Token jti 加入 Redis 禁用名单。 阻断:如果是密钥泄露,立即更新环境变量中的 JWT_SECRET。 通知:提醒受影响用户,并建议其修改密码(以防攻击者通过其他渠道获取了账号控制权)。 审计:检查该 Token 泄露期间产生的异常操作日志,进行回滚或补偿。 参考答案 由于 JWT 是无状态的(服务器默认不存储 Token,无法像 Session 那样轻易在服务端作废),快速响应的核心思路在于阻断该 Token 的有效性或强制变更鉴权环境。 以下是针对不同场景的快速响应方案: 1. 服务端拦截:引入“黑名单”机制(最快响应) 这是最直接的补救措施。虽然 JWT 旨在去中心化校验,但在紧急情况下,必须引入中心化检查。 操作:将泄露的 Token 唯一标识(通常是 jti 载荷)或整个 Token 字符串存入 Redis 等极速缓存中,并设置过期时间(等于该 Token 的剩余有效期)。 逻辑更新:修改后端的鉴权中间件,在校验 JWT 签名通过后,额外多一步检查:“该 Token 是否在 Redis 黑名单中?”。如果在,则拒绝请求。 优点:秒级生效,精准拦截。 2. 变更签名密钥(最彻底但代价大) 如果怀疑是大规模泄露,或者加密私钥(Secret Key)本身已暴露: 操作:立即在服务端更换用于签署 JWT 的 Secret Key。 后果:由于密钥变了,之前所有基于旧密钥生成的 Token(包括合法用户的)都会瞬间失效。 适用场景:密钥泄露或遭受系统性攻击。虽然这会导致全服用户强制下线(需重新登录),但在极端安全风险下是必要的“熔断”。 3. 强制用户重新登录:变更 User Secret 如果你的 JWT 签名逻辑中引入了用户特有的变量(例如:HMAC256(payload, base_secret + user_password_hash)): ...

场景题 面试题

共 84 道 场景题 面试题。答案默认折叠,便于先自行作答。 1. 现在大模型已经能高效生成界面代码,有人认为前端会被AI颠覆,也有人认为这只是工具革新。你更倾向哪种观点?你认为前端未来3-5年真正的机会点或转型方向是什么? 难度:3 · 类型:QA 题目要点 更倾向于“工具革新”而非“彻底颠覆”;大模型替代的是低层实现工作,而前端正在向工程化、系统设计以及 AI 应用方向演进,未来机会集中在高阶工程能力与“前端 + AI”结合的领域。 参考答案 更倾向于“工具革新”的判断,但需要补充一个前提:被替代的不是前端岗位,而是低抽象层的前端工作。大模型把“写代码”这一步极大压缩,但没有消解前端在系统中的职责。 从本质上看,前端的价值不在于把设计稿转成代码,而在于如何在浏览器这个受限运行时中构建复杂系统并保证体验、性能与稳定性。大模型擅长生成局部正确的代码,但在跨模块一致性、状态复杂性、长期可维护性等方面仍然依赖工程设计能力。 一、为什么更接近“工具革新” 大模型改变的是生产方式,而不是问题本身: 代码生成 → 自动化:组件、页面、样板代码的产出成本趋近于零 问题复杂度 → 上移:从“怎么写”转为“怎么设计、怎么约束、怎么演进” 人机协作 → 常态化:开发流程变成“设计 + 约束 + 校验 + 修正” 这和当年从 jQuery 到框架、再到工程化工具的演进类似,本质是抽象层不断上移。 二、未来 3–5 年的核心变化 可以用一句话概括:前端从“实现层”走向“系统层 + AI 协同层” 1. 前端工程能力会被放大,而不是削弱 大模型生成的代码,如果没有约束,很容易出现: 状态混乱(状态源不清晰) 性能问题(重复渲染、无效请求) 架构失控(组件边界混乱) 因此需要更强的工程能力去“约束 AI”: 设计组件规范、状态模型、数据流 统一代码风格与约束(lint / schema / contract) 构建可持续演进的架构 本质上:人负责“系统正确性”,AI 负责“实现效率” 2. “前端 + AI”的应用能力成为新分水岭 未来不是“用 AI 写代码”,而是“用前端做 AI 产品”: ...

小程序 面试题

共 15 道 小程序 面试题。答案默认折叠,便于先自行作答。 1. 小程序可以做哪些性能优化? 难度:2 · 类型:QA 题目要点 小程序性能优化围绕逻辑层与视图层分离架构展开,重点是减少 setData 频率与数据体积,控制列表节点数量,优化首包体积与启动链路,并通过分包加载、WXS 与懒加载等手段降低渲染压力。核心原则是减少跨线程通信与避免无意义重渲染,从架构层面解决性能瓶颈。 参考答案 小程序的性能优化,不能只从“前端渲染”角度看。它的运行模型和 Web 不同,存在 逻辑层(JSCore)与视图层(WebView)分离 的架构特征,核心瓶颈往往出在: 跨线程通信成本 setData 传输体积 渲染节点数量 包体与启动链路 优化必须围绕这些机制展开。 一、理解运行架构是前提 以 微信小程序 为例: 逻辑层:JS 执行环境 视图层:WebView 渲染 两层通过 JSON 序列化通信 每一次 setData: 数据序列化 线程间传输 视图层 diff 真实节点更新 所以小程序性能优化的第一原则是: 减少跨线程通信的数据量与次数。 二、控制 setData 的粒度与频率 1. 避免大对象全量更新 错误方式: this.setData({ form: newFormObject }) 如果 form 很大,每次都会整体传输。 正确方式: this.setData({ "form.username": value }) 使用路径更新,最小化数据传输。 2. 合并多次 setData 多次调用: this.setData({ a: 1 }) this.setData({ b: 2 }) 会触发多次通信。 ...

工具 面试题

共 40 道 工具 面试题。答案默认折叠,便于先自行作答。 1. 你了解 Git 的 Worktree 吗? 难度:3 · 类型:QA 题目要点 Git Worktree 允许多个工作目录共享同一个仓库对象库,从而同时检出多个分支,避免频繁 checkout 带来的上下文切换问题。其核心依赖 Git 的对象数据库与工作区分离设计,相比 clone 具有更低的磁盘与网络成本,适用于并行开发、hotfix、版本对比和 CI 场景,本质是同一仓库的多工作视图机制。 参考答案 Git Worktree 是 Git 提供的一种 在同一个仓库中同时检出多个工作目录(working directory) 的机制,本质目的是解决“一个仓库只能对应一个当前分支”的限制。 在传统 Git 使用方式下,一个仓库目录只能 checkout 一个分支。如果正在开发 feature 分支,此时需要紧急修复线上 bug,就必须经历: stash 当前修改 切换分支 修复问题 再切回原分支恢复现场 这个过程不仅低效,而且容易产生冲突或遗漏。Worktree 的出现,就是为了避免这种上下文切换成本。 一、Worktree 的核心原理 Git 仓库实际上由两部分组成: .git:对象数据库(commit、tree、blob、refs) working directory:当前检出的文件 Worktree 允许 多个 working directory 共享同一个 .git 对象库。 也就是说: commit history 只有一份 objects 只有一份 但可以有多个独立的代码目录,每个目录对应不同分支 Git 内部会在 .git/worktrees/ 下维护额外工作区的元信息,每个 worktree 都拥有自己的 HEAD、index 和 checkout 状态。 ...

工程化 面试题

共 87 道 工程化 面试题。答案默认折叠,便于先自行作答。 1. webpack 中的 Loader ,链式调用顺序会影响编译结果吗? 难度:2 · 类型:QA 题目要点 Webpack Loader 是一个按流水线执行的源码转换系统,Loader 的链式顺序直接决定源码被处理的阶段;配置顺序为从左到右,但执行顺序为从右到左,每个 Loader 都处理前一个 Loader 的输出,因此顺序错误会导致语义阶段错乱甚至编译失败;通常应遵循“靠近源码的 Loader 放右侧,靠近运行时的 Loader 放左侧”的原则,同时 Loader 还存在 pitch 与 normal 两阶段执行机制,使顺序对最终编译结果具有决定性影响。 参考答案 会,而且这是 Webpack Loader 机制中非常关键且经常被误解的一点: Loader 的链式调用顺序不仅会影响编译结果,而且很多语法是否能正确工作,完全取决于 Loader 的执行顺序。 理解这个问题的关键在于:Loader 本质是一个“源码转换流水线(transform pipeline)”。 一、Loader 本质:源码的逐步转换 在 Webpack 中 hookup 的 Loader,并不是同时运行的,它们会对模块源码进行一层一层的转换: 原始源码 → loaderA → loaderB → loaderC → 最终 JS 模块 每个 Loader: 接收上一个 Loader 的输出 返回新的源码字符串(或 AST 转换结果) 交给下一个 Loader 因此: ...

性能优化 面试题

共 52 道 性能优化 面试题。答案默认折叠,便于先自行作答。 1. 响应式开发中,如何避免窗口大小监听导致的重排抖动? 难度:2 · 类型:QA 题目要点 响应式开发中 resize 导致抖动的本质原因是窗口变化会高频触发事件,从而反复触发布局计算。常见优化方式包括对 resize 事件进行节流或防抖以降低执行频率,避免在回调中混合 DOM 读写以减少强制同步布局,利用 requestAnimationFrame 控制更新时机,以及使用 ResizeObserver、媒体查询或容器查询等浏览器能力,让布局响应更多地交给浏览器完成,从而降低重排和重绘的成本。 参考答案 在响应式开发中,如果直接监听 resize 事件并在回调中执行布局计算或 DOM 操作,很容易引发频繁的 重排(reflow)和重绘(repaint)。原因在于浏览器在拖动窗口尺寸时会持续触发 resize 事件,如果每次触发都进行样式读取和 DOM 更新,就会造成布局计算不断被打断,从而出现明显的抖动或性能下降。 实际工程中通常会从 事件触发频率控制、布局计算方式优化、浏览器能力利用 三个层面进行处理。 首先是对 resize 事件进行频率控制。浏览器在拖拽窗口时可能每秒触发几十到上百次 resize,如果每次都重新计算布局,主线程压力会非常大。常见做法是通过 节流(throttle)或防抖(debounce) 将计算频率降低。例如在节流策略下,每隔一段时间才执行一次布局更新,从而避免在连续拖动过程中频繁触发布局计算。对于布局实时性要求较低的场景,防抖也比较适合,即在窗口停止变化后再执行计算。 其次是避免在 resize 回调中混合 DOM 读写操作。浏览器在读取布局信息(例如 offsetWidth、getBoundingClientRect)时,如果之前存在未提交的样式修改,会强制触发布局计算,这被称为 强制同步布局(Forced Reflow)。因此更好的方式是将读取和写入操作进行分离,例如先批量读取尺寸,再统一修改样式,或者借助 requestAnimationFrame 将 DOM 更新安排到浏览器下一帧执行,避免频繁打断渲染流程。 另外一个更现代的方案是尽量减少对 window.resize 的依赖,而是使用 元素级尺寸监听机制。浏览器已经提供了 ResizeObserver API,可以直接监听某个容器尺寸变化。当布局响应依赖的是容器宽度而不是窗口宽度时,这种方式更加精确,同时也减少无关 resize 触发带来的性能损耗。 在 CSS 层面也可以减少 JavaScript 的参与。例如使用 媒体查询(media query) 或 容器查询(container query) 来处理布局变化,让浏览器在样式计算阶段直接完成响应式适配。CSS 驱动的响应式布局通常比 JavaScript 监听 resize 更高效,因为浏览器可以对样式计算进行统一调度。 ...

算法 面试题

共 22 道 算法 面试题。答案默认折叠,便于先自行作答。 1. 不重复最大子串 难度:2 · 类型:QA 给定一个字符串,请实现一个函数来找到其中的不重复最大子串。例如,对于字符串"abcabcbb",不重复最大子串是"abc",长度为3。 请写出实现该功能的代码,并说明其时间复杂度。 考虑到性能优化,你认为还有哪些改进空间?请提出优化思路并实现优化后的代码。 题目要点 滑动窗口 是解决最长不重复子串问题的核心思想。 Set 方法简单直观,但每遇到重复字符可能多次移动左指针。 Map 优化通过记录字符索引,直接跳过重复区域,减少不必要操作。 时间复杂度 O(n),空间复杂度 O(Σ)。 参考答案 一、滑动窗口实现 function lengthOfLongestSubstring(s) { let set = new Set(); let left = 0, maxLen = 0; for (let right = 0; right < s.length; right++) { while (set.has(s[right])) { set.delete(s[left]); left++; } set.add(s[right]); maxLen = Math.max(maxLen, right - left + 1); } return maxLen; } // 测试 console.log(lengthOfLongestSubstring("abcabcbb")); // 输出 3 思路 使用 滑动窗口 [left, right] 遍历字符串。 用 Set 存储当前窗口内字符。 当遇到重复字符时,移动左指针,直到窗口内无重复字符。 每次窗口扩大时更新最大长度。 时间复杂度 每个字符 最多进出窗口一次 → O(n) 空间复杂度:O(min(n, Σ)),Σ 是字符集大小。 二、性能优化 上面方法每遇到重复字符,需要 逐个删除左边字符。可以进一步优化为 直接跳过重复字符的索引,使用 Map 存储字符上次出现的索引。 ...

编程题 面试题

共 136 道 编程题 面试题。答案默认折叠,便于先自行作答。 1. 实现发布订阅模式 难度:2 · 类型:QA 题目要点 发布订阅模式通过事件中心解耦消息发送者与接收者,核心实现是维护事件名与回调函数列表之间的映射关系。订阅阶段注册回调函数,发布阶段遍历执行所有监听函数,取消订阅则从列表中移除对应回调。工程实践中通常会扩展一次性订阅、事件隔离等能力,但在大型应用中需要避免滥用事件总线,以免造成事件链复杂和内存管理问题。 参考答案 发布订阅模式(Publish–Subscribe Pattern)是一种典型的 消息通信模式。其核心思想是将消息的发送者(Publisher)与接收者(Subscriber)进行解耦,消息不会直接发送给某个具体对象,而是发布到一个事件或主题(Topic)上,由所有订阅该事件的监听者统一接收。 在前端应用中,这种模式常用于 组件通信、事件总线、状态变化通知、插件系统等场景。例如一个模块只负责发布事件,而其他模块只需要订阅事件即可响应变化,双方不需要知道彼此的存在。 实现发布订阅模式通常需要三个核心能力: 注册订阅(subscribe / on) 发布事件(publish / emit) 取消订阅(unsubscribe / off) 本质上可以通过维护一个 事件名 → 回调函数列表 的映射结构来实现。 一个简单实现如下: class EventEmitter { constructor() { this.events = Object.create(null); } on(eventName, handler) { if (!this.events[eventName]) { this.events[eventName] = []; } this.events[eventName].push(handler); } emit(eventName, ...args) { const handlers = this.events[eventName]; if (!handlers) return; handlers.forEach(fn => fn(...args)); } off(eventName, handler) { const handlers = this.events[eventName]; if (!handlers) return; this.events[eventName] = handlers.filter(fn => fn !== handler); } } 使用方式如下: ...