ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

贝壳找房前端笔试题深度拆解:考点、原理与避坑指南

贝壳找房前端笔试题深度拆解:考点、原理与避坑指南 去年春招我把贝壳找房的这套前端笔试卷完整刷了一遍来回做了三遍才把考点吃透。作为一个在房产行业写过几年前端的老兵看到这套题的第一反应是出题人是真的懂业务不光是考 API 记忆而是考你在真实环境下会不会踩坑。这篇内容不打算做标准答案复读机而是以亲身经历把每道题的考点、出题人意图、以及我踩过的坑都拆开来讲一遍。不管你是不久后要上战场的应届生还是想查漏补缺的初中级前端这份拆解应该都比单纯刷一套题更有参考价值。我按真实笔试卷的题目类型重新组织了一遍把同类的考点并在一起讲这样你复习的时候思路更清楚。1. 整卷结构与出题思路解读1.1 题型分布与分值逻辑春招笔试题一般在线作答限时 90 到 120 分钟。贝壳这套卷子整体题型分布很典型单选、多选、填空题占了大概 40% 的分数剩下的大部分是手写代码题最后有一道相对完整的编程题。这个分布其实很有讲究单选多选考察的是知识面的广度手写题考察的是动手能力的下限编程题则是在筛选思维深度。很多人拿到卷子习惯按顺序从第一题做到最后一题这是个很大的误区。笔试卷子不是为了让你按顺序做完而是为了在有限时间内把你最大的亮点暴露出来。我建议先花两分钟把整张卷子扫一遍简单标注每道题预计耗时然后先写手写题因为手写题分值高而且最容易拉开差距。选择和填空如果卡住了先空着跳过去回头再用排除法处理。1.2 这份试卷到底想筛选什么样的人通过题目能反推出贝壳前端团队在意什么。房产行业的业务场景有一个特点数据量大、列表渲染频繁、地图交互复杂、房源筛选条件多。所以卷子里出现了不少关于性能优化、组件通信、数据处理的内容这都不是白出的题。注意卷子里几乎没有那种“背诵框架生命周期”的死题更多是把生命周期和实际业务场景结合起来问。比如“某个房源列表组件在路由切换时为什么会内存泄漏”这种问法就是在考察你有没有真正写过大型业务页面而不是只在 demo 项目里跑过路由。如果你平时只写过组件库练习对这种题会非常陌生整道题的思路就会跑偏。从出题逻辑来看这份卷子在优选的并不是背题高手而是能真正理解前端运行机制、能独立解决业务问题的人。所以后面我讲的考点拆解也会重点补充“背后的原理”让你知其然也知其所以然。2. JavaScript 基础从变量、作用域到事件循环2.1 作用域与闭包的经典考法这套卷子的选择题里必然有一道作用域的题目我当时遇到的是问以下代码的输出结果var a 10; function foo() { console.log(a); var a 20; } foo();答案是undefined不是 10也不是 20。原因是函数作用域内的var a会被提升到函数顶部也就是所谓的“变量提升”但在赋值之前打印所以结果是undefined。这道题考的就是你对提升机制的理解而不是简单背结论。闭包的题一般不会直接问你“什么是闭包”而是给一段代码让你判断输出。常见套路是循环里用var绑定事件结合异步回调考察。我在笔试卷上遇到一题是for (var i 0; i 5; i) { setTimeout(function() { console.log(i); }, 100); }如果你填的是0 1 2 3 4那这里就要扣分了。答案是五个5。原因本质上还是作用域的问题var声明的i是全局作用域循环结束后i已经是 5setTimeout 回调执行时拿到的都是同一个i。2.2 Event Loop 与异步编程的必考题这套卷子里的异步题出得很有水平考的是宏任务和微任务的执行顺序。类似的代码是这样的console.log(1); setTimeout(() console.log(2), 0); Promise.resolve().then(() console.log(3)); console.log(4);输出顺序是1 4 3 2。这里的核心机制是同步代码先执行微任务队列在宏任务之前被清空Promise.then属于微任务setTimeout属于宏任务所以3先于2输出。实际做题时我会先把整段代码里的微任务和宏任务都列出来标注各自所属的任务队列再按轮次执行。这个方法在遇到嵌套 Promise 时特别有用因为一旦任务层层嵌套肉眼判断很容易出错。再往后题里可能还会出现async/await版本。来看一个很典型的坑async function test() { console.log(1); await Promise.resolve(); console.log(2); } console.log(3); test(); console.log(4);这里1和3的顺序容易搞反。注意test()调用后函数体是同步执行的所以先输出3然后进入函数输出1遇到await让出执行权继续输出4最后Promise.resolve()的微任务回调把2输出。因此完整顺序是3 1 4 2。2.3 this 指向问题怎么快速判断笔试里 this 指向前前后后出现了三次这也是覆盖度很高的考点。我提供一个自己用的判断方法只看函数调用位置而不是定义位置。var obj { name: beike, getName: function() { return this.name; } }; var fn obj.getName; console.log(fn());这段代码调用fn()时调用者是全局对象非严格模式下 this 指向 windowwindow.name通常是空字符串或者 undefined所以输出不会是你以为的beike。严格模式下会直接报错。箭头函数则是另一种玩法它没有自己的 this只会继承外层作用域的 this。碰到箭头函数的题直接看它定义时所处的外层函数即可。如果外层是普通函数那 this 就是那个普通函数执行时的 this如果外层没有普通函数那 this 就是全局对象。掌握这个判断链箭头函数相关题基本不会错。3. CSS 与浏览器渲染布局题和性能题不能丢分3.1 布局题Flex、Grid 与双栏圣杯布局手写 CSS 布局几乎是必考题。贝壳这套卷子有一道“左侧固定右侧自适应”的经典布局题。最常见的答案是用 Flex.container { display: flex; } .left { width: 300px; flex-shrink: 0; } .right { flex: 1; min-width: 0; }这里有个坑很多新手忘了给左侧加flex-shrink: 0。在容器宽度不够时左侧会被压缩布局直接变形。右侧加min-width: 0是为了解决 flex 子项默认min-width: auto导致的文本溢出问题这个细节不仅是面试加分项也是平常开发中的关键。除了第一种方法还需要掌握 Grid 写法.container { display: grid; grid-template-columns: 300px 1fr; }Grid 在处理这种两栏布局时不需要额外处理 flex-shrink 问题代码更简洁。但 Grid 的兼容性和 flex 略有差异项目里要结合兼容要求来选。还有更古老的圣杯布局、双飞翼布局虽然现在用 float 实现的频率不高但选择题里依然会考它们的原理。3.2 浏览器渲染机制与性能优化渲染机制考得比较多的点是“重排重绘”和“合成”。题目会给你一个操作让你判断它是触发 reflow 还是 repaint。比如修改width会触发重排修改color只触发重绘而使用transform做动画则可以跳过布局和绘制阶段直接进入合成。平时开发中也应该养成一个习惯把频繁读取和修改 DOM 的操作分开使用requestAnimationFrame批量处理样式变更而不是同步地反复读写。笔试里那道有关性能优化的简答题答到“减少重排、避免强制同步布局、合理使用 transform 和 opacity”这几个核心点就足够说明问题了。从房产类应用的实际场景看房源列表的长列表滚动非常考验渲染性能。如果这道题能顺带提到虚拟滚动思路考官的认可度会明显不一样。我在答题时就补了一句“大数据量场景通过只渲染可视区域来降低 DOM 节点数量”这比单纯罗列重排重绘概念要更有吸引力。4. 手写编程题防抖节流、深拷贝与 Promise4.1 手写防抖与节流注意边界情况手写防抖节流出现在手写题第一题属于送分但也不能丢分的题。防抖的核心是“每次触发都重置定时器”节流的核心是“固定时间间隔内只执行一次”。function debounce(fn, delay) { let timer null; return function(...args) { clearTimeout(timer); timer setTimeout(() fn.apply(this, args), delay); }; }注意这里用了fn.apply(this, args)目的是保留调用时的 this 指向。很多人写防抖时直接用fn(...args)会导致 this 丢失这在事件处理函数里会出问题。节流我通常用时间戳或定时器实现function throttle(fn, interval) { let last 0; return function(...args) { const now Date.now(); if (now - last interval) { last now; fn.apply(this, args); } }; }这种写法会“忽略”定时器到期之前最后几次触发。还有一种带 trailing 的节流写法能保证最后一次触发也被执行适合做搜索框输入的场景。写完这两个函数后我都补了一句话说明使用场景防抖用于 input 输入、窗口 resize节流用于滚动监听、按钮点击限频。一句话就能让改卷的人知道你理解了两者的区别。4.2 深拷贝的手写实现与边界处理深拷贝属于高频手写题但这道题想拿满分不容易。基础版本大家都能写function deepClone(obj) { if (obj null || typeof obj ! object) return obj; const result Array.isArray(obj) ? [] : {}; for (let key in obj) { if (obj.hasOwnProperty(key)) { result[key] deepClone(obj[key]); } } return result; }这个版本的问题在于没有处理循环引用。假设对象里有一个属性指向自身递归会无限循环最终导致栈溢出。解决方法是利用 WeakMap 记录已经拷贝过的对象function deepClone(obj, map new WeakMap()) { if (obj null || typeof obj ! object) return obj; if (map.has(obj)) return map.get(obj); const result Array.isArray(obj) ? [] : {}; map.set(obj, result); for (let key in obj) { if (obj.hasOwnProperty(key)) { result[key] deepClone(obj[key], map); } } return result; }如果对象里还有Date、RegExp、Map、Set基础逻辑就不够用了。我的做法是先把这些特殊类型单独判断再走通用递归。作为笔试题只要能把循环引用处理好再提一句“生产环境建议用 structuredClone 或 lodash 的 cloneDeep”就已经是完整的回答。4.3 手写一个符合规范的 Promise这套卷子的编程压轴里有一版是让实现一个简易 Promise。别慌其实只需要满足 then 的链式调用和状态转换就够了。我抽核心结构写class MyPromise { constructor(executor) { this.state pending; this.value undefined; this.reason undefined; this.onFulfilled []; this.onRejected []; const resolve (value) { if (this.state pending) { this.state fulfilled; this.value value; this.onFulfilled.forEach(fn fn()); } }; const reject (reason) { if (this.state pending) { this.state rejected; this.reason reason; this.onRejected.forEach(fn fn()); } }; try { executor(resolve, reject); } catch (e) { reject(e); } } then(onFulfilled, onRejected) { onFulfilled typeof onFulfilled function ? onFulfilled : v v; onRejected typeof onRejected function ? onRejected : r { throw r; }; const promise2 new MyPromise((resolve, reject) { if (this.state fulfilled) { setTimeout(() { try { const x onFulfilled(this.value); resolvePromise(promise2, x, resolve, reject); } catch (e) { reject(e); } }); } if (this.state rejected) { setTimeout(() { try { const x onRejected(this.reason); resolvePromise(promise2, x, resolve, reject); } catch (e) { reject(e); } }); } if (this.state pending) { this.onFulfilled.push(() { setTimeout(() { try { const x onFulfilled(this.value); resolvePromise(promise2, x, resolve, reject); } catch (e) { reject(e); } }); }); this.onRejected.push(() { setTimeout(() { try { const x onRejected(this.reason); resolvePromise(promise2, x, resolve, reject); } catch (e) { reject(e); } }); }); } }); return promise2; } }还要写一个resolvePromise函数来处理 then 回调里返回的普通值和 Promisefunction resolvePromise(promise2, x, resolve, reject) { if (promise2 x) { reject(new TypeError(Chaining cycle)); return; } if (x instanceof MyPromise) { x.then(resolve, reject); } else { resolve(x); } }当我笔试时把这段核心逻辑写出来后这道题基本就稳了。面试官真正想看的不是你能不能默写完整版而是你懂不懂promise2为什么要等x处理完才能 resolve、为什么回调要放进微任务队列里。只要把这两点讲明白了得分就上去了。5. 框架与工程化考察Vue3/React 的常见追问5.1 Vue3 vs React笔试常出的对比题贝壳业务里 Vue 用得比较多但笔试卷上不会直接站队往往会通过场景题来考察框架的底层理解。比如问“Vue3 的响应式原理和 Vue2 有什么区别”。答法不是简单说 Proxy 比 defineProperty 强而是要说清楚 defineProperty 需要提前遍历对象属性、新增属性时需要用 Vue.set 才能响应、数组的部分方法无法自动侦测而 Proxy 直接代理整个对象新增删除属性都能自动追踪。React 相关的题目则容易聚焦在 hooks 上。比如常见的“为什么不能在循环里调用 hook”。根本原因是 hooks 的调用顺序依赖链表的顺序存储每次渲染时 hook 必须按同样的顺序被调用否则 React 无法把当前 hook 的状态跟上一次对应上。这个机制决定了setState结果和页面表现会出现不可预期的错乱。笔试中如果遇到这种对比题我会遵循“先讲底层机制再讲业务影响”的结构来答。比如 Vue 的响应式是数据驱动的依赖追踪React 更偏向函数式不可变数据加重新渲染。最后落到业务里Vue 适合快速开发、细粒度更新React 适合团队约束较重的复杂应用。这样的答案有结构、有观点比上来就背优缺点更容易让改卷人记住。5.2 工程化与打包工具Webpack/Vite 核心概念这套卷子里的工程化题不算难但覆盖面广loader 和 plugin 的区别、sourceMap 的作用、tree-shaking 的前提、Webpack 打包流程等。loader 和 plugin 的区别是高频题。loader 是一个转换器负责把模块源码做转换比如把 TypeScript 转成 JavaScript、把 SCSS 转成 CSS。plugin 的范围更广从打包优化、资源管理到注入环境变量都由插件完成本质是在 Webpack 生命周期中挂载函数、干预构建过程。答题时举一个例子就能说明白babel-loader是 loaderHtmlWebpackPlugin是 plugin。tree-shaking 的题通常会给一个 ES Module 的代码片段问打包后哪些会被移除。底层原理是基于 ES Module 的静态结构分析通过标记未使用的 export 来删除冗余代码而 CommonJS 的 require 是动态的没法安全地做静态分析所以 tree-shaking 在 CommonJS 模块下基本失效。这个点单独拿来讲往往就是那个拉开差距的加分项。Vite 相关的题如果出现一定会考“为什么 Vite 开发环境比 Webpack 快”。核心是 Vite 在开发环境利用浏览器原生 ES Module 按需加载只有真正 import 到的模块才被编译不再像 Webpack 那样启动时对全部模块做打包处理。生产环境构建时 Vite 底层又使用 Rollup所以它并不是“基于原生 ES Module 就万事大吉”。答题时把这个弯绕过来会让面试官觉得你不仅知道特点还知道它的边界。6. 算法题与模拟题字符串处理与数组操作6.1 字符串类高频题与双指针思路编程题里出现了一道“最长无重复字符子串”的变种题。这种题的核心解法是滑动窗口加哈希表。以abcabcbb为例left 指针维护当前无重复子串的左边界right 指针一直右移用 map 记录每个字符最近出现的下标。当遇到重复字符时把 left 跳到重复字符上一次出现的下标加一的位置之后不断计算窗口长度。function lengthOfLongestSubstring(s) { let left 0; let max 0; const map new Map(); for (let right 0; right s.length; right) { const char s[right]; if (map.has(char)) { left Math.max(left, map.get(char) 1); } map.set(char, right); max Math.max(max, right - left 1); } return max; }关键在于left Math.max(left, map.get(char) 1)而不是直接使用map.get(char) 1。这是为了防止老的重复字符把 left 拉回去导致窗口出现负长度。很多人这个细节没处理虽然测试用例能过但边界用例会挂掉。6.2 数组去重与排序的边界情况除了滑动窗口题还有一道偏实战的数组去重题。它比常规的new Set(arr)多了一层要求比如“把 NaN 也正确去重”。直接用 Set 处理NaN是没问题的因为 Set 内部的 SameValueZero 算法把 NaN 视为相等的值。但如果用手写的indexOf方案就会因为indexOf使用的是严格相等导致 NaN 永远无法被找到去重失效。还有一个常见的边界是对象数组按某个属性去重。比如房源列表里按房源 ID 去重const list [{ id: 1, name: a }, { id: 1, name: b }, { id: 2, name: c }]; const map new Map(); list.forEach(item map.set(item.id, item)); const uniqueList [...map.values()];这种写法比双重循环排序然后去重高效得多而且代码可读性更好。算法题未必都考复杂数据结构能把 Map 和 Set 的运用落实清楚已经能拿到不错的分数。排序方面如果题目不要求手写快排我建议直接用Array.prototype.sort但要注意 sort 默认会将元素转换为字符串再排序这点是经典坑数字数组必须传比较函数。7. 常见问题与避坑指南7.1 笔试中最容易翻车的几个点第一选择题里的多选和单选混在一起很多人没看清题目要求把单选当多选答或者反过来漏选。我的习惯是把题目中的“单选/多选”字眼先圈出来再开始作答。尤其在在线平台上题型标记有时候不够显眼需要在开始前点一遍“预览模式”确认。第二环境变量问题。笔试卷里经常给一段前端代码要求判断输出结果但没写明是浏览器环境还是 Node 环境。遇到window、globalThis、self这种全局对象时答题前要先判断环境。我在写 this 相关题目时踩过一次坑按浏览器环境思考后看到题目末尾附了一句“Node.js 环境运行”答案全反了。所以拿到题目先扫环境说明再动手。第三手写代码时没有注意函数的边界条件。很多人在笔试时只写了主逻辑空值判断完全没写。比如防抖函数里没有考虑 this 指向深拷贝没有判断 nullPromise 构造函数里执行 executor 没有包 try-catch。这些小细节单独看似乎不重要但改卷人扫一眼就能区分谁是背题的谁是真正写过渡的。7.2 牛客平台答题的实操技巧贝壳这套卷子我在牛客上刷的这类在线平台有一些共性技巧。代码题部分通常需要从标准输入里读取数据没有自动生成的输入模板时很多人会卡住。我建议熟悉一下固定的输入解析套路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 });在浏览器环境下则可能要用readline()或内置的输入 API不同平台不一样。考前先在牛客的模拟环境里跑一道测试题确认输入输出模板能通过再开始写正式题目。这个准备工作最大作用不是在输入输出语法上省时间而是让你在真正紧张的答题节奏里少一个未知项。牛客平台面试题还有一个特点代码保存后可以进行本地调试但要注意调试输出和最终输出必须分开。我在做编程题时习惯用console.log打点调试如果提交前忘记删掉调试日志最终输出会被多出来的行污染导致判题直接判错。正确做法是调试完把所有 console.log 清干净再用样例数据自测一遍提交。7.3 时间分配与答题节奏90 分钟的卷子我自己的节奏是前 15 分钟做完全部选择和填空遇到耗时的题直接标记跳过绝不恋战。紧接着用 40 分钟做手写代码题优先写思路最清晰的防抖节流和深拷贝最后 25 分钟留给编程题和检查。检查时重点看代码题有没有语法错误选择题有没有看错题型。笔试最大的敌人不是题难而是时间分配失衡。我见过不少同学在一个选项题上纠结 10 分钟导致后面 20 分的手写题草草交卷。性价比极低。选择题再难也就 2 分手写题一旦空白就是整块丢分这个账一定要算清楚。最后再分享一个我个人经验笔试前一天不要刷新题把所有经典手写题的核心逻辑在纸上默写一遍包括防抖节流、深拷贝、Promise、数组去重、快排。默写不是背代码而是把这些代码里的关键点像“防抖要传 this”、“深拷贝要处理循环引用”这种边角细节过一遍。考试时候你会发现真正拉开差距的往往不是“会不会写主逻辑”而是这些边界细节有没有照顾到。
返回列表