前端春招笔试复盘:从JS基础到手写代码的完整解析

前端春招笔试复盘:从JS基础到手写代码的完整解析 1. 笔试整体印象与备考背景一份卷子折射出的前端行情2023年开春互联网大厂和中小厂都陆续启动了春招我也在“金三银四”前把简历投了一轮。小满科技这家的岗位是Web前端岗做跨境电商SaaS方向的业务偏B端技术栈以Vue和React为主笔试题目出来之后整体给我的感觉是不偏不怪但覆盖面相当广且很看重基础功底和编码习惯。第一批笔试的时间是2023年3月上旬线上笔试时长90分钟总共四道大题加若干小题题量不算大但想全部答完并且拿到高分并不容易。为什么这么说因为这套笔试题的“坑”不在题目难度本身而在于很多题看起来简单实际写起来要么边界情况容易漏要么考察的是你有没有真正理解原理而不是背过概念。就我的观察春招笔试一般分成三个层次第一类是大厂算法题刷屏考察数据结构和算法功底第二类是中小厂的基础知识简单算法考察知识面和工程能力第三类是纯项目经历问答基本不怎么考察代码能力。小满这套题属于第二类和第一类之间既有比较扎实的JS基础题也有需要在限定时间内完成的手写代码题同时还有一两道需要动脑子的算法题。我在笔试前重点复习了几个方向ES6新特性、浏览器缓存机制、事件循环、Vue/React核心原理、手写实现常见工具函数。从结果来看这套复习思路是有效的但也暴露了一些盲区后面会详细展开。如果你正在准备前端岗的春招笔试这篇复盘文章希望能帮你建立一个相对完整的认知框架笔试到底考什么、为什么这样考、哪些地方容易失分、以及我踩过哪些坑。我会把回忆中的题目和解题思路尽量还原结合考后的复盘整理成文供大家参考。2. 试卷结构与时间分配90分钟里应该把力气花在哪里先说一下这套笔试卷面的大致结构。小满的笔试是线上作答前端岗和部分后端岗共用一个笔试平台界面分为题目区和答题区代码题支持在线运行测试但不支持切屏或复制外部代码全程有摄像头监控。时间一共90分钟题量我回忆了一下大致如下单选题8题每题4分共32分主要涉及HTML/CSS/JS基础、网络协议、浏览器原理。多选题4题每题5分共20分考察点更加综合容易出现漏选或错选。手写代码题3题共48分考察JS编程能力和算法思维题目要求在线书写并跑通测试用例。总分100分没有明确的及格线但从招聘流程来看笔试成绩会直接影响面试筛选。90分钟对于这个题量来说时间其实是比较充裕的关键在于选择题上能不能快速拿准把时间多留给手写题。我当时的时间分配是这样的题目类型题量预计用时实际用时备注单选题820分钟15分钟有些题目答案非常明确直接用排除法多选题415分钟20分钟多选和漏选都不得分需要谨慎手写代码题355分钟50分钟留了5分钟检查选择题和补漏这里我想强调一个经验除非你基础非常扎实否则不要试图把所有选择题都做对后再开始写代码题。选择题的干扰项往往设置得很巧妙一旦卡住就容易浪费时间而手写题是拉开分数的关键。我的做法是先把所有选择题快速过一遍拿不准的题目先标记等手写题做完后如果有剩余时间再回来仔细推敲。实际做题时选择题部分比我想象的要顺利一些因为很多知识点我在复习时正好覆盖了。但有3道选择题让我印象很深一道涉及CSS的层叠上下文一道考察JS的变量提升和暂时性死区还有一道是React的渲染机制问题。这些题目我在后面会逐一拆解。手写代码题的第一题相对简单——实现一个深拷贝函数第二题是典型的防抖函数实现第三题是一道中等偏上的算法题跟数组和双指针有关。这三道题我都在规定时间内写出了可运行的代码但第二题和第三题在考后复盘时发现还有优化空间这个后面详细说。3. 选择题里藏着的原理深水区层叠上下文、变量提升与渲染机制3.1 层叠上下文CSS问题里的隐形陷阱选择题中有一道大概是这样的在设置了position: relative的元素内部有多个子元素设置了不同的z-index值问最终哪个元素显示在最上层。这道题看起来考察的是z-index的基本用法但实际上是考层叠上下文的形成条件和嵌套规则。z-index并不是简单地数字大就显示在上面只有当元素形成了层叠上下文stacking context之后z-index的比较才在一个上下文中进行。层叠上下文的形成条件包括根元素htmlposition值为absolute或relative且z-index不为autoposition值为fixed或stickyflex容器的子元素且z-index不为autoopacity小于1的元素transform不为none的元素设置了filter、perspective、clip-path等属性的元素这道题的陷阱在于父元素设置了opacity: 0.9这导致父元素本身形成了一个新的层叠上下文。即使内部子元素的z-index很高也无法超越兄弟层叠上下文中的同级元素。很多人包括我一开始都会忽略opacity这个属性对层叠上下文的影响直接按z-index数值比较就会选错。这个知识点在面试中也是高频考点因为实际开发中经常遇到弹层被遮住、下拉菜单被遮挡等问题排查到最后往往是层叠上下文的问题而不是z-index设得不够大。3.2 变量提升与暂时性死区ES6里最容易被绕进去的考点题目大概是这样的一段代码中在let声明之前访问某个变量console.log会输出什么console.log(a) let a 10结果是直接抛出ReferenceError而不是undefined。这就是暂时性死区Temporal Dead Zone简称TDZ的概念。let和const声明的变量不会被提升到作用域顶部进行初始化而是在作用域的开始到声明语句之间形成一个“死区”在这个阶段访问变量会报错。而var声明的变量则会被提升并初始化为undefinedconsole.log(a) // undefined var a 10这个区别背后是JS引擎的执行机制。变量提升是JavaScript在ES5时代就存在的特性var声明会被挂到当前作用域的顶部但只提升声明不提升赋值。而let和const虽然不是传统意义上的“提升”但它们在作用域创建时也会被登记只是不允许在声明前访问这本质上也是提升的一种变体只是加了访问限制。这道题我在复习时专门整理过几种变体比如函数声明和变量声明的优先级console.log(foo) function foo() { console.log(1) } var foo 2这个输出结果是函数本身因为函数声明的提升优先级高于变量声明并且函数声明会被完整提升。但如果是函数表达式情况就不一样了。这类题目在选择题里出现频率很高建议备考时多做几道变体题。3.3 React渲染机制从一道多选题说开去多选题中有一道关于React性能优化的问哪些操作可能导致组件不必要的重渲染。选项包括父组件setState、props对象每次渲染时重新创建、使用React.memo包裹组件、useCallback依赖项变化等。这道题考察的是对React渲染机制的理解深度而不只是API的记忆。React组件的重渲染触发条件主要有三个state变化、props变化、父组件重渲染。其中父组件重渲染时默认情况下所有子组件都会跟着重渲染除非使用React.memo或shouldComponentUpdate来阻断。值得留意的是React.memo本身也有局限性它只对props做浅比较。如果传给子组件的props是一个内联对象比如Child config{{ name: test }} /那么每次父组件渲染时config都是一个新对象React.memo的浅比较会发现引用变了子组件照样会重渲染。解决办法是用useMemo缓存这个对象或者把对象拆分后传基本类型。这道题我在复习时正好整理过所以答得比较顺。但从考后交流来看很多同学在这道题上纠结了很久因为多个选项在某种条件下都成立需要结合React.memo的工作机制来判断哪些“能真正避免不必要的重渲染”。多选题的难点就在这里——不是选一个正确答案而是所有选项都要判断准确漏选和多选都不得分。3.4 网络协议和浏览器缓存选择题里的“送分题”与“送命题”还有几道选择题涉及网络和浏览器原理比如HTTP缓存命中流程、强缓存和协商缓存的区别、Cookie与localStorage的差异等。这些题目属于基础知识正常情况下应该是送分题但具体到题目细节还是有一些容易忽略的点。例如HTTP缓存题问的是设置了Cache-Control: no-cache的资源命中缓存时的行为。很多同学看到no-cache就以为“不缓存”实际上no-cache的意思是“使用缓存前必须先向服务器验证”而no-store才是不缓存。如果资源没过期且验证通过还是会使用缓存副本的。这类细节在笔试中出现考察的就是对概念理解的准确性。我复习时专门整理过HTTP缓存相关的思维导图把强缓存Expires、Cache-Control和协商缓存Last-Modified、ETag的优先级和流程理清了所以答题时不慌。4. 手写代码题逐题拆解从题意分析到边界处理4.1 深拷贝函数易写难对的经典题目第一道手写代码题是定义明确的实现一个深拷贝函数要求能够处理数组、普通对象和嵌套结构。这道题看起来非常简单就是一个递归遍历的过程function deepClone(target) { if (typeof target ! object || target null) { return target } const result Array.isArray(target) ? [] : {} for (const key in target) { if (Object.prototype.hasOwnProperty.call(target, key)) { result[key] deepClone(target[key]) } } return result }如果只是写出上面这个版本这道题大概只能得一半分。为什么因为没有处理循环引用。当对象内部存在循环引用时上面的递归会陷入无限递归最终导致栈溢出。例如const obj {} obj.self obj deepClone(obj) // 栈溢出正确的做法是使用WeakMap或Map来记录已经拷贝过的对象遇到循环引用时直接返回之前拷贝的结果function deepClone(target, map new WeakMap()) { if (typeof target ! object || target null) { return target } if (map.has(target)) { return map.get(target) } const result Array.isArray(target) ? [] : {} map.set(target, result) for (const key in target) { if (Object.prototype.hasOwnProperty.call(target, key)) { result[key] deepClone(target[key], map) } } return result }这里用WeakMap而不是Map的原因在于WeakMap的键是弱引用不会阻止垃圾回收。如果使用Map在拷贝结束后那些被记录的原始对象和拷贝结果之间的强引用关系仍然存在可能导致本应被回收的对象无法释放。虽然在这个函数内部声明周期内影响不大但作为一个追求完美的答案用WeakMap是更合适的。考后复盘时我还想到一个问题这个实现没有处理Date、RegExp、Map、Set等特殊对象。虽然题目没有明确要求但如果能在答案中体现对特殊对象的处理会让阅卷人眼前一亮。完整的深拷贝在工程中通常直接使用lodash的cloneDeep或structuredClone但笔试里能把循环引用处理好已经超过了多数候选人。提示手写题一定要重视边界条件。写代码时不要只考虑“正常情况”要把空对象、数组嵌套、循环引用、函数属性等情况都过一遍。4.2 防抖函数从基础版到带cancel和立即执行的高级版第二道手写题是实现一个防抖debounce函数。防抖的核心思想是在事件被连续触发时只有在最后一次触发后等待一定时间才执行目标函数。基础版实现如下function debounce(fn, delay 300) { let timer null return function(...args) { if (timer) { clearTimeout(timer) } timer setTimeout(() { fn.apply(this, args) timer null }, delay) } }这个版本能处理大部分场景但我在答题时额外加了两个功能取消cancel和立即执行选项immediate。为什么要加取消功能呢因为实际开发中有时需要在卸载组件或页面跳转时取消防抖任务的执行避免对已经销毁的组件进行操作。function debounce(fn, delay 300, immediate false) { let timer null let isInvoked false function debounced(...args) { if (immediate !isInvoked) { fn.apply(this, args) isInvoked true } if (timer) { clearTimeout(timer) } timer setTimeout(() { fn.apply(this, args) timer null isInvoked false }, delay) } debounced.cancel function() { if (timer) { clearTimeout(timer) } timer null isInvoked false } return debounced }这里有个细节必须注意使用箭头函数时this的绑定是在定义时确定的无法通过apply来改变。所以上面的实现必须使用function关键字声明返回的函数或者在setTimeout回调中也使用普通函数。我见过不少人在面试中写debounce时因为用了箭头函数导致this指向出问题这是一个很容易被忽略的硬伤。防抖的经典应用场景包括输入框实时搜索、窗口resize事件、滚动事件等。理解了防抖的用途写起来思路就会清晰许多因为你知道它要解决的问题是“频繁触发导致性能问题”以及“最后一次触发后还要更新状态”。4.3 双指针算法题寻找两数之和的所有组合第三道手写题是一道算法题题目大意是给定一个升序排列的整数数组和一个目标值找出数组中所有两数之和等于目标值的组合要求不重复。这道题如果用暴力解法两层循环遍历时间复杂度是O(n^2)。在笔试环境下如果数组长度较大很可能会超时。因为数组是升序的最优解法是使用双指针时间复杂度可以降到O(n)function findTwoSumPairs(arr, target) { const result [] let left 0 let right arr.length - 1 while (left right) { const sum arr[left] arr[right] if (sum target) { result.push([arr[left], arr[right]]) // 跳过重复值 while (left right arr[left] arr[left 1]) { left } while (left right arr[right] arr[right - 1]) { right-- } left right-- } else if (sum target) { left } else { right-- } } return result }这个解法的核心思路是因为数组升序所以左指针向右移动时和值增大右指针向左移动时和值减小。通过与目标值比较大小每次移动一个指针逐步逼近目标。在处理重复组合时我加了两个内层while循环跳过重复值。这个细节很重要因为如果不跳过重复值数组里有多个相同元素时就会产生重复组合。比如[1, 1, 2, 2, 3]中找目标值4时如果不跳过重复就会得到[1, 3]和[1, 3]两个相同的组合。考后我复盘时发现这道题我虽然写对了但对“返回所有不重复组合”这个要求理解得不够充分。题目里的“不重复”有两种理解方式一种是不出现下标完全相同的组合另一种是组合的值不重复。我按后者实现了实际上前者的实现更简单只需要在找到组合后同时移动左右指针即可。但总之把边界情况处理到位是算法题拿高分的关键。5. 这次笔试暴露的知识盲区考后复盘的观察与反思笔试结束后我没有急着等结果而是花了大概两个小时把每道题重新过了一遍把记忆中的答案和标准答案做对比。这个复盘习惯是我从秋招开始坚持的每次笔面后都做效果好于盲目刷题。下面说说这次笔试暴露的几个知识盲区也是我后来重点补强的地方。5.1 HTTP缓存状态码与优先级的模糊选择题里有一道关于304状态码的题目我在场上纠结了一段时间。304Not Modified是协商缓存命中时服务器返回的状态码表示资源没有修改可以继续使用浏览器本地缓存的副本。但我当时的纠结在于Cache-Control和Expires同时存在时哪个优先级更高这个问题的答案是Cache-Control优先因为Cache-Control是HTTP/1.1的标准而Expires是HTTP/1.0的产物现代浏览器在两者同时存在时会忽略Expires。另外浏览器请求资源的完整流程是先检查强缓存如果命中了强缓存直接使用本地缓存不发请求如果强缓存未命中或已过期浏览器会带上If-Modified-Since或If-None-Match头向服务器发起请求服务器根据条件判断资源是否有更新没有更新则返回304有更新则返回200和最新资源。这个知识点的考试频率极高不仅在笔试面试也几乎必问。建议准备一张这样的表格来辅助记忆缓存类型关键字段命中时的行为状态码强缓存Cache-Control: max-age, Expires不发请求直接用本地缓存200 (from disk cache)协商缓存ETag / If-None-Match发请求服务器判断未修改304协商缓存Last-Modified / If-Modified-Since发请求服务器判断未修改304不缓存Cache-Control: no-store每次都从服务器获取完整资源2005.2 手写代码题的时间复杂度分析与优化习惯第三道算法题我能用双指针做出来说明基本的算法思维是有的。但复盘时我意识到我的思考过程仍然偏向“先暴力再优化”而不是一开始就根据输入特征选择最优算法。为什么会这样因为平时刷题时我习惯先用暴力解法跑通测试用例再去优化。笔试时面对同样的问题我也先想了一遍暴力解法然后才想到用双指针优化。这个顺序在平时练习中没问题但在限时笔试中会浪费时间而且可能会让你在代码注释里写出“时间复杂度O(n^2)”这种不是最优的描述给阅卷人留下不好的印象。正确的做法是拿到算法题后先看输入数据的规模和特征再决定算法方向。题目里已经说明数组是升序排列这是一个强烈暗示——排序数组配合查找优先考虑双指针或二分查找。如果数组无序就需要先排序或用哈希表来优化。小满这套笔试虽然有算法题但整体难度没有到大厂那种需要狂刷LeetCode才能通过的程度。比起算法更看重你对JS语言本身的掌握程度、边界情况的处理能力和代码可读性。不过从春招趋势来看前端岗位的算法要求确实在逐年提高如果想冲更好的平台LeetCode的Hot 100还是建议认真刷一遍。5.3 工程化知识在笔试题中的渗透这套笔试题里还有一个值得注意的倾向部分选择题和手写代码题其实在考察工程化相关的知识只是包装成了基础题的形式。比如防抖函数表面考察的是闭包和setTimeout用法实际是在考察你是否理解前端性能优化中“控制函数执行频率”这一核心思想。深拷贝函数表面考察递归实际是在考察你对不可变数据、引用类型赋值的理解以及是否能写出健壮的代码。小满做的是电商SaaS产品前端项目和大多数B端项目类似数据表格、表单、复杂交互、状态管理、性能优化。笔试题对这些能力的考察都比较贴合实际业务场景。这一点对备考有很强的指导意义——在准备笔试时不要只刷题还要理解这道题在工程中解决了什么问题。6. 给后续考生的备考建议从这场笔试延伸到整个春招作为一个经历过秋招和春招的人我想给准备Web前端岗笔试的同学一些具体可执行的建议而不是泛泛而谈。第一重视基础知识体系化不要东一榔头西一棒子。前端笔试的选择题虽然覆盖面广但并不是无边无际的。以这份试卷为例考察的知识点主要集中在ES6新特性、CSS布局与层叠规则、浏览器渲染与缓存、网络协议、Vue/React框架原理。把这些模块的知识点用思维导图或笔记整理成体系远比零散刷题有效。我在秋招时整理了一份前端知识大纲春招笔试前花一天时间过一遍帮助很大。第二手写题要练到“条件反射”的程度。深拷贝、防抖节流、数组去重、对象扁平化、实现Promise.all、发布订阅、instanceof实现等常用手写题建议在笔试前至少独立完成两遍。第一遍是在理解了原理后手写第二遍是限时默写。这里说的默写不是逐字背诵而是能在5-8分钟内写出一个考虑边界情况的版本。这次笔试的防抖题如果不是提前练过我很难在答题时间内想到加上cancel方法。第三算法题以数组、字符串、双指针、哈希表为主攻方向。对于前端岗位算法题通常不会出太难的数据结构题树和图偶尔会出现但频率不高。把数组和字符串的常见题型练熟性价比最高。刷题时建议按“题目类型”而不是“题号顺序”来刷比如集中练双指针、集中练滑动窗口这样能更快建立解题模式识别能力。第四注意代码风格和规范。笔试阅卷时代码风格会影响主观印象分。变量命名要语义化逻辑分支要清晰必要的地方写注释说明思路。不要为了追求代码行数短而使用一些晦涩的写法。比如深拷贝函数完全可以用一行JSON.parse(JSON.stringify(obj))但这样写不仅无法体现你对深拷贝原理的理解还会因为函数和特殊值无法被拷贝而扣分。笔试不是炫技场是展示工程素养的窗口。第五安排好答题顺序学会取舍。线上笔试一般不会告诉你每道题的分值但从题量和时间可以大致估算。我的个人习惯是先把有把握的选择题快速做掉拿到基本盘分数然后做手写题中自己有思路的先完成核心逻辑再填充边界处理最后如果有剩余时间回头处理拿不准的选择题。不要在一道题上死磕超过15分钟笔试的目标是总分最大化不是每题满分。第六注册一些在线刷题平台保持手感和限时训练。我在春招准备期每天都固定花一小时在牛客网或者LeetCode上刷题模拟真实的笔试环境。牛客网上的前端笔试题库和实际考试题风很像建议优先刷。做完题目后一定要看评论区的大神解法自己想到的是“能用的解法”看别人的答案才能学到“更好的解法”。小满科技的这次笔试从投递到收到笔试通知节奏比较紧凑。笔试结束后大概一周左右我收到了面试通知。如果你正在走春招流程建议投递后保持手机和邮箱畅通笔试前后也可以准备几个版本的自我介绍。毕竟笔试只是第一关过了笔试还有技术面和HR面整个流程打的是持久战。说完笔试本身最后再分享一个我个人的体会笔试刷题这件事看似是为了一次考试实际上是给整个职业生涯打底。前端入门容易想走深却需要持续学习。一份笔试题里看到的深拷贝、防抖、双指针后面都会以各种形态出现在实际需求和项目优化中。真正吃透了笔试只是顺手的事。