公司动态

前端春招笔试题拆解:从事件循环到Vue响应式与微前端

📅 2026/9/1 7:22:54
前端春招笔试题拆解:从事件循环到Vue响应式与微前端
前阵子一个学弟发来一份 2023 贝壳找房春招前端工程师笔试卷2 的回忆版问我这套题现在还有没有刷的必要。我的态度很明确太有必要了。贝壳的前端笔试题在互联网公司里不算最难但考察范围特别典型基本把前端工程师日常要用的核心知识都串了一遍——事件循环、闭包、类型转换、手写 Promise、Vue 响应式原理、微前端、长列表优化全都能在卷子里找到对应题。对准备春招的学生来说它是很好的“摸底考试”对已经工作一两年的前端来说它也能帮你快速发现自己哪些基础已经开始生疏。下面我就按考点把完整拆一遍不会只给答案重点讲为什么这么考以及怎么答才能拿高分。1. 这份笔试卷的整体画像题型分布与考察逻辑1.1 试卷构成单选、多选、填空题与两道编程题从整理的回忆版来看贝壳找房 2023 春招前端工程师笔试卷 2 的题量不算少总时长 90 分钟大致结构如下表具体题量和分值以实际卷面为准模块题量建议用时主要考察方向单选20 题左右20 分钟基础语法、浏览器、HTTP、安全多选10 题左右15 分钟概念辨析、边界场景填空/简答5 题左右15 分钟输出结果、代码补全手写代码2 题25 分钟防抖节流、Promise、深拷贝等场景设计1 题15 分钟复杂列表、权限、性能优化这样设计其实是合理的前 50 分钟考察基础后 40 分钟考察“你会不会真正干活”。很多同学前边慢慢悠悠结果编程题只剩 10 分钟这是最可惜的。客观题里即使有不会的先标记跳过把会做的拿稳再回头啃难题分数一定比死磕一道题高。1.2 为什么贝壳的笔试题值得单独做一遍贝壳的业务以房产交易平台为主前端会碰到很多特征明显的问题房源列表数据量大、筛选条件复杂、地图与列表联动、IM 实时沟通、权限繁多的后台管理系统。这些业务特点会直接投射到笔试题里。所以贝壳不像有些公司那样只考算法题它更在意你对“前端在真实业务中怎么解决问题”的理解。举一个例子同样是考权限问题普通公司可能问“路由守卫怎么写”贝壳更可能问“一个房产经纪人后台不同角色看到不同菜单和不同按钮你怎么设计”。后者明显更贴近真实项目。正因为这样这套笔试卷对准备春招的同学有两个价值第一它能帮你把“前端八股文”里最核心的内容筛出来避免无脑背题第二它里面的场景题往往贴近真实业务比如动态路由、长列表优化、大文件上传这些题在面试中也会以项目问题的形式再问一遍。刷一套题等于同时准备了笔试和面试效率非常高。1.3 阅卷逻辑笔试不只是看答案也看思路我见过很多同学把编程题写出来就以为稳了实际上阅卷人会重点看代码风格和边界处理。同一个防抖函数有人写了 5 行有人写了 20 行表面上看都对但前者可能在this指向、参数透传、立即执行参数上有隐患阅卷人一眼就能看出来。建议在答题时顺手加上注释说明自己考虑了哪些边界情况比如“这里用clearTimeout是为了取消上一次的调用”“这里透传args是为了保留事件对象”。这既是提醒自己也是让阅卷人看到你的思路。还有一个容易被忽略的点手写代码题如果遇到不会的也不要留空白。把题目里能想到的关键步骤写下来比如“这个函数应该返回一个新的 Promise”“这里需要遍历数组拿到所有结果”哪怕只是伪代码也能拿到一部分过程分。空白是零分写了就有机会。2. 基础题里的隐藏陷阱闭包、类型转换与事件循环2.1 闭包与内存泄漏答对现象只是第一步闭包几乎是前端笔试必考贝壳这套卷子考的形式也比较经典给你一段for循环加setTimeout让写输出结果。例题for (var i 0; i 3; i) { setTimeout(function () { console.log(i); }, 100); }输出是3 3 3不是0 1 2。原因是var没有块级作用域循环结束后i已经变成 3三个定时器回调共享同一个变量。改成let就能得到0 1 2因为let每次迭代都会创建一个新的绑定。除了输出结果还要会解释背后的原理闭包让回调函数保存了对i的引用而不是保存了当时的“快照”所以变量后续变化会影响闭包内的读取结果。如果面试官追问怎么用var实现0 1 2可以用 IIFEfor (var i 0; i 3; i) { (function (j) { setTimeout(function () { console.log(j); }, 100); })(i); }闭包还会牵扯到内存泄漏。这里可以记一个结论闭包本身不是问题问题在于闭包引用的对象是否被意外持有。比如给一个 DOM 元素绑定事件事件回调里引用了大对象元素被移除前没有解绑那这个大对象就无法被回收。实际写代码时要在合适的时机清理定时器、解绑事件、置空引用。答题时如果能把“闭包持有引用”和“如何避免内存泄漏”一起说出来会比单纯输出结果更有竞争力。2.2 类型转换和隐式规则最容易失分的客观题笔试卷 2 里有一类题看着简单错误率却很高就是相关的判断。典型题目console.log([] ![]); // ? console.log(0 ); // ? console.log(null undefined); // ?结果是true、true、true。很多人第一反应是“数组不等于数组的取反不是 false 吗”实际上根据规范![]先转成布尔值false然后[] false会把[]转成原始值 false再转成0 0所以是true。不要死背答案要记转换步骤两边有布尔值先转数字。一边是字符串一边是数字字符串转数字。一边是对象另一边是数字或字符串对象调用ToPrimitive转原始值。null和undefined互相相等但不和其他值相等。笔试如果遇到这种题别凭直觉按这四步走基本不会错。扩展一下如果对象自定义了Symbol.toPrimitive规则会变成先用它取值再比较。比如const obj { [Symbol.toPrimitive]() { return 1; } }; console.log(obj 1); // true这类题目考察的是对 ECMAScript 隐式转换规范的理解虽然平时写代码基本用但笔试题就是喜欢从规范里挑知识点。2.3 事件循环的异步顺序题宏微任务必须画图贝壳的前端笔试卷对异步顺序的考察比较密集多选和填空里都出现过。核心是搞清楚执行顺序同步代码 → 微任务Promise.then、queueMicrotask、MutationObserver → 宏任务setTimeout、setInterval。一道常见题console.log(a); setTimeout(() console.log(b), 0); Promise.resolve().then(() console.log(c)); console.log(d);输出是a d c b。原因setTimeout是宏任务即使延迟为 0 也要等当前宏任务执行完、微任务清空后才执行Promise.then是微任务会在当前同步代码结束后立即执行。如果题目里带async/await注意await后面的代码相当于放进了.then回调要等await右侧的表达式 resolve 之后才继续。这里有一个经典易错点await右侧如果是一个 Promise它会先执行 Promise 构造函数里的同步代码然后再把后续代码放进微任务队列。建议做题时在草稿纸上画一条时间线把宏任务和微任务队列分别列出来每次执行完一个任务就更新队列。这样即使题目再复杂也不会乱。2.4 this指向与箭头函数一套清晰的判断顺序this指向的题目在笔试卷 2 里也有而且经常结合对象方法、解构赋值和箭头函数一起出。普通函数的this判断顺序先看是不是new调用是的话指向新对象再看有没有通过call/apply/bind显式绑定然后看函数是不是某个对象的方法obj.method()形式最后是非严格模式下的默认绑定指向全局对象。箭头函数没有自己的this它会捕获定义时外层作用域的this所以在对象方法里用回调箭头函数是更安全的选择。比较经典的坑是const obj { name: test, getName() { return this.name; } }; const fn obj.getName; fn(); // undefined方法被赋值给变量再调用时this丢失了指向全局对象。要拿到name可以用fn.call(obj)或换成箭头函数捕获外层this。回答时最好把判断顺序说完整让面试官知道你是真正理解了规则而不是碰巧记住了一两道题。3. 手写题不能只背答案从防抖节流到 Promise 链式调用3.1 防抖节流高频笔试题的边界条件手写防抖节流几乎是前端笔试的“必点菜”贝壳这套卷子里也有一道。看起来简单但真正能写对的人不多。防抖的核心思路事件触发后不立即执行等待一段时间如果等待期间再次触发就重新计时。适用于搜索输入、窗口 resize。function debounce(fn, delay 300, immediate false) { let timer null; return function (...args) { const callNow immediate !timer; clearTimeout(timer); timer setTimeout(() { timer null; if (!immediate) { fn.apply(this, args); } }, delay); if (callNow) { fn.apply(this, args); } }; }这里必须保留this和args否则在作为事件处理函数时回调里的this会指向错误对象而且拿不到event对象。immediate参数控制的是“第一次触发先执行”还是“最后触发之后执行”实际业务里两种场景都有。比如搜索框一般用非 immediate 版本等用户输入完停顿 300ms 再请求而按钮点击防重复提交通常需要 immediate 版本第一次点击立即执行后续点击在等待窗口内无效。节流的核心思路每隔一段时间最多执行一次适用于滚动、拖拽、游戏触发。function throttle(fn, interval 200) { let last 0; return function (...args) { const now Date.now(); if (now - last interval) { last now; fn.apply(this, args); } }; }基于时间戳的节流会忽略最后一次触发如果希望“停止触发后还能补执行一次”就改用定时器或者在时间戳方案基础上再叠加一个 trailing 定时器。写笔试题时如果题目没明确要求至少要把时间戳版写对再口述怎么扩展。阅卷人很看重这种“我能想到边界情况”的表达。3.2 手写 Promise笔试卷 2 比卷 1 多考的一个点贝壳笔试卷 2 在手写题上比卷 1 多了一个 Promise 方向可能是手写Promise.all也可能直接让你实现一个简化版 Promise。这类题考察的不只是记 API而是你是否理解 Promise 的状态机、回调收集、微任务执行。如果时间有限先把Promise.all写对Promise.myAll function (promises) { return new Promise((resolve, reject) { const results []; let count 0; promises.forEach((p, index) { Promise.resolve(p).then((value) { results[index] value; count; if (count promises.length) { resolve(results); } }, reject); }); }); };关键在于results[index] value用索引赋值而不是push否则返回顺序会被异步完成时间打乱。Promise.resolve(p)是为了兼容传入非 Promise 值。这个写法在真实项目里的并发请求合并中也很常用。如果要求写一个简化版 Promise核心结构是class MyPromise { constructor(executor) { this.state PENDING; this.value undefined; this.reason undefined; this.onFulfilledCallbacks []; this.onRejectedCallbacks []; const resolve (value) { if (this.state PENDING) { this.state FULFILLED; this.value value; this.onFulfilledCallbacks.forEach((fn) fn()); } }; const reject (reason) { if (this.state PENDING) { this.state REJECTED; this.reason reason; this.onRejectedCallbacks.forEach((fn) fn()); } }; try { executor(resolve, reject); } catch (err) { reject(err); } } then(onFulfilled, onRejected) { if (this.state FULFILLED) { onFulfilled(this.value); } if (this.state REJECTED) { onRejected(this.reason); } if (this.state PENDING) { this.onFulfilledCallbacks.push(() onFulfilled(this.value)); this.onRejectedCallbacks.push(() onRejected(this.reason)); } } }完整版还要处理 then 返回新 Promise、穿透、错误捕获、微任务调度等笔试能写到这个程度已经能说明理解了。建议把Promise.race、Promise.allSettled也顺手练一遍因为现在面试官越来越喜欢在编程题基础上追问“如果其中一个失败了怎么办”。答出all遇到一个 reject 就整体 rejectallSettled会等待所有结束并返回每个结果的状态这一对比就能体现你 API 掌握得比较全。3.3 深拷贝与数组去重小心“看着简单”的题深拷贝在笔试卷里经常以“代码补全”或“简答题”的形式出现。最简单的答案是JSON.parse(JSON.stringify(obj))但要知道它的问题丢失函数、undefined、Symbol、Date变成字符串、正则变成空对象、循环引用直接报错。更好的做法是递归拷贝并用WeakMap记录已拷贝对象解决循环引用function deepClone(target, map new WeakMap()) { if (target null || typeof target ! object) { return target; } if (map.has(target)) { return map.get(target); } const result Array.isArray(target) ? [] : {}; map.set(target, result); Reflect.ownKeys(target).forEach((key) { result[key] deepClone(target[key], map); }); return result; }这里用Reflect.ownKeys可以拿到包括 Symbol 在内的所有键比Object.keys更完整。数组去重的题目就更多了Array.from(new Set(arr))、filter加indexOf、reduce实现。如果数组里有对象需要考虑根据某个字段去重const unique (arr, key) { const map new Map(); return arr.filter((item) !map.has(item[key]) map.set(item[key], true)); };一个容易失分的细节是去重时不要用JSON.stringify(item)作为 Map 的 key因为对象键顺序变化会导致判断错误。字段去重是更稳定的方案。4. 框架与工程化考点Vue 响应式原理到微前端4.1 Vue 响应式原理从 Object.defineProperty 到 Proxy贝壳这套笔试卷里的框架题以 Vue 为主这和很多中后台项目选型一致。最常见的问题是“Vue 2 的响应式原理是什么Vue 3 有什么改进”Vue 2 用Object.defineProperty拦截对象的属性读写同时为每个属性维护一个依赖收集器。读取时收集依赖修改时触发更新。但Object.defineProperty只能拦截已有属性所以对象新增属性和删除属性都无法触发视图更新需要Vue.set/Vue.$set。数组的push、pop等方法也不能直接触发必须重写数组方法。Vue 3 改用Proxy代理整个对象可以拦截属性新增、删除、in操作符等不再需要$set。同时依赖收集会精确到属性级配合WeakMap存储依赖性能更好。回答时最好说出两到三个具体区别能不能监听新增删除、性能开销、对数组的支持程度。如果能再补一句“Proxy 返回的是新代理对象所以需要保持对代理对象的统一引用”会显得理解更深。这里有一个实际排查经验如果你在 Vue 2 项目里发现“数据变了但视图不更新”第一反应就是检查是不是给对象新增了属性或者用索引直接改了数组项。前者用$set解决后者用splice或整体替换数组。这套排查思路其实就是从原理题里长出来的笔试答完原理再说一句“我遇到过一次……”瞬间就有实战感。4.2 diff 算法与 key 的作用虚拟 DOM 不是全部多选或简答题里会问“虚拟 DOM 和 diff 的流程”“key 的作用”。要点是diff 不是把新旧两棵树完全递归比较而是同层比较复杂度从 O(n^3) 降到 O(n)。Vue 和 React 的 diff 实现有区别但“同层比较 key 复用”的思路是一致的。key 的作用是让 diff 能精确判断节点是移动、复用还是重建。一个很常见的实际问题是列表使用index作为 key当列表头部插入一项或做倒序时所有节点的 key 都变了可能导致组件状态错乱比如输入框里的内容对不上号。正确做法是用业务唯一 id。这道题表面在问 key其实考的是你对“组件状态与虚拟节点复用”的理解。举一个直观的例子列表渲染三个输入框用户在第一格输入“贝壳”然后在列表头部插一条新数据。如果 key 是 index原本第一格的输入框会被复用成新数据那一行但输入框里的 value 还是“贝壳”视觉上就是内容错位了。如果 key 是业务 iddiff 能识别出其他节点没有变只有新增节点需要创建就不会丢状态。面试时能把例子讲清楚比背定义有用得多。4.3 微前端与工程化贝壳这类中后台场景喜欢考什么贝壳前端团队负责的业务包含经纪人端、买卖租赁前台、管理后台、数据大屏等多个系统技术栈不太可能完全统一所以微前端很容易成为笔试或面试的加分项。题目一般从“为什么要用微前端”开始多团队独立开发、不同技术栈共存、独立部署、灰度发布。然后会问框架原理比如 qiankun 的沙箱怎么做 JS 隔离和样式隔离。常见的回答路径JS 沙箱快照沙箱或Proxy代理沙箱让子应用的全局变量不污染主应用。样式隔离严格隔离可以使用 Shadow DOM但体验和兼容性要权衡或者通过 CSS 前缀、CSS Module、动态样式标签清理来做。通信通常用props传递、全局事件总线、window上的自定义事件不要轻易把所有状态放到全局。工程化题还会涉及构建工具webpack 的SplitChunks、tree shaking、按需加载Vite 为什么开发快ESM 按需编译 预构建依赖上线前怎么做资源压缩和 CDN 部署。这里不需要背特别细的配置项但要把“能解决什么问题”说清楚。比如 tree shaking 依赖 ES Module 的静态结构所以业务代码尽量不用 CommonJS 写模块。还有一个常见问题“怎么保证微前端子应用之间样式不互相污染”除了沙箱还可以在子应用打包时给所有类名加统一前缀或者在主应用加载子应用时动态插入和移除style标签。回答这些细节时能把自己的实践经验说出来是最好的如果只是背概念面试官追问两句就容易露馅。5. 场景题的现实解法从路由鉴权到长列表优化5.1 路由鉴权与动态路由不是把 token 存下来就行笔试卷 2 最后一道场景题经常来自真实业务比如“用户登录后不同角色看到不同菜单怎么办”。完整回答至少包括三层登录后保存 token并解码用户角色信息。使用路由守卫Vue Router 的beforeEach或 React Router 的自定义封装判断登录态未登录跳登录页。根据角色动态注册路由而不是把所有路由一次性注册。Vue Router 4 支持router.addRoute可以在登录后把有权限的路由动态加进去同时配置 404 兜底。还要记得提按钮级权限通常用自定义指令v-permission控制元素显示。同时强调前端权限只是体验优化真正的权限校验必须后端做因为前端任何控制都能被绕过。这个回答能体现工程实践意识。如果题目再深入一点会问“刷新页面后动态路由怎么恢复”。这时候要说明刷新时必须重新从后端拉取权限或从本地持久化里读取角色信息再重新 addRoute否则刷新就白屏。这个问题在实际项目里很容易踩坑笔试能答出来会是加分项。5.2 长列表与大数据量渲染虚拟列表的取舍贝壳房源列表动辄上万条这类场景题很常见“后端一次返回 1 万条数据前端怎么渲染”。直接v-for渲染 1 万个 DOM 一定会卡方案是分页、上滑加载、虚拟列表。虚拟列表的思路是只渲染可视区域内的元素通过scrollTop和元素高度计算起始索引。伪代码const