前端校招笔试题深度复盘:从JS核心到性能优化

前端校招笔试题深度复盘:从JS核心到性能优化 1. 写在前面这道题到底值不值得做第四范式2019校园招聘前端笔试题放到今天来看可能很多同学会觉得“过时了”。但如果你真花几个小时把这份卷子从头到尾捋一遍会发现一个很有意思的事实里面考的原型链、事件循环、手写Promise、构建优化在2026年的前端面试里依然是高频考点。换句话说前端行业技术迭代很快但面试官想考察的能力模型本质上没怎么变过。我当年是校招季做的这套题当时的感觉是“题目不算偏但每一道都能往下深挖”。它不像某些公司喜欢考冷门API记忆而是更看重你有没有真正理解JavaScript这门语言的核心机制以及你在面对一个开放性问题时能不能给出有层次、有取舍的方案。这篇文章我会把整套题的考察思路拆开讲清楚并给出核心题目的参考解法和我自己的踩坑复盘。不管你是正在准备校招还是工作几年想回头补基础都值得花点时间看看。需要说明的是网上流传的版本可能略有出入我这里按我当年实际做到的题目组合来讲如果你拿到的题目不完全一样思路是可以直接迁移的。2. 整体考卷结构与考察思路拆解2.1 从岗位画像反推考点设计第四范式做的是AI平台和机器学习相关产品前端岗位要支撑的产品形态主要是模型管理后台、数据标注平台、可视化大屏、机器学习实验平台这类偏B端、偏重数据交互的系统。这类系统的前端特点可以总结成三句话数据量大、交互状态复杂、对性能敏感。所以笔试题不是随手出的它背后对应的是岗位的实际工作场景。比如考察数组去重、排序、扁平化是因为你在实验平台里经常要处理模型返回的嵌套数据考察重排与重绘是因为大屏可视化页面里一次不当的DOM操作可能直接导致掉帧考察事件循环和异步顺序是因为前端要频繁处理接口请求的竞态、loading状态、批量提交这类问题。明白这一点你就知道为什么这套卷子里几乎没有考“display有哪些取值”或者“CSS选择器优先级口诀”这类死记硬背的题目而是把考点埋在场景里。面试官真正想筛掉的是那些只会背八股文、但一遇到真实问题就卡壳的候选人。反过来这对我们准备笔试也有一个很重要的启示不要孤立地刷题而是把每一道题和你未来可能做的业务场景绑定起来。你答的不是一道题而是告诉面试官“我具备搞定这类业务问题的能力”。2.2 题型分布与时间分配建议我印象中这套卷子的题型大概分四块基础选择题、手写代码题、算法题、开放设计题。题量不算特别大但手写题比较费时间如果不在前面控制节奏很容易在最后的大题上仓促作答。我根据自己的答题经验做了一个时间分配参考表题型大致题量建议用时考察重点基础选择题10道左右20分钟JS基础、CSS基础、浏览器机制手写代码题3-4道40分钟原型链、闭包、异步、Promise算法题2道左右30分钟数组/字符串处理、复杂度意识开放设计题1道30分钟工程化思维、性能优化方案这里想多说一句选择题的性价比其实很高因为它是整张卷子唯一有确定答案的部分拿分相对容易。但很多同学容易在前面的选择上纠结太久遇到一道不太确定的题就想“再想想”结果后面手写题时间不够匆忙交上去一堆半成品反而丢了大头分。我的建议是选择题遇到不确定的先凭第一感觉标记一个答案整张卷子做完还有时间再回来细想千万不要恋战。2.3 为什么说这套题“入门容易答好难”这卷子表面上没有那种让你完全无从下手的偏题怪题但每道题都有往上发挥的空间。打个比方选择题里问你typeof null是什么你答“object”只能拿基础分如果能在旁边补一句“这是历史遗留bugES规范里规定null是原始值但typeof返回值是object空值判断要用 null而不是用typeof”那就完全不一样了。我在后来的面试复盘里发现一个规律笔试分数并不是看你会不会做对每道题而是看你在有限的作答里能不能展示出超过同届候选人的深度。面试官每天看几百份卷子什么叫“背过答案”什么叫“真正理解”从答题的旁注、代码的注释、方案的取舍上一眼就能分辨出来。所以这篇复盘里我不仅会给出标准解法还会告诉你哪些地方可以“多写两笔”来加分哪些地方写多了反而是画蛇添足。3. 核心题目深度解析与参考解答3.1 JS基础原型链与闭包的那些“坑”先说我印象最深的一道选择题大概是考察new和原型链的。原题记不太清了但核心考点是这样的定义一个构造函数给它的原型上挂了一个方法然后new出两个实例其中一个实例自己又定义了一个同名方法问两个实例调用方法的输出结果。function Person(name) { this.name name; } Person.prototype.sayHello function() { return Hello, I am this.name; }; const p1 new Person(Alice); const p2 new Person(Bob); p2.sayHello function() { return Hi, I am this.name; }; console.log(p1.sayHello()); // Hello, I am Alice console.log(p2.sayHello()); // Hi, I am Bob这个知识点本身不难就是实例属性会遮蔽原型属性。但如果你只是在选项里选了正确答案这题的价值就没发挥出来。阅卷人更希望看到你理解背后的机制当访问p2.sayHello时JavaScript会沿着原型链一层层找先在p2自身的属性上找到了那就不需要再往上走。如果p2身上没有才会去Person.prototype上找再没有就继续往上到Object.prototype最终找不到才返回undefined。顺便说一句原型链这块还有一个高频衍生考点就是instanceof的原理。它检查的是构造函数的prototype是否出现在实例的原型链上。p1 instanceof Person返回true因为Person.prototype确实在p1的原型链上。但如果手动修改了原型比如Person.prototype {}那之前创建的实例再instanceof Person就变成false了。这个坑在业务代码里不太常见但面试题里经常拿来“钓鱼”建议复习的时候把原型链的完整查找过程、prototype和__proto__的关系、instanceof的实现机制一起串起来理解。闭包的题也很典型我记得有一道是问循环里用var声明变量setTimeout输出什么的。for (var i 0; i 3; i) { setTimeout(function() { console.log(i); }, 100); } // 输出 3 3 3这道题的经典程度已经快被讲烂了但每年面试还是有人答错。核心原因在于var声明的变量是函数作用域这里的i实际上只有一个循环结束后i的值是3三个定时器回调共享同一个i执行时输出的自然都是3。解决方案也简单要么把var改成let利用块级作用域每次都生成一个新的绑定要么用闭包包一层for (var i 0; i 3; i) { (function(j) { setTimeout(function() { console.log(j); }, 100); })(i); } // 输出 0 1 2这里我想补一个很多人忽略的细节let之所以能解决这个问题是因为for循环的每次迭代都会创建一个新的词法环境并把当前的i值绑定进去。这其实是ES6规范里专门针对循环做的处理不是在setTimeout层面做文章。你能把这个机制讲明白比单纯说“var改let就行”要有说服力得多。3.2 手写代码实现一个简易版Promise这套卷子里的手写题最让我头疼的是让实现一个Promise具体要求是支持resolve、reject、then、catch能做到链式调用并且能处理异步的resolve调用。当时我看到这道题心里是有点慌的因为Promise的完整实现涉及到状态机、微任务队列、值穿透、数组化的回调队列想在一个笔试环境里分分钟写完整很困难。但后来我想明白一件事面试官未必期待你写出和规范完全一致的promise而是想看你对异步流程控制的基本功。所以答题策略应该是先实现最小可用版本把状态机和then的注册逻辑写对再补上链式调用的支持这样已经能拿到大部分分了。我写的最基础版本大概是这样的function MyPromise(executor) { this.state pending; this.value undefined; this.reason undefined; this.onFulfilledCallbacks []; this.onRejectedCallbacks []; const resolve (value) { if (this.state ! pending) return; this.state fulfilled; this.value value; this.onFulfilledCallbacks.forEach((fn) fn()); }; const reject (reason) { if (this.state ! pending) return; this.state rejected; this.reason reason; this.onRejectedCallbacks.forEach((fn) fn()); }; try { executor(resolve, reject); } catch (error) { reject(error); } } MyPromise.prototype.then function(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还需要处理链式调用和异步微任务这里给出核心状态机逻辑”。事后复盘这个标注其实是很重要的一步它让面试官知道你知道完整的promise是什么样的只是在笔试环境里没法写全。现在回头想如果当时时间更充裕我会把链式调用补上虽然不要求完全按Promise/A规范来但至少要体现“then返回新Promise”这个核心思想。下面这个版本是我后来在项目里整理过的可以表演“then的返回值是Promise、里层返回值会被resolve”这个关键行为MyPromise.prototype.then function(onFulfilled, onRejected) { const promise2 new MyPromise((resolve, reject) { const handleFulfilled () { try { const result onFulfilled(this.value); resolve(result); } catch (error) { reject(error); } }; const handleRejected () { try { const result onRejected(this.reason); resolve(result); } catch (error) { reject(error); } }; if (this.state fulfilled) { handleFulfilled(); } else if (this.state rejected) { handleRejected(); } else { this.onFulfilledCallbacks.push(handleFulfilled); this.onRejectedCallbacks.push(handleRejected); } }); return promise2; };这里有几个细节值得注意。第一then回调里如果抛异常要能被下一个catch捕获所以handleFulfilled里必须有try/catch。第二如果onFulfilled返回的本身是一个Promise那严格来说需要递归调用resolvePromise来处理这属于Promise/A规范的进阶内容。笔试里你写出简化版并理解为什么需要递归处理我觉得已经可以了。3.3 布局与CSS三栏布局与重排重绘第四范式的业务页面上三栏布局特别常见左侧模型列表、中间主内容区、右侧参数面板这是标准的B端产品形态。所以卷子里面有一道三栏布局题我完全不意外。题目要求大概是这样的左右两栏固定宽度中间自适应并且要求中间栏最先渲染。这里的关键点是“中间栏最先渲染”因为它决定了你不能简单地用浮动把三栏排成一行而是要利用flex或者绝对定位的顺序调整能力。如果面试官没有特殊要求直接用flex是最优雅的div classcontainer div classmain中间自适应内容/div div classleft左侧固定 200px/div div classright右侧固定 200px/div /div.container { display: flex; } .left { width: 200px; order: -1; } .right { width: 200px; } .main { flex: 1; }注意order这个属性它的默认值是0数值越小排列越靠前。把left的order设为-1相当于强制它排在中间栏前面但DOM结构上中间栏确实是在最前面的这样就满足了“中间栏最先渲染”的要求。为什么要强调这个因为在浏览器解析HTML时DOM顺序会影响首屏渲染时间。如果中间栏放最后用户要先等左右两栏的资源加载完才看到核心内容体感会差很多。实现三栏布局的方法很多除了flex还有圣杯布局、双飞翼布局、grid布局。笔试里如果时间充裕你可以多写一种方案比如grid.container { display: grid; grid-template-columns: 200px 1fr 200px; } .main { grid-column: 2; } .left { grid-column: 1; } .right { grid-column: 3; }这个写法更简洁但有一个问题grid-template-columns是按照DOM顺序排的如果中间栏在第一个默认情况下它会占据第一列而不是中间那一列。所以要配合grid-column显式指定每一栏的位置。这也是为什么grid在有些人看来“不够直觉”的原因。我个人的建议是笔试里优先写flex方案因为它兼容性更好、理解成本低阅卷人看起来也舒服。如果你能同时写出grid方案并解释两者的适用场景那会是一个不错的加分项。3.4 算法题数组扁平化、去重与排序的手写姿势算法题大概是这张卷子里让很多人纠结的部分。它不是LeetCode上那种复杂的动态规划题而是更偏向工程场景的数组处理。比如让你实现一个数组扁平化函数要求支持传入多级嵌套的数组并顺便去重、排序。如果只要求扁平化一行flat(Infinity)就完了。但笔试题里一般会限制你不能用原生方法考察的是递归和reduce的掌握程度function flatten(arr) { return arr.reduce((acc, current) { return Array.isArray(current) ? acc.concat(flatten(current)) : acc.concat(current); }, []); } const result flatten([1, [2, [3, [4, 5]]]]); // [1, 2, 3, 4, 5]这里有个容易被问到的细节为什么用concat而不是push因为concat返回的是新数组不会修改原数组这样在reduce里可以链式累积逻辑更清晰。如果你用push需要先acc.push(...)再return acc也可以但代码就没那么优雅了。如果要求扁平化之后顺便去重和排序可以这样组合function uniqueSort(arr) { const flat flatten(arr); return Array.from(new Set(flat)).sort((a, b) a - b); }注意这里的sort一定要传比较函数否则默认按字符串编码排序[10, 9, 8]会被排成[10, 8, 9]这是新手特别容易踩的坑。我在面试别人时经常看到有人写sort()没传参结果数字数组排出来的顺序不对自己还半天没反应过来。关于去重还有一个高频追问是new Set和Array.from(new Set())的区别。其实Array.from只是把Set对象转回数组的其中一种方式你也可以用展开运算符[...new Set(arr)]效果一样。但如果你面对的是大量数据处理要注意Set的去重是基于SameValueZero算法的NaN也可以被正确去重而indexOf判断NaN就会出现问题。这也是一个可以写进注释里展示你懂原理的点。3.5 工程化与性能优化前端构建笔试的开放题我还记得卷子最后有一道开放题大意是你的页面在线上加载很慢白屏时间长请给出分析和优化方案。这种题目没有标准答案但恰恰是最能拉开分差的。大部分同学会写“压缩静态资源、开启CDN、懒加载”这些当然没错但如果你只写这三条说明你还没从工程化的角度系统思考过问题。我的答题思路一般是按“从输入URL到页面展示”这条链路来梳理每个环节都找可以优化的点网络层配置HTTP缓存强缓存协商缓存、开启CDN加速、使用HTTP/2多路复用、合并请求减少RTT。资源层JS/CSS压缩混淆、图片格式换成WebP或AVIF、按路由拆分代码而不是打一个巨大的bundle。渲染层骨架屏减少白屏焦虑、关键CSS内联、提前预加载核心资源。代码层减少重排重绘、避免长任务阻塞主线程、合理使用requestAnimationFrame来分批更新DOM。如果能在答案里突出“首屏”这个关键词并说明你的分析是基于“关键渲染路径”那就更有深度了。比如我当年写的一点是脚本放在head里会阻塞HTML解析应该改用defer或async或者把不关键的脚本移到body末尾。为什么要区分defer和asyncdefer会保证脚本按顺序执行适合有依赖关系的脚本async下载完就执行顺序不保证适合独立统计脚本。我后来在项目里用这个原则优化过数据上报脚本确实有效果。另外如果这道题里cue到了Webpack那你还得提一嘴SplitChunks插件。它的作用是把第三方库和你自己的业务代码分开这样浏览器可以长期缓存那些不变的vendor包而不是每次发布都让用户重新下载所有依赖。我第一次接触这个配置是在一个老项目里所有代码打包成一个bundle5MB大小每次上线用户都要重新加载。后来改成SplitChunks拆分之后虽然首次加载没小太多但二次访问命中缓存的概率大幅提升体感差异非常明显。4. 我当年的作答过程与复盘4.1 时间分配先做会的再做难的说句实话我当年做这套卷子的过程并不是很顺利。前面的选择题还算顺手到了手写Promise那道题我卡了大概十分钟反复改了好几版都觉得不对劲。当时我心里有点慌因为后面还有算法题和开放题。后来我强制自己停下来采取了“跳题策略”先把有把握的题全做完最后再回来啃硬骨头。原理很简单笔试的核心目标是拿分最大化而不是证明某一道题你能做到完美。卡在一道题上超过五分钟边际收益就开始下降了这时候果断跳过去把确定能拿的分先装进口袋心态也能稳下来。第二个策略是“先写骨架再补细节”。遇到手写题先别急着写具体逻辑先把函数签名、核心变量、整体流程用注释写出来。比如实现Promise我先写了三行注释1. 状态管理 pending/fulfilled/rejected、2. 成功/失败回调数组、3. then返回新Promise。然后再往里面填代码。这样做的好处是即使最后代码没写完阅卷人也能通过注释看出你的思路是完整的。4.2 我在笔试现场踩过的坑这里整理几个我当时踩过、以及后来帮别人复盘时常见的坑专门列成一张速查表供你对照自查容易踩的坑具体表现解决办法数组排序不传比较函数[10, 9].sort()得到[10, 9]写成sort((a, b) a - b)typeof null判断失误以为typeof null null记住它是object空值判断用 null事件循环执行顺序混乱微任务和宏任务的输出顺序搞反记住先微任务再宏任务Promise.then比setTimeout先执行原型链查找方向搞反以为会从顶往下找记住是从实例自身往原型链顶层找手写题只写最终代码不写思路代码有bug时被判零分先用注释写思路让阅卷人看到过程忽略题目里的“最先渲染”三栏布局没有调整DOM顺序用flex的order或grid的grid-column控制视觉顺序第一行和第三行看起来很简单但我在实际批改简历笔试时发现真的有好多人犯。尤其事件循环很多人背过口诀“先微后宏”但一遇到await就乱了。这里补一个最简单的分析框架await后面跟的表达式会立即执行但await之后的代码相当于被放进了微任务队列。所以async function test() { console.log(1); await console.log(2); console.log(3); } test(); console.log(4); // 1 2 4 3这里console.log(2)是立即执行的因为它是await后面的表达式console.log(3)被推迟到微任务里console.log(4)是同步代码所以先在宏任务里执行。以后做题遇到async/await就把“await之后的部分看作微任务”这个规则记牢基本不会再错。4.3 面试官视角什么样的答卷会被判高分我后来参加过一次技术部门的笔试阅卷多少能了解一点评分逻辑。阅卷人看一份卷子短则两三分钟长也就五六分钟。在这么短的时间里他们重点看的就是一是代码能不能跑、二是思路是否清晰、三是答题态度是否认真。第三点听起来很虚但体现在卷面上其实很具体你的变量命名是否语义化、关键地方有没有注释、边界条件有没有考虑到。比如数组去重那道题大部分人写的都是[...new Set(arr)]。有一个人额外写了如果数组里有对象、Set的去重会失效因为对象是按引用来判断是否相等的所以如果需要按对象某个字段去重得自己实现一个reduce函数。我看到的时候真的有点印象分加成因为这明显是做项目时踩过坑的人才会注意到的角度。这种“基于实践的补充说明”比你在卷子上多写十行没必要的代码都管用。5. 备考建议2026年了这套题还有意义吗5.1 经典考点为什么一直考你可能会想2019年的题目放到2026年React都出了新版本、Vue都迭代好几轮了这题还有参考价值吗我的回答是有而且价值不小。前端框架每年都在变但JavaScript语言的核心机制、浏览器的渲染原理、网络加载的流程、工程化构建的思路这些东西的变化速度远没有框架那么快。原型链还是那套原型链事件循环还是那套事件循环重排重绘也还是那套重排重绘。更重要的是这类考点代表着一种“抽象能力”。你如果只会在Vue里写页面遇到问题就搜答案那笔试一遇到没见过的场景立刻就会露馅。但如果你的基础足够扎实即使没做过某个业务也能通过原理推导出大概的解决方案。面试官考这些老题本质上不是在为难你而是想看看在新技术层出不穷的环境下你有没有把地基打牢。在我看来准备这类笔试题最有效的方式不是把题目背下来而是把每道题背后的知识点铺开画一张思维导图把一个考点延伸到它附近的所有内容。比如看到原型链就要联想到new的过程、Object.create、class语法糖、instanceof原理看到闭包就要联想到模块模式、柯里化、防抖节流、内存泄漏。这样复习一遍你覆盖的知识面会非常可观。5.2 现在还需要补充哪些新内容当然只复习老题是不够的。2026年的前端面试有些新方向是2019年基本不会考的需要你们自己额外补上。工程化方面Vite已经成了很多新项目的标配它的开发服务器启动速度比Webpack快好几个量级关键原理是浏览器原生ESM不用提前打包。面试官可能会问你Vite和Webpack的区别以及它们各自的性能瓶颈。性能优化方面除了传统的加载优化现在更关注Web Vital指标比如LCP最大内容绘制、CLS布局偏移、INP交互到下一次绘制延迟。这些指标直接关系到用户体验也是很多公司做性能优化的复盘核心。AI工具的使用也成了面试里的一个话题。比如用AI辅助写代码时如何审查它生成的代码、如何避免它写出多余的逻辑、如何让它遵循项目的既有规范。这不是笔试硬性考点但面试聊项目的时候可能会被问到。我自己在带新人的时候会建议他们不要把AI工具当成“答案生成器”而是当成“思路讨论者”。遇到一个不懂的问题先自己想一遍再让AI给一个参考实现对比一下自己的想法哪里不对哪里可以优化。这种学习方式比纯粹刷题要高效得多。6. 一点个人体会做这套题已经是好几年前的事了现在回头看真正让我受益的并不是“会做某道题”而是备考过程中被逼着把JavaScript基础重新啃了一遍。我后来在工作中遇到过很多棘手的问题比如线上偶发的白屏、大数据量列表的渲染卡顿、第三方SDK加载顺序导致的冲突最后定位到根因时发现都离不开原型链、事件循环、渲染机制这些最基础的知识。所以如果你现在正在准备笔试我的建议是不要只盯着“这道题答案是什么”多问自己一句“为什么是这个答案”。想不明白的地方去翻ES规范、去读源码、去跑个小demo验证都比死记硬背有价值得多。笔试只是第一关能让你真正走的更远的是那种遇到问题敢往下钻、能往下钻的能力。这套题刷完了希望你能带走的不只是几个标准答案而是这种对基础的敬畏和一个更扎实的前端知识体系。