AI相关 面试题

共 43 道 AI相关 面试题。答案默认折叠,便于先自行作答。 1. 如何实现 ChatGPT 类似的流式输出? 难度:1.5 · 类型:QA 题目要点 流式输出的核心是服务端通过 SSE、fetch stream 或 WebSocket 持续返回增量内容,前端通过 ReadableStream 边读边渲染。实现时要注意网络 chunk 不等于完整消息,需要做缓冲和协议解析。前端渲染要做节流,避免频繁状态更新导致卡顿。Agent 场景下建议使用结构化事件协议,把文本增量、工具调用、状态、错误、完成信号分开处理。最后还要支持取消、错误恢复、最终 Markdown 校正和滚动体验,才能达到接近 ChatGPT 的交互质量。 参考答案 类似 ChatGPT 的流式输出,本质是:服务端不要等完整答案生成后再返回,而是把模型生成的 token 或文本片段持续写入 HTTP 响应;前端边读边渲染,并在完成、取消、异常时维护好状态。 实际项目里最常见的是 SSE 或 fetch + ReadableStream。如果只是服务端向前端单向推送,SSE 足够稳定;如果需要双向实时交互,比如语音、多人协作、实时 Agent 控制,可以用 WebSocket。在 ChatGPT 这类文本生成场景中,fetch 读取流也很常用,因为它支持 POST、请求体、鉴权、AbortController,控制能力比原生 EventSource 更强。 前端核心代码大致是这样: async function streamChat(messages: any[], onDelta: (text: string) => void) { const controller = new AbortController(); const response = await fetch('/api/chat/stream', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ messages }), signal: controller.signal, }); if (!response.ok || !response.body) { throw new Error('stream request failed'); } const reader = response.body.getReader(); const decoder = new TextDecoder('utf-8'); let buffer = ''; while (true) { const { value, done } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const events = buffer.split('\n\n'); buffer = events.pop() || ''; for (const event of events) { const line = event .split('\n') .find((item) => item.startsWith('data:')); if (!line) continue; const data = line.replace(/^data:\s*/, ''); if (data === '[DONE]') { return; } const payload = JSON.parse(data); if (payload.type === 'delta') { onDelta(payload.content); } } } controller.abort(); } 服务端返回时通常使用类似这样的协议: ...

CSS 面试题

