前端春招笔试复盘:从JS基础到手写代码的完整解析
2026/8/29 23:39:48 网站建设 项目流程

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的比较才在一个上下文中进行。

层叠上下文的形成条件包括:

  • 根元素(html)
  • position值为absolute或relative且z-index不为auto
  • position值为fixed或sticky
  • flex容器的子元素且z-index不为auto
  • opacity小于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声明的变量则会被提升并初始化为undefined:

console.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状态码的题目,我在场上纠结了一段时间。304(Not 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每次都从服务器获取完整资源200

5.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面,整个流程打的是持久战。

说完笔试本身,最后再分享一个我个人的体会:笔试刷题这件事,看似是为了一次考试,实际上是给整个职业生涯打底。前端入门容易,想走深却需要持续学习。一份笔试题里看到的深拷贝、防抖、双指针,后面都会以各种形态出现在实际需求和项目优化中。真正吃透了,笔试只是顺手的事。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询