资讯动态

快手前端面试全流程复盘:基础、框架与场景题

发布时间:2026/8/30 12:53:52 来源:尧图企业网站定制
快手这家公司的前端面试整体风格给我的感觉是基础考得细框架问得深场景题特别务实。不像有些公司上来就甩一堆偏题怪题快手更关注你“有没有真的做过东西”以及“做的时候有没有想过为什么”。这篇文章我就把当时的面经完整复盘一遍把每道题背后的考察点、我当时怎么答的、后来复盘觉得应该怎么答更好全部拆开讲清楚。如果你是准备跳槽的前端或者正在面快手这篇内容应该能帮你少走不少弯路。1. 面试全流程回顾从简历筛选到HR面1.1 快手的整体面试流程与节奏快手的面试流程一般是三轮技术面加一轮HR面部分地区或者部门可能会有加面但整体节奏比较紧凑。我当时走完整个流程用了大概两周时间每轮面试间隔三到五天不会让你等得特别焦虑。第一轮通常是基础面主要考察JavaScript、CSS、浏览器原理这些基本功偶尔会穿插一两道简单的算法题。第二轮开始偏向框架和工程化React或者Vue的原理、组件设计、打包工具、性能优化都会涉及这一轮也最容易刷人。第三轮一般是leader面或者交叉面这时候不再单纯问知识点而是给你一个实际场景看你怎么分析和解决问题更像是在考察你的综合能力。这里要提醒一下快手的面试官普遍喜欢顺着你的回答往下追问。比如你说自己用过某个API他会接着问这个API的源码实现再问如果让你自己实现一个类似的你会怎么做。所以面试前一定要把自己简历里写的东西吃透没有真正实践过的内容宁可不写否则追问两三轮就会露馅。1.2 面试前的准备策略与心态调整面快手之前我其实刚经历了另一家公司的面试打击那家公司在框架原理上追得特别深我有一半问题都答得磕磕绊绊。所以这次我调整了策略没有一上来就刷题而是先把前端知识体系重新梳理了一遍。具体做法是画了一张脑图按照JavaScript、CSS、浏览器、网络、框架、工程化、性能优化这几个大方向把每个方向下的核心知识点列出来然后逐一自问自答。比如JavaScript下面我会列出来原型链、闭包、作用域、事件循环、异步方案这些然后问自己“如果让我给一个刚入门的人讲清楚闭包是什么我能不能做到”。如果发现某个问题讲不清楚就说明这块还有盲区。心态上也要调整好。面快手这种体量的公司面试官见过太多候选人你的水平几斤几两基本上十分钟之内他就摸清楚了。所以不要试图伪装或者背答案大大方方承认自己哪些地方没深入过反而比硬撑着胡编要好得多。当然承认不会之后最好补一句“但是我的理解是大概怎样我打算面试结束后去深入看一下”这样至少能体现你的学习意识。2. 第一轮技术面JavaScript基础与浏览器原理2.1 高频考点事件循环、闭包与异步机制第一轮面试刚开始面试官先是让我做了个自我介绍然后就直接切入了JavaScript基础。第一个问题就是经典的事件循环不过他没有直接问“什么是事件循环”而是给了一段代码让我说出输出顺序。console.log(script start); setTimeout(function() { console.log(setTimeout); }, 0); Promise.resolve().then(function() { console.log(promise1); }).then(function() { console.log(promise2); }); console.log(script end);这道题考察的是宏任务与微任务的执行顺序核心结论是同步代码先执行然后执行当前宏任务队列中的所有微任务微任务执行过程中如果产生了新的微任务也会一起执行完最后才会轮到下一个宏任务。所以输出顺序是script start、script end、promise1、promise2、setTimeout。我当时答对了但面试官紧接着追问了一句“如果setTimeout和Promise嵌套在一起会怎样”这才是真正的考点。比如setTimeout(function() { console.log(timeout1); Promise.resolve().then(function() { console.log(promise in timeout); }); }, 0); setTimeout(function() { console.log(timeout2); }, 0);这里要注意的是两个setTimeout都属于宏任务先执行第一个setTimeout的回调打印timeout1然后它内部产生的微任务promise in timeout会被加入微任务队列但需要等当前宏任务执行完毕后才会处理。所以输出是timeout1、promise in timeout、timeout2。这个细节很容易答错大家一定要记住每个宏任务执行完之后都会清空当前的微任务队列再进入下一个宏任务。闭包也是必考的点。面试官让我“说一段闭包的代码并解释为什么能访问外部变量”然后继续追问了闭包的内存泄漏问题。生产环境常见的坑是循环引用比如把DOM元素保存在数组里又在DOM上挂了一个引用指向这个数组就会形成循环引用导致内存无法释放。现在大部分浏览器引擎的垃圾回收器已经能处理循环引用了但闭包本身持有外部作用域的引用如果这个外部作用域包含了很大的对象即使闭包本身很小也会把大对象一直留在内存里。所以闭包不是不能用而是要注意控制作用域范围。2.2 经典问题原型链与继承方案对比面试官问完事件循环之后直接跳到原型链。他给我画了一个简单的对象关系图让我说出obj、Obj.prototype、Obj.__proto__、Function.prototype之间的关系。这个问题其实是在考察你是否真正理解JavaScript里“函数也是对象”这个概念。我当时的回答思路是这样的obj是一个普通对象它的__proto__指向Obj.prototypeObj是个构造函数它的__proto__指向Function.prototypeObj.prototype是一个对象它的__proto__指向Object.prototypeFunction.prototype的__proto__指向Object.prototype画出来就是一条完整的链路obj - Obj.prototype - Object.prototype - null而构造函数那一侧是Obj - Function.prototype - Object.prototype - null。接着他问继承的实现方式我列了原型链继承、构造函数继承、组合继承、寄生组合继承这几种然后重点说了ES6的class继承其实也是基于寄生组合继承实现的。Class里面的extends关键字做的事情本质上是把子类的原型对象指向父类原型对象的一个新对象同时修改子类的__proto__指向父类构造函数。这个细节可能很多写业务代码的人没注意过但在面试里答出来会很加分。关于原型链这块我后来复盘觉得有一个点值得补充不要死记硬背继承的代码实现而要理解prototype和__proto__的指向关系。面试官考察继承的目的不是想让你背代码而是想看你是否理解“JavaScript通过原型链实现属性查找”这个底层机制。你只要把“对象查找属性 - 顺着原型链向上找 - 找到就返回找不到就返回undefined”这条链路想清楚很多问题都能迎刃而解。3. 第二轮技术面框架原理与工程化实践3.1 框架考察方向Diff算法、Hooks闭包陷阱与响应式原理进入第二轮面试官明显更关注框架深度。我面的是React方向所以上来先问了React的Diff算法。这个问题属于“人人都能说几句但很少人能说透”的高频题。我当时的回答分了三层第一层是“同层比较”React的Diff不会跨层级比较节点而是逐层比较一旦发现某个节点类型不同就直接替换整个子树不做进一步比较。第二层是“key的作用”在同一个列表里React通过key来判断节点是复用还是重新创建。高效的Diff需要尽可能减少节点移动所以在用key的时候要选稳定且唯一的标识不能选数组下标。因为如果列表头部插入了新元素用index作为key会导致后面的所有元素都被认为改变了。第三层是“Fiber的引入”在React 16之后Diff过程变得可中断了。Fiber把整个渲染过程拆成一个个小单元每完成一个单元就让出主线程这样就不会因为一次大渲染把页面卡死。这个设计其实是把同步的、不可中断的递归调用改成了异步的、可中断的链表遍历。面试官听完之后追问了一个场景如果列表顺序发生了调换用index作为key会有什么问题这个我确实踩过坑当时答得还算顺利。用index作为key当列表顺序调换时React并不知道这些节点只是换了位置它会认为每个位置上的节点类型和内容都变了于是销毁重建全部DOM。而在某些情况下如果这个列表项是有内部状态的组件比如输入框位置的调换会导致输入框内容错乱因为React复用了组件实例但props和state没有正确对应起来。Hooks闭包陷阱也是现在React面试的必问内容。面试官给了一个场景function Counter() { const [count, setCount] useState(0); useEffect(() { setInterval(() { console.log(count); }, 1000); }, []); return div{count}/div; }这段代码的问题在于useEffect传入的是空数组所以effect只会在组件挂载时执行一次闭包里捕获的count始终是初始值0。即使后面count变了setInterval里的回调拿到的还是旧的闭包变量。解决方式有几种一是把count加入依赖数组让effect重新执行但这样会导致定时器被反复清除和重建二是用ref来保存最新的count值三是用函数式更新比如setCount(prev prev 1)这种。面试官继续追问为什么依赖数组里的值变化了闭包就能拿到新值因为useEffect在依赖变化时会清理上一次的effect然后重新创建新的effect。新的effect在创建时会捕获当前这次渲染的count值所以闭包里的值就是新的。这个解释把“闭包陷阱”和“effect的清理机制”串起来了我在回答的时候也明显感觉到面试官比较满意。3.2 工程化考点Webpack构建优化与Vite对比快手这种体量的公司前端代码库非常庞大所以工程化能力一定是考察重点。面试官问的是如果线上项目首屏加载很慢你会怎么定位和优化。这个问题很开放我按“从外到内”的思路来答先看网络层面资源体积是否过大有没有开启gzip或者br压缩CDN节点是否覆盖到位。接着看代码层面首屏有没有懒加载路由是不是按需拆分的有没有加载了当前页面用不到的第三方库。再看缓存策略静态资源的contenthash有没有配好能不能做到长期缓存。最后看渲染层面有没有SSR或者预渲染白屏时间到底花了多久。面试官听完之后直接让我结合Webpack说具体配置方案。我提到了这几个点用splitChunks把第三方库拆成单独chunk这样业务代码更新时不会导致第三方库缓存失效用thread-loader或者terser-webpack-plugin开启多进程压缩加快构建速度用cache-loader或者Webpack 5内置的持久化缓存二次构建速度能快很多用import()语法做路由懒加载配合React.lazy使用然后他问了我一个对比问题既然Webpack已经能完成这些事为什么还要用Vite。我的理解是开发阶段Vite利用浏览器原生ESM能力启动速度比Webpack快非常多因为Webpack启动时要先打包整个项目而Vite只有在你请求某个模块时才实时编译那个模块。但在生产构建阶段Vite还是通过Rollup打包所以如果你遇到的是构建速度问题Vite的优化效果主要体现在开发环境而不是生产环境。4. 场景题与综合能力考察实际问题分析与方案设计4.1 场景题设计一个支持并发控制的请求队列第三轮面试是leader面开场没有直接问技术题而是先聊了一下我在上一家公司的项目经历然后抛出来一个场景题如果前端需要向后端发送大量请求比如1000个但后端接口同时只能承受10个并发你会怎么处理。这种题在快手这种业务流量大的公司特别常见因为他们的页面里确实会碰到大量数据请求的场景比如推荐流的批量数据上报、批量查询用户状态等等。我当时的思路是设计一个带并发控制的请求队列核心逻辑是初始化一个任务队列把所有请求都放进队列里设置一个最大并发数比如10一开始先启动10个任务并行发送每完成一个任务从队列里再取出一个任务补上来直到队列清空所有请求完成我用JavaScript伪代码写了一个版本核心内容是这样的function limitRequest(urls, limit 10) { const results []; let current 0; let finished 0; const total urls.length; return new Promise((resolve) { function run() { while (current total results.length - finished limit) { const index current; fetch(urls[index]) .then(res res.json()) .then(data { results[index] data; }) .catch(err { results[index] err; }) .finally(() { finished; if (finished total) { resolve(results); } else { run(); } }); } } run(); }); }说实话这段代码我写的时候稍微有点紧张results.length - finished limit这个条件其实写得不严谨因为results数组在并发过程中会有空洞直接取length有误差。如果换一种更严谨的方式应该维护一个当前正在执行的任务数计数器每次发起请求时加一请求完成时减一然后用循环判断是否还能继续发起请求。不过面试官看过之后没有太纠结代码细节而是继续问我如果某个请求一直挂起不返回队列会不会被卡死。这个问题就是考察你有没有考虑到超时和失败重试。我补充了AbortController来做请求超时控制比如超过10秒就主动中断再从队列里拿出下一个请求执行。同时也想了一下失败重试的策略不能无脑重试因为后端如果已经处于过载状态重试只会加重压力所以一般要用退避策略比如第一次失败后等1秒重试第二次等2秒最多重试3次。4.2 手写题实现一个带过期时间的localStorage封装场景题结束后面试官说“来一道手写题吧”给了我一个需求封装一个localStorage的读写方法支持设置过期时间过期后读取返回null。这个题目本身不算难但他的要求是“写一个生产可用的版本”这就意味着需要考虑异常情况比如存储空间满了、JSON解析失败、单条数据格式损坏等。我当时写了一个大概的版本const storage { set(key, value, expireSeconds) { const data { value, expire: expireSeconds ? Date.now() expireSeconds * 1000 : null }; try { localStorage.setItem(key, JSON.stringify(data)); } catch (e) { // 处理存储已满的情况 console.error(存储失败, e); } }, get(key) { const raw localStorage.getItem(key); if (!raw) return null; try { const data JSON.parse(raw); if (data.expire Date.now() data.expire) { localStorage.removeItem(key); return null; } return data.value; } catch (e) { // 数据损坏直接清除 localStorage.removeItem(key); return null; } }, remove(key) { localStorage.removeItem(key); } };写完之后面试官问我如果同一个key被设置了很多次之前的过期时间怎么管理。其实这里有个细节localStorage本身没有索引机制同一个key被覆盖写旧值就丢了所以只需要存储最新的过期时间即可不需要管理历史版本。后面复盘时我想到还可以考虑一个更完善的方向做一个lru淘汰策略。当存储空间满了优先清除那些即将过期的数据或者是最久没被访问的数据。不过这个方向在面试时间限制内一般不会展开要求只要能说出思路和实现方案就够了。4.3 性能优化面从首屏加载到交互流畅度快手作为短视频和直播平台对页面性能的要求比一般公司高很多。leader面里有一道题让我印象很深他问我一个资讯类的Web页面首屏要在1秒内达到可交互状态你会怎么做。我按流程拆解了一下资源加载阶段做DNS预解析、建立HTTP/2多路复用、关键资源加preload、非关键资源加defer数据获取阶段能用SSR的首屏数据就用SSRSSR的数据注入到客户端可以减少一次请求往返如果是纯CSR考虑在HTML里内联首屏数据渲染阶段避免首屏出现长任务把耗时的计算拆成多个小任务用requestIdleCallback处理不紧急的工作图片优化首屏的图片用CDN的二倍图或者WebP小图标用SVG内联或者iconfont面试官继续问如果你的页面在低端机上滚动时有明显的卡顿你会怎么排查。这个问题我实战经验不算多所以回答中规中矩说了检查是否有频繁的layouts、长列表渲染、大量使用box-shadow这类会造成重绘的样式。后来面试官给了个比较实用的排查思路先在Performance面板录制一段滚动操作看看每个帧的耗时分布是脚本执行占用太高还是渲染本身占用太高然后再针对性地优化。低端机性能弱很多在高端机上跑得很顺畅的动画在低端机上会卡到没法用所以做性能优化时最好在低端机上测试一遍。5. 算法与手写题专项题目难度与解题策略5.1 面试中的算法题从简单到中等的梯度设计快手技术面的算法题难度整体处于LeetCode简单到中等之间不会出现特别极端的hard题但会结合实际业务场景来做变形。我遇到的两道题都跟业务场景有绑定关系。第一道题出现在一面是字符串反转的变体给定一个字符串按单词反转比如hello world foo变成foo world hello要求不使用split和reverse等内置方法。这道题其实考察的是双指针思路。我当时用双指针从尾部往头部扫描遇到空格就切一个单词拼接到结果里。需要注意的是题目的边界条件开头和结尾可能有多个空格单词之间也可能有多个空格如果事先搞不清楚这些要求在面试中会反复调整代码。第二道题出现在二面是个数组去重加排序的变形给定一个整数数组返回出现频率最高的前K个元素。这道题最常见的解法是用哈希表统计频率然后用桶排序或者小顶堆取出频率前K的元素。我当时用了小顶堆的方式时间复杂度是O(n log k)空间复杂度是O(n)。面试官接着问如果n很大比如上亿内存装不下怎么办。这时候就需要考虑外部排序、分治或者近似算法比如用Count-Min Sketch这种概率性数据结构来统计频率。5.2 手写题实现Promise.all与防抖函数手写Promise.all是现在前端面试的标配几乎每个公司都会问。快手这轮也没有例外但面试官加了一个小限制条件不能使用async/await只能基于原生的Promise实现。写Promise.all时最关键的点是要处理两种情况一是全部成功时按传入顺序返回结果数组二是有任意一个失败时直接reject这个错误。我当时用了一个计数器来判断是否全部完成结果数组用索引赋值的方式保证顺序一致。还处理了传入空数组的情况直接resolve一个空数组。防抖函数也是高频手写题。这个函数的核心是在事件被连续触发时只有最后一次触发后等待指定时间才会执行。我在实现时用闭包保存定时器ID每次触发时先清除定时器再重新设置。面试官还问到了immediate参数也就是第一次触发时立刻执行后面的连续触发不执行直到停止后才重新reset。这里要特别小心this的指向问题如果用普通函数写法内部要用context来保存this用箭头函数则会丢失this指向。6. 面试总结常见陷阱与个人经验体悟6.1 面试过程中暴露的薄弱点与改进方向整个面试下来我最大的感受是快手面试官问问题的逻辑性很强不是零散地考知识点而是会围绕一个核心主题层层深入。比如他在问React Diff的时候是从“diff是什么”问到“为什么需要key”再到“Fiber为什么能中断”最后到这个设计对开发者有什么影响。这个追问链条其实是在模拟一个真实的工作场景当你在开发中遇到性能问题时需要一层层往下定位而不是只会说“组件加载慢加个memo”。我自己的薄弱点在CSS布局和浏览器渲染细节上。一面的时候面试官问了一个flex布局中flex: 1的含义我没有完整答出它其实是flex-grow: 1; flex-shrink: 1; flex-basis: 0%的缩写形式只说了会占满剩余空间。这个点虽然在业务上够用但在面试里会显得理解不够深入。后来我复盘时专门把CSS的常用布局、BFC、层叠上下文这些内容重新过了一遍。6.2 给准备面试的朋友的几点实操建议结合这次面快手的经验我想给正在准备前端面试的同学几个建议尤其是对标这个方向的朋友第一是不要只背面试题要理解题目背后的原理。前端面试题在网上随处可见但如果你只是把答案背下来面试官换个角度追问就露馅了。最好的状态是能用自己的话把一个知识点顺下来并且能举例说明。第二是刷题要有针对性。算法题不用刷太多但要让自己的思路保持熟练。重点复习哈希表、双指针、二叉树遍历、动态规划、数组和字符串操作这几种类型概率最大。第三是面试前把简历里的项目彻底复盘一遍。想想你做的每个功能技术方案是怎么选的有没有其他方案为什么没用上线后效果如何。这些内容是面试官追问的主要来源也是你展现自己技术深度的最好机会。第四是提前准备反问环节的问题。面试官最后一般都会问“你有什么想问我的”这时候别问那种明显能从官网找到答案的问题也别直接说“没有”。可以问团队的主要技术栈是什么当前团队在做的核心项目是什么或者对新人来说最重要的能力是什么。这既能显示出你对这个机会是认真的也能帮你了解自己进去之后要面对的是什么。6.3 我自己踩过的坑与一些小经验最后再分享几个我亲身踩过的坑这些细节如果没人提醒真的很容易吃亏。第一个坑是面试的时候不要为了展示自己懂得多而主动把话题带偏。比如面试官在问闭包你觉得这个话题很简单就开始扯垃圾回收机制、V8引擎的优化策略结果面试官顺势追问发现你的垃圾回收理解也就停留在表面反而暴露了短板。面试是展示自己最强的地方而不是把所有会的都倒出来更不是在自己最弱的地方硬展开。第二个坑是手写题的时候一定要先想清楚再动笔不要边写边改。我二面的时候写Promise.all一开始没想清楚空数组的处理逻辑写了十几行发现有问题又回头改整个过程看起来就很不专业。后来面试官跟我说其实他不在乎你的代码写得多么完美更在意的是你遇到问题时的处理方式。是慌慌张张乱改还是先想清楚再动手这两者的差距是很明显的。第三个坑跟情绪有关不要被上一轮的失利影响下一轮的心态。快手的面试节奏比较快可能你刚觉得一面某个问题没答好第二天就开始二面了。但面试官之间是独立评价的一面没答好的问题二面可能会从另一个角度重新验证你的能力。所以每一轮都把它当成全新的开始不要背负上一轮的心理包袱。我在实际面试过程中发现一个规律真正让你通过面试的往往不是你答对了多少道题而是你面对不熟悉的问题时能不能沉住气按照“分析问题 - 拆解问题 - 提出思路”的方式来处理。如果你能做到这一点哪怕最后方案不完美面试官也会觉得你是个有潜力的人。这一点在快手的面试中表现得特别明显也是我认为整个面试过程中最值得借鉴的地方。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价