共 114 道 CSS 面试题。答案默认折叠,便于先自行作答。 1. Tailwind 的响应式断点(如 md:)底层如何实现? 难度:2 · 类型:QA 题目要点 Tailwind 的响应式断点基于构建期的变体系统实现。md: 在编译阶段被解析为 screen 变体,并根据 screens 配置生成对应的 @media (min-width) 包裹规则。所有响应式逻辑依赖原生 CSS media query,遵循 mobile-first 原则,通过规则生成管线支持多变体组合,不涉及运行时计算。 参考答案 这个问题的关键在于理解: md: 不是运行时逻辑,而是 构建期的变体(variant)展开机制。 在 Tailwind CSS 中,响应式系统建立在三个核心之上: screens 配置映射 变体解析机制 JIT 构建阶段规则生成 一、md: 本质是什么? 当写: <div class="md:text-red-500"> Tailwind 在构建阶段会将其解析为: variant:md utility:text-red-500 然后生成如下 CSS: @media (min-width: 768px) { .md\:text-red-500 { color: #ef4444; } } 关键点: md: 只是一个前缀标记 构建阶段转换成 @media 冒号会被转义为合法 CSS 选择器 二、断点从哪里来? 断点定义在配置文件中: ...

ES6 面试题

共 51 道 ES6 面试题。答案默认折叠,便于先自行作答。 1. 说说 Class 语法糖的底层实现 难度:3 · 类型:QA 题目要点 class 是基于函数构造器和原型链的语法封装,实例方法本质挂载在 prototype 上,继承通过 Object.create 构建原型链,同时通过构造函数原型链继承静态方法。super 依赖内部 [[HomeObject]] 机制定位父类方法。class 并未引入新的对象模型,而是在原型继承基础上增加语义约束与语法增强。 参考答案 这个问题的关键在于理解:class 并不是一种新的面向对象机制,而是对 原型继承(prototype-based inheritance) 的语法封装。底层仍然是函数构造器 + 原型链。 如果把 class 当作 Java 那种基于类的模型去理解,就会产生误解。 先看一个简单例子: class Person { constructor(name) { this.name = name; } say() { console.log(this.name); } static create(name) { return new Person(name); } } 它在底层大致等价于: function Person(name) { this.name = name; } Person.prototype.say = function () { console.log(this.name); }; Person.create = function (name) { return new Person(name); }; 一、实例方法的实现 ...

HTML 面试题

共 77 道 HTML 面试题。答案默认折叠,便于先自行作答。 1. SPA和MPA的区别? 难度:1 · 类型:QA 题目要点 SPA 通过前端路由在单一 HTML 中完成页面切换,交互流畅但首屏和 SEO 成本较高;MPA 每次导航请求新页面,SEO 友好且隔离性强,但交互体验有限;两者本质差异在于前后端渲染与路由职责的划分;现代项目常通过 SSR、SSG 等方式在两种模式之间折中。 参考答案 SPA 和 MPA 的区别,表面看是“页面跳转方式不同”,本质上是前端与服务器在渲染职责、路由控制以及资源加载策略上的分工差异。理解这一点,才能在具体业务中做出合理选择。 SPA(Single Page Application)只有一个 HTML 入口,首屏加载时会拉取完整的 JavaScript 应用骨架,之后的页面切换通过前端路由完成。URL 的变化并不会触发浏览器重新请求文档,而是由前端框架根据路由状态切换视图并复用已有资源。这种模式的优势在于页面切换流畅、状态可以在客户端长期保留,复杂交互体验更接近原生应用,非常适合后台系统、强交互工具型产品。但代价也同样明显:首屏资源压力大,SEO 先天不足,对 JS 执行和异常处理的依赖极高,一旦主 bundle 出现问题,整个应用都无法正常工作。 MPA(Multi Page Application)则以“页面”为核心单元。每一次导航都会向服务器请求一个新的 HTML 文档,路由和渲染主要由服务端控制。页面之间天然隔离,资源和状态会随页面刷新而重置。这种模式的优势在于首屏直观、SEO 友好、容错性强,适合内容型站点、营销页和对搜索引擎依赖较高的业务场景。但在交互复杂度上,MPA 往往需要付出更多成本,例如重复加载公共资源、跨页面状态共享困难,整体体验也更接近传统 Web。 从工程角度看,SPA 和 MPA 的差异还体现在构建和部署方式上。SPA 更强调前端工程化和运行时能力,构建目标是一个可长期运行的客户端应用;MPA 更强调页面级拆分和服务端模板渲染,前端更多是增强层而非控制中心。在现代实践中,两者并非对立关系,常见的折中方案包括 SSR、SSG 以及“多入口 SPA”,试图在交互体验和首屏性能、SEO 之间取得平衡。 因此,选择 SPA 还是 MPA,并不存在绝对优劣,关键在于业务目标:是以复杂交互和状态管理为核心,还是以内容分发和搜索可见性为核心。 2. 常见的H5标签有哪些 ,你是怎么用的 难度:1 · 类型:QA 题目要点 常见 H5 标签包括结构语义标签、文本语义标签、表单标签和媒体标签;实际使用中优先考虑语义表达和可访问性,而非样式;充分利用表单和媒体标签的原生能力以减少 JS 复杂度;canvas 与 svg 需根据场景选择;合理使用语义化标签能显著提升页面质量和长期可维护性。 ...

JavaScript 面试题

共 407 道 JavaScript 面试题。答案默认折叠,便于先自行作答。 1. requestAnimationFrame 与 requestIdleCallback 在渲染优化中的执行时机差异?谁优先触发? 难度:3 · 类型:QA 题目要点 requestAnimationFrame 在浏览器即将进行下一帧渲染之前执行,用于动画与视觉更新,优先级高于渲染阶段后的任务。requestIdleCallback 只会在当前帧所有渲染任务完成且仍有剩余时间时触发,属于低优先级空闲调度机制。因此在同一帧中,rAF 必然先于 rIC 执行。rAF 用于保证帧同步,rIC 用于利用空闲时间执行非关键任务。 参考答案 这个问题本质是理解 浏览器一帧的调度模型。 不搞清楚一帧里发生了什么,很容易回答成“谁快谁慢”这种表层结论。 核心结论先给出: 在有下一帧渲染需求的情况下,requestAnimationFrame 一定优先于 requestIdleCallback 执行。 requestIdleCallback 只会在当前帧还有“空闲时间”时才会触发。 下面从浏览器帧模型拆解。 一、浏览器一帧的基本流程 以 60fps 为例,一帧约 16.6ms。 一次完整渲染循环大致是: 执行宏任务(例如 setTimeout) 清空微任务队列 触发 requestAnimationFrame 回调 进行样式计算(Style) 布局计算(Layout) 绘制(Paint) 合成(Composite) 如果还有时间 → 执行 requestIdleCallback 需要明确一点: rAF 是“下一帧渲染前”的回调 rIC 是“当前帧剩余时间”的回调 二、requestAnimationFrame 的执行时机 requestAnimationFrame 的设计目标是: 在浏览器即将进行下一次重绘之前执行回调。 特点: 每帧最多执行一次 与刷新率同步 在样式计算之前触发 如果标签页不可见,会暂停 执行时机可以理解为: ...

leetcode 面试题

共 38 道 leetcode 面试题。答案默认折叠,便于先自行作答。 1. 最大公共前缀 难度:2 · 类型:QA 编写一个函数,接收一个字符串数组作为输入,找出这些字符串的最大公共前缀。如果没有公共前缀,则返回空字符串。 输入输出要求: 输入:一个字符串数组。 输出:一个字符串,表示最大公共前缀。 示例: const input = ["flower", "flow", "flight"]; const result = longestCommonPrefix(input); console.log(result); // 输出:'fl' 题目要点 最大公共前缀问题的核心是逐步缩小候选前缀;横向扫描通过不断用后续字符串修剪前缀,实现简单且高效;在最坏情况下时间复杂度为 O(n·m),空间复杂度为 O(1),非常适合在实际工程和面试中使用。 参考答案 这个问题本质是在多个字符串之间求一个公共的、连续的前缀子串,并且要求尽可能长。关键不在于字符串操作技巧,而在于如何逐步收敛搜索空间。 一、整体思路说明(横向扫描) 横向扫描的核心思想是: 先假设第一个字符串是公共前缀,然后不断用后续字符串去“修剪”它。 执行过程可以概括为: 取第一个字符串作为初始前缀 从第二个字符串开始,逐个比较 如果当前字符串不以该前缀开头,就不断缩短前缀 一旦前缀缩短为空,说明不存在公共前缀,可以提前结束 这个过程的好处是逻辑直观,且在工程中可读性和可维护性都很好。 二、示例代码实现(横向扫描) function longestCommonPrefix(strs) { if (!strs || strs.length === 0) return ""; let prefix = strs[0]; for (let i = 1; i < strs.length; i++) { while (!strs[i].startsWith(prefix)) { prefix = prefix.slice(0, -1); if (prefix === "") return ""; } } return prefix; } 示例验证 const input = ["flower", "flow", "flight"]; console.log(longestCommonPrefix(input)); // "fl" 三、时间与空间复杂度分析 时间复杂度: 最坏情况下为 O(n * m) ...

Node.js 面试题

共 40 道 Node.js 面试题。答案默认折叠,便于先自行作答。 1. NestJs、Nust.js、Next.js 这几个框架有什么区别 难度:2 · 类型:QA 题目要点 NestJS 是 Node.js 的企业级服务端框架,强调模块化、依赖注入和 TypeScript,适合构建 API 和微服务; Nuxt.js 是 Vue 生态的全栈框架,在 Vue 基础上提供 SSR、SSG、文件路由等能力; Next.js 是 React 生态的全栈框架,除了 SSR、SSG 外,还支持 React Server Components、Server Actions 等现代特性。 三者最大的区别在于定位不同:NestJS 负责后端服务,Nuxt.js 和 Next.js 负责全栈 Web 应用开发,分别服务于 Vue 和 React 技术栈。 参考答案 这几个框架名字非常相似,但定位完全不同。简单来说: NestJS:Node.js 服务端开发框架。 Nuxt.js:Vue 生态的全栈应用框架。 Next.js:React 生态的全栈应用框架。 三者分别解决的是不同层面的问题,并不存在直接竞争关系。 先来看 NestJS。 NestJS 是基于 Node.js 构建的服务端框架,底层默认使用 Express,也可以切换到 Fastify。它借鉴了很多 Java Spring 的设计思想,例如依赖注入(DI)、模块化、装饰器以及 IoC 容器,因此对于大型团队开发来说,代码组织会更加规范。 ...

React.js 面试题

共 148 道 React.js 面试题。答案默认折叠,便于先自行作答。 1. 说说 Hooks 的依赖数组原理 难度:3 · 类型:QA 题目要点 Hooks 的依赖数组本质是一次浅层引用比较机制。React 在 commit 阶段通过 Object.is 逐项比较新旧依赖数组,决定是否重新执行 effect,并在变化时先执行 cleanup。依赖数组不会监听变量变化,也不负责触发 render,只用于声明副作用与状态之间的关系。依赖缺失会导致闭包捕获旧值,依赖不稳定会导致 effect 频繁执行,因此依赖必须完整且保持引用稳定。 参考答案 这个问题的核心,不是“依赖数组怎么写”,而是:React 是如何判断副作用是否需要重新执行的。 依赖数组的存在,本质是为了让 React 在两次 render 之间进行一次“浅层比较(shallow compare)”,从而决定是否跳过本次 effect 的执行。 一、从 useEffect 的执行时机说起 useEffect(() => { // effect }, [a, b]); React 在每次 render 结束后,会把 effect 收集到 Fiber 节点的 effect 链表中。 当进入 commit 阶段时,React 会: 取出当前 Fiber 上一次保存的依赖数组 与本次 render 生成的新依赖数组进行比较 决定是否标记该 effect 需要执行 如果依赖变化,则: ...

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 时: ...