公司动态

贝壳找房前端笔试全复盘:事件循环、DP与错误监控一次讲清

📅 2026/8/29 23:20:20
贝壳找房前端笔试全复盘:事件循环、DP与错误监控一次讲清
2024年8月的一天下午我拿着手机盯着邮箱里贝壳找房2025届秋季校园招聘前端岗位第一批笔试通知那行字心跳快了半拍。秋招做过的笔试不少大部分公司都是在牛客或者赛码上扔一套卷子题目从题库里随机抽既看不出诚意也测不出水平。贝壳这套题却不太一样有基础的八股选择题有需要动手写的编程题还有一道开放设计题。整体难度不算变态但它非常典型典型到可以说就是一线互联网公司对前端应届生的标准能力画像。考完之后我复盘了整整两天把每道题背后的知识点、当时的错误判断、以及这道题到底想筛选什么样的人都重新过了一遍收获比刷十套LeetCode都大。这篇文章不只是把题目和答案复述一遍我会把选择的逻辑、踩坑的过程、以及怎么在时间不够的情况下拿到该拿的分尽量完整地还原出来。无论你是2025届、2026届正在备战的在校生还是已经工作一阵子想跳槽的工程师应该都能从这里读出一些通用的东西。1. 笔试摸底这套题考什么、该怎么分配时间1.1 三个模块选择、编程、开放贝壳第一批前端笔试整体分为三个模块。第一个模块是选择题单多选混合覆盖JavaScript基础、CSS、浏览器与网络、Vue/React框架原理。这部分大概占了40%的分数看起来简单但多选题特别容易翻车因为很多选项是在一个细微的地方做改动少选、错选都不得分。第二个模块是编程题一般是2到4道考察数据结构和算法。整体难度在LeetCode中等题偏下的水平但有一道题会稍微复杂一点。值得注意的是贝壳的编程题里经常有一道前端场景题不直接考算法而是让你手写某个工具函数、组件逻辑或者业务场景中常见的函数这类题目的核心是考工程思维而不是纯算法。第三个模块是开放设计题给你一个前端工程问题让你论述设计方案。我遇到的是错误监控相关这个我后面专门展开讲。1.2 开考前我做的三件事收到笔试通知到正式开考大概有三天时间。我没有盲目刷题而是做了三件比较有效的事情。第一件去牛客和社区翻以往的面经、笔经。虽然每个批次的题不完全一样但出题风格和侧重点是有延续性的比如往年就有人提到贝壳喜欢考事件循环、缓存和动态规划这让我在复习时有了明确方向。第二件把JavaScript中事件循环、Promise、闭包、原型链这四块重新精读了一遍。不是背概念而是要求自己能在纸上画出代码执行过程中调用栈、宏任务队列、微任务队列的变化过程。这一步在后面选择题上直接帮我省出了至少5分钟。第三件重新练了一遍牛客/赛码平台的输入输出环境。这里提醒大家笔试前一定要花时间熟悉你即将使用的在线平台。比如赛码的Node.js环境下输入输出是用readline模块自己处理的和本地浏览器里写的脚本完全是两套玩法。我有同学因为不熟悉光是在输入输出上就卡了十分钟直接把心态打崩。1.3 时间分配策略先拿稳的再啃难的贝壳这批笔试总时长是90分钟。我的时间分配是选择题20到25分钟编程题和场景题55到60分钟开放题10到15分钟。这个顺序不是随便定的背后的逻辑是先把确定性能拿到的分数全部拿到再去挑战不确定的部分。选择题虽然分值不算高但做起来快而且很多是记忆型题目趁头脑清醒的时候回答正确率最高。编程题一定要先花一两分钟把所有题目都扫一遍评估每一道题的难度和熟悉度然后严格按照从易到难的顺序来写不要一上来就死磕最难的DP题。开放设计题放在最后因为它的主观性比较大即使只剩10分钟写一个清晰的框架也能拿到部分分完全空着就彻底没戏了。提示笔试系统普遍有切屏检测千万不要想着开另一个标签页查资料。贝壳这批是双机位监控切屏一次会记录切屏多次可能直接判作弊代价太大不值得。2. 选择与填空题事件循环、闭包和浏览器缓存的那些经典陷阱2.1 事件循环输出题await不是简单等一等选择题第一类高频考点是输出题就是给你一段代码让你判断输出顺序。前端人应该都懂这种题考的就是事件循环、宏任务与微任务以及Promise的执行时机。贝壳的题里自然也少不了。举个例子给你这样一段代码让你写出输出顺序async function async1() { console.log(async1 start); await async2(); console.log(async1 end); } async function async2() { console.log(async2); } console.log(script start); setTimeout(() { console.log(setTimeout); }, 0); async1(); new Promise((resolve) { console.log(promise1); resolve(); }).then(() { console.log(promise2); }); console.log(script end);正确答案是script start - async1 start - async2 - promise1 - script end - async1 end - promise2 - setTimeout。这个输出题几乎每年每个公司都会考但每次正确率都不高原因在于很多人对await的理解停留在等一等再执行后面代码这种模糊层面。实际执行时async1函数体里的await async2()这一行会先同步执行 async2()所以在控制台能立刻看到 async2 输出。然后遇到awaitasync1让出控制权把后续的console.log(async1 end)注册为微任务。紧接着继续执行同步代码也就是输出 promise1并把 promise2 注册为另一个微任务。同步代码全部执行完后输出 script end然后进入微任务队列按注册顺序执行先 async1 end再 promise2。最后执行宏任务队列里的 setTimeout。这里最容易错的两个点一是误以为await async2()整句都是异步的把 async2 的输出顺序排到后面二是没有意识到async1 end和promise2在同一个微任务队列里的先后顺序取决于注册顺序。如果只背微任务比宏任务先执行遇到多个微任务混杂的情况就会算错。2.2 闭包与变量提升一个隐藏的let另一道非常经典的输出题是循环闭包。面试官把这段代码放在选择题里var a []; for (var i 0; i 3; i) { a[i] function () { console.log(i); }; } a[0](); a[1](); a[2]();输出是 3 3 3因为var声明的i没有块级作用域循环结束后i变成了3所有闭包捕获的是同一个变量i。要想输出0、1、2要么把var改成let要么用立即执行函数包一层for (var i 0; i 3; i) { a[i] (function (num) { return function () { console.log(num); }; })(i); }这类题本身不难但它后面的引申很关键。比如考你如果循环体内部有异步操作比如setTimeout那输出顺序会怎样变化或者考你let声明的变量在块级作用域里到底创建了几个独立绑定。在JavaScript中let的块级作用域会让每一次循环都创建一个新的词法环境所以闭包捕获到的i是每一轮自己的值。理解这个比背答案有用得多。2.3 缓存与跨域从场景判断而不是背概念浏览器缓存也是贝壳选择题的常客。但贝壳很少让你直接默写什么是强缓存、什么是协商缓存而是给你一个实际场景让你判断。比如某网站静态资源在文件名上带上了内容hash如app.8f3a9b.js那么部署时最适合使用什么缓存策略正确答案是对这类带hash的静态资源应该使用强缓存设置较长的Cache-Control: max-age例如一年。因为内容变了文件名就会变浏览器不会拿到旧的文件名不变就说明内容没变可以直接用浏览器本地缓存省去HTTP请求。反过来如果资源文件名不带hash就不能设置过长的强缓存否则更新了代码用户还是老页面这时候更适合用协商缓存通过ETag或Last-Modified在每次请求时向后端校验资源没变化时返回304浏览器复用本地缓存。我把这两种策略对比一下资源类型推荐缓存策略原因带hash的静态资源强缓存Cache-Control: max-age31536000内容变了文件名变不存在旧文件命中问题不带hash的资源协商缓存ETag / Last-Modified需要每次校验资源是否更新避免拿到旧内容入口HTML文档不缓存或协商缓存保证用户能尽快拿到最新的页面引用另外Shell也比较喜欢考跨域。不能只背JSONP、CORS、代理这几个词要能说清JSONP原理是动态插入script标签、只能支持GET请求CORS需要服务端返回Access-Control-Allow-Origin等响应头浏览器才会放行开发环境常用的webpack devServer代理则是通过服务端转发来规避浏览器的同源限制。2.4 框架原理Vue3的Proxy和React的key框架相关的题目占比不小。贝壳会考Vue3的响应式是基于什么实现的答案是Proxy而不是Vue2的Object.defineProperty。更进一步它可能会问Proxy相比Object.defineProperty的优势在哪里。这个问题不能只答性能更好要说清楚三点第一Proxy可以直接监听对象新增属性和删除属性第二Proxy可以监听数组索引和length的变化不需要像Vue2那样重写数组方法第三Vue2初始化时需要对data做深度递归遍历用Object.defineProperty逐个劫持而Proxy是代理整个对象访问到某个属性时才惰性递归初始化性能更好。React相关的题关键是key。在列表渲染时给每个元素设置一个稳定且唯一的key本质是帮助diff算法识别节点是否被复用。很多人觉得用index做key也没问题反正能跑但在列表项有顺序变化、插入删除时用index会导致节点的状态错乱比如输入框内容串行、图片加载出bug。笔试里它会给你一段代码让你判断为什么在某些场景下使用index作为key会出现问题这种题就是要考察你是否有真实项目的渲染优化经验而不是只在文档里读过。选择部分整体感受是考得广但不偏每个题都是前端日常工作中实际关心的点。只要基础扎实拿高分并不难。3. 编程题第一关字符串统计与数组扁平化的边界教训3.1 字符串频次排序sort的比较函数不是可选项编程题里有一道非常典型的题目大意是给定一个字符串统计每个字符出现的次数并按出现次数从高到低排序输出如果次数相同按字符的ASCII码升序。这道题看起来很简单很多人的第一版代码长这样function frequencySort(s) { const map new Map(); for (const ch of s) { map.set(ch, (map.get(ch) || 0) 1); } const arr [...map.entries()]; arr.sort((a, b) b[1] - a[1]); return arr.map(([ch, count]) ch.repeat(count)).join(); }如果把提交后再自测几个用例就会发现当两个字符出现次数相同时输出顺序不符合ASCII升序要求。原因是上面的sort((a,b) b[1] - a[1])只按次数排序次数相同的情况下sort的稳定排序会让它们保持原本在Map中的插入顺序而这个插入顺序是字符串中第一次出现字符的顺序不是ASCII码顺序。题目明确要求ASCII升序所以要再加一个比较条件arr.sort((a, b) { if (a[1] ! b[1]) { return b[1] - a[1]; // 次数降序 } return a[0].charCodeAt(0) - b[0].charCodeAt(0); // ASCII升序 });另一个容易踩的坑是Array.prototype.sort默认比较方式是把元素转为字符串后按字典序排列不是按数值升序。如果不传比较函数直接用sort对数字数组排序结果会完全不符合直觉。这个知识点很多人平时没注意但笔试时一紧张很容易犯。3.2 数组扁平化去重递归写法与Set的局限还有一道数组相关题目大意是多维数组展开成一维数组再去重。要求不能用Array.prototype.flat甚至可能要求不能用Set。如果允许用Set递归写法非常清晰function flattenAndUnique(arr) { const result []; const seen new Set(); const dfs (val) { if (Array.isArray(val)) { val.forEach(dfs); } else if (!seen.has(val)) { seen.add(val); result.push(val); } }; arr.forEach(dfs); return result; }这里有一个非常隐蔽的问题Set判断引用类型时是按引用地址去重不是按值去重。如果数组里有对象比如[{ a: 1 }, { a: 1 }]用Set去重后两个对象都会被保留因为它们的内存引用不同。如果题目要求对象按内容去重就必须做深比较比如用JSON.stringify序列化后作为Set的key但这样又会有key顺序或函数丢失的问题。笔试中如果遇到带对象的去重需要多想一层这也是这类题真正拉开差距的地方。3.3 为什么简单题通过率反而不高从周围的反馈看第一道编程题的通过率并不高。原因不是解法难而是环境、时间、边界三座大山压在一起。在线笔试平台的JavaScript环境有两个特点第一输入输出要走标准输入输出不是浏览器里的alert和console第二Node.js环境下输入被分成一行一行读取需要自己用readline或者process.stdin处理。很多人在本地IDE里写得很溜一旦切到平台的代码编辑器光标一闪烁思路就乱了。应对办法是在笔试开始前把输入输出模板先写好。比如赛码平台常用的Node.js模板const readline require(readline); const rl readline.createInterface({ input: process.stdin, output: process.stdout, }); const lines []; rl.on(line, (line) { lines.push(line.trim()); }); rl.on(close, () { // 在这里编写业务逻辑lines就是所有输入行 const s lines[0]; console.log(frequencySort(s)); });边界条件也不能忽视。字符串为空、全是同一个字符、字符是空格、包含数字和大小写字母这些用例都要自己先跑一遍。一个常见低级错误是字符串里有空格而输入读取时用了.trim()把整行空格去掉了导致空格的统计结果不对。如果题目明确说字符包括空格就应该用line.split()这样的方式保留原始内容而不是统一trim掉。4. 编程题第二关带约束的DP和手写防抖背后的竞态处理4.1 从跳台阶到状态机不能连续跳两格的DP编程题里出现了一道让我卡了挺久的动态规划题。题目大意我尽量复述给定一个长度为n的台阶每次可以跳1格或2格但有一个额外约束——不能连续两次都跳2格问到达第n级台阶有多少种不同的跳法。第一眼看上去大家都觉得是斐波那契数列的变形但不能连续两次跳2格这个约束让单一状态数组没办法处理。因为当我们站在第i级台阶时能不能选择跳2格这一步取决于上一次是不是通过跳2格上来的。也就是说上一步的信息会影响当前的选择这是一个典型的状态机问题。正确的解法是用二维DP把到达第i级且最后一步是跳几格分开记录。定义dp[i][0]到达第i级台阶最后一步是跳1格这样的方案数。dp[i][1]到达第i级台阶最后一步是跳2格这样的方案数。转移也很直观如果最后一步是跳1格那么到达它的前一个状态可以是跳1格也可以是跳2格所以dp[i][0] dp[i-1][0] dp[i-1][1]。如果最后一步是跳2格那么前一步绝对不能是跳2格所以只能从i-2且最后一步是跳1格的状态过来即dp[i][1] dp[i-2][0]。初始条件dp[0][0] 1表示在起点有一种什么都没做的方案dp[1][0] 1表示从第0级跳1格到第1级dp[1][1] 0显然跳到第1级不可能最后一步跳2格。然后从2开始迭代即可。这里想多说一句这个DP模型放到前端开发里并不是学了也没用。想想一个交互组件的状态迁移比如未开始/加载中/成功/失败/空数据这几个状态之间有些迁移是允许的有些是不允许的写代码时如果不把这种约束显式建模就会出现状态错乱。算法题训练的其实就是这种把业务约束转成状态转移的能力而不是纯粹为了考试。4.2 业务场景题防抖加竞态序号还有一道业务场景编程题我印象很深。题目大意是搜索框输入时需要每300ms防抖发送一次请求同时要保证请求返回后不会把旧请求的结果渲染到界面上。要求手写实现。大部分人的第一反应是写一个防抖函数function debounce(fn, delay) { let timer null; return function (...args) { if (timer) { clearTimeout(timer); } timer setTimeout(() { fn.apply(this, args); }, delay); }; }写到这里这道题只能算完成了一半。防抖可以保证请求不会频繁发出但它不能保证响应顺序不变动。用户输入贝壳先发了?keyword贝的请求再发?keyword贝壳的请求但网络环境复杂先发的那个请求可能后返回如果后返回的旧响应直接渲染到界面上就会出现搜索贝壳却显示了贝的结果的经典竞态问题。解决方案是给每次请求加一个序号只有当前序号等于最新序号时才渲染数据let seq 0; async function search(keyword) { const current seq; const data await fetchData(keyword); if (current seq) { render(data); } }这段代码里的current seq是核心。它的意思就是只有当我这次请求仍然是最新的一次请求时结果才允许登场。竞态问题在实际项目中非常常见不只是搜索引擎还包括列表分页、下拉刷新、编辑保存等场景。这道题之所以值得写是因为它考的不仅是防抖更是工程中一个真实的痛难点。4.3 笔试编程题的取舍先AC哪些编程题部分如果总共有4道理想的策略是用最短时间找出你最有把握的两道先把它们AC然后回头啃剩下的。时间分配上每道题给自己设定一个上限我在实战中用的是10分钟超过10分钟还没有明确思路就跳到下一道。不要觉得可惜你能拿到的分数才是真的分数卡在难题上反而会导致后面的开放题空着那才是更大的损失。另外写代码时如果一道题实在写不出来也要把核心思路以注释的形式写在答题区。在很多公司的笔试评分体系中通过部分用例也有分思路备注能帮助面试官在简历筛选阶段对你建立一个更好的印象。5. 开放设计题如何从做题家切换成工程师5.1 一个典型的开放题错误监控SDK设计贝壳的开放设计题是这样的风格给你一个实际的前端工程问题不要求写完整代码而是请你给出设计方案。我遇到的是请设计一个前端错误监控SDK说明你会采集哪些信息、如何上报、如何减少对业务性能的影响。这类题对于只刷算法题的人来说会有点懵因为八股文里没有现成答案。但对真正做过线上项目、用过Sentry、听过前端监控方案的候选人来说是很有话说的。所以它考察的本质是你有没有真正思考过线上质量保障。我的作答思路分四个层面第一采集什么信息。JS运行时错误用window.onerror监听未捕获的Promise异常用unhandledrejection监听。资源加载错误需要在window.addEventListener(error, fn, true)的捕获阶段处理因为资源加载错误不会冒泡。接口请求异常则可以在axios拦截器或fetch封装层统一捕获记录状态码、请求耗时、请求参数。除了错误本身还要采集发生错误的页面URL、用户操作路径、浏览器和系统信息、以及上下文的关键数据方便开发者复现问题。第二用什么方式上报。优先推荐navigator.sendBeacon它的特点是即使页面正在关闭浏览器也会尽力把数据发送出去非常适合统计日志和监控数据。也可以用new Image().src url的方式做图片打点但注意URL长度有限制日志内容多时会被截断。普通情况下的日志上报循环发送太多请求还会挤压正常业务请求所以往往需要一个批量上报队列把一段时间内的多条错误合并成一次上报。第三如何减少性能影响。监控SDK不能成为业务卡顿的源头做业务的人最反感这一点。一般的要求是SDK异步加载不阻塞首屏渲染上报数据时对错误堆栈做截断控制单条日志大小错误记录先经过一个缓存队列批量发送避免大量并发请求还可以通过采样率控制上报量比如线上只采样10%的用户保证在不影响性能的前提下获得足够的错误样本。第四如何保证监控自身不出问题。SDK本身要容错比如捕获逻辑被业务代码覆盖了、某些跨域脚本拿不到详细堆栈、旧浏览器不支持sendBeacon等都要有降级方案。这层思考往往是加分项能体现出工程经验的深度。5.2 作答框架先定性再展开最后给验证方案开放设计题最怕把答案写成一句话。比如给window加个error监听然后上报到服务器这种回答等于没答因为它和工程师的思考方式完全不匹配。我的框架是先明确设计目标再列出关键模块每个模块展开讲一到两个实现细节最后补上验证和监控手段。比如错误监控SDK的目标是快、全、稳、瘦——快速采集、尽量全面、自身稳定、对业务的影响小。在这个目标框架下展开逻辑就会非常清晰。在具体的答题过程中还可以补充一个验证方案如何测试SDK是否真的生效。比如写一个demo页面手动触发一个未捕获异常看控制台的数据请求是否发出模拟低网速环境观察SDK是否影响页面交互使用Lighthouse跑一下性能分数对比接入前后有没有明显下降。这些内容不需要你真的做过但作为工程师的正常思考路径写出来是合理的。5.3 开放题拿部分分的三个原则第一绝不空题。哪怕是只有5分钟把采集、上报、性能三个大字写出来也能让阅卷人知道你有基本概念。第二写分点和分层用第一/第二/第三或者1/2/3组织内容不写一坨没有结构的段落。第三不要写自己都不确定的技术细节答错比不答扣分更多。比如你忘了sendBeacon的兼容性就别强行编可以说如果是老旧浏览器使用Image打点降级。这类题的评分方式不完全看结论更看思路是否完整、有没有考虑到真实业务场景这是这道题能筛选出有过真实项目经验的人的原因。6. 复盘与看见这套题背后藏着的前端能力预期6.1 我这次填错的三个地方交卷后我第一时间在草稿纸上复盘了整场笔试记录了自己几个明确的失误点。第一个失误在第一道编程题上我把输入行做了trim结果题目里要求统计的字符串包含了空格导致空格这个字符的频次统计错了。这类教训其实很基础笔试中任何对输入的好心清洗都要先想清楚题目要求。第二个失误出在事件循环的选择题上我在多个微任务嵌套时犹豫了太久。当时那道题已经把Promise和async/await层层嵌套我虽然知道微任务先于宏任务但在微任务队列内部的顺序上花了几分钟才理清。如果平时训练多一点就完全不会卡壳。第三个失误是DP题的状态定义花了我太多时间。我第一反应是直接用一维dp基于斐波那契的思路往下走走了三四分钟才发现不能连续跳2格这个约束无法在一维状态里表达。后来从最后一步是跳几格这个角度去想才找到二维状态的定义方式。平时写动态规划时养成先想清楚状态维度再想转移方程的习惯能省很多时间。6.2 从笔试信号看秋招前端考察趋势回头来看贝壳这套题其实透露出很多信息。第一个信号是前端笔试正在从纯八股算法向八股算法工程场景转变。防抖加竞态、错误监控SDK这类题目已经不是在考记忆而是在看候选人有没有真正的项目管理思维和线上问题排查意识。第二个信号是动态规划这类经典的算法题依然有重要位置但考点越来越偏向约束条件下的状态设计比如不能连续跳两格。这类题目考察的是抽象建模能力同类型的还有打家劫舍不能连续偷两家买卖股票有冷冻期等刷题时应该重点掌握这种状态机的建模方式。第三个信号是框架、原理、浏览器缓存不再以死板的背诵形式出现而是给了更多场景要求你基于实际条件做判断。这意味着平时的学习不能只靠背面试题一定要动手写代码、做项目、调试线上问题积累真正的体感。6.3 给后续批次同学的备赛清单根据这次笔试的复盘我整理了一份可以作为参考的备赛清单事件循环输出题。把宏任务、微任务、Promise、async/await的代码执行顺序练到形成肌肉记忆特别是多层嵌套的微任务顺序。闭包与作用域。循环闭包、变量提升、let/var的区别会写会解释。浏览器缓存。强缓存、协商缓存、启发式缓存以及针对不同资源的实际策略选择。Vue3与React。响应式原理、key的作用、diff算法、组件生命周期要能讲出为什么。LeetCode hot100。中等难度的字符串、双指针、栈、队列、简单DP优先难题学会及时放弃。前端场景题。防抖节流、深拷贝、数组扁平化、事件总线、带并发限制的请求队列不仅要会写还要能说出边界问题和改进方案。这份清单并不仅针对贝壳对绝大部分互联网公司前端岗位的秋招笔试都是适用的。笔试结束那一刻我心里其实挺没底的觉得自己好几处都犯了低级错误。但复盘完才发现一场笔试真正的价值不是那个分数而是它像一面镜子把你在知识体系和工程思维上的薄弱点照得清清楚楚。我后来顺利进入了面试环节回头看仍然觉得这套笔试题出得很有水平——它没有为难人也没有放水而是老老实实考察了一个前端工程师日常学习中应该积累的东西。如果你也正在准备前端秋招希望这篇复盘能帮你少踩几个坑在考场上把该拿的分数都稳稳拿下。