小红书2020校招前端笔试题卷二,在当年的校招群里流传度非常高。我记得好几个学弟学妹轮番来问我同一个问题:这套卷子该怎么准备?我翻完流传出来的题目版本之后,最大的感受是它没有一句废话——闭包、this指向、事件循环、跨域、渲染机制、手写代码,几乎把前端基础里最容易出错的硬骨头都点名了一遍。哪怕现在已经不是2020年,前端技术栈一直在变,但这套卷对准备校招、跳槽和想系统性自查基础的人来说,依然是一份很好的体检清单。它能帮你快速找到那些“我以为是会的,其实根本不懂”的地方。
如果你正在准备前端岗位的笔试面试,或者工作两三年后感觉自己基础不牢,都可以拿这套题做一次自测。下面我会按模块拆解卷子里最容易考、也最容易丢分的几个方向,把核心原理、答题思路和当时同学踩过的坑一起说清楚。
1. 试卷结构与考察思路拆解
1.1 卷二在整轮笔试中的定位
小红书2020校招前端笔试,实际投放的不止一套卷子,卷二属于典型的基础综合卷,逻辑是在简历筛选之后、面试之前,先用机器或者人工快速筛一遍候选人的下限。这类卷子不会故意出偏题怪题,但普遍题量大、覆盖广,限时内想拿高分并不轻松。当时我拿到流传出来的版本,印象最深的是题目层级很分明,大致可以分成三类:选择题负责考察概念辨析和语法易错点,包括单选和多选;简答题负责考察理解深度,比如让你解释某个机制或者某个方案;编码题则直接要求手写实现,比如防抖节流、数组去重、深浅拷贝等。
选择题测的是“知不知道”,简答题测的是“理不理解”,编码题测的是“能不能写出来”。这三个层次层层递进,比单纯刷算法题更能反映一个前端候选人的真实水平。所以如果你只是在牛客、LeetCode上狂刷题,忽视了对知识体系的理解,卷二这类试卷很容易让你原形毕露。
1.2 校招笔试真正想看到的答案长什么样
很多同学容易陷入一个误区:简答题写得越多越好,最好把所有相关名词都堆上去。其实打分的人更想看到“先说什么、后说什么、为什么这样设计”的逻辑链条。举一个高频简答题为例,“跨域有哪些解决方式”。如果只是列出来JSONP、CORS、postMessage、代理转发,那只能算及格。真正能拿高分的答法,是先点出跨域的本质是同源策略对“协议、域名、端口”的限制,再分别说明每种方案适合什么场景,最后给出当前项目里的推荐做法和注意事项。
卷二里的简答题占比不低,答法不同,分数差距会很大。我建议平时复习时不要只背面经,可以试着把每个知识点当成一个“小报告”,先定义、再解释原理、最后给场景,这样无论题目怎么变形,你都能答到点子上。
1.3 考点分布速查表
| 考察模块 | 常见题型 | 推荐优先级 |
|---|---|---|
| JavaScript 基础 | 选择、简答、编码 | 极高 |
| 浏览器与网络 | 选择、简答 | 高 |
| CSS 与布局 | 选择、编码 | 高 |
| Vue/React 框架 | 选择、简答 | 中高 |
| 手写代码 | 编码 | 极高 |
这个优先级参考了近几年的校招笔试题和面经统计,不一定完全对应某一次考试,但大方向是稳定的。考生复习时应该优先保证JavaScript和手写代码的正确率,因为它们几乎每套卷都会出现,而且一旦写错,基本没有蒙对的概率。
2. JavaScript 基础:闭包、作用域与this指向
2.1 闭包经典考法:从输出结果到修复代码
卷二在JavaScript部分最常出现的是闭包相关题目。闭包的经典考法有两种:一种是问输出结果,另一种是让你修复代码中的问题。问输出结果几乎是绕不开这个场景的:
for (var i = 0; i < 5; i++) { setTimeout(function() { console.log(i); }, 1000); }这段代码最终不是打印0、1、2、3、4,而是一秒后连续打印五个5。原因有两层:第一,var声明的i是函数级作用域,循环体里外共用同一个变量;第二,setTimeout的回调是在循环完全结束之后才执行的,此时i已经自增到了5。只要抓住“变量作用域 + 异步执行时机”这条主链,类似题目基本不会再错。
考试里还会继续追问:改成let i为什么就对了?因为let在每次迭代中都会创建一次新的词法环境绑定,闭包捕获的是当前这一次的i,而不是循环结束之后的最终值。另一个常见修法是用立即执行函数,把i作为参数传进新函数作用域:
for (var i = 0; i < 5; i++) { (function(j) { setTimeout(function() { console.log(j); }, 1000); })(i); }如果考简答,大概率会让你解释闭包是什么、为什么存在、有什么坑、怎么解决。要答得漂亮,最好讲清楚三点:闭包能在函数外部访问到其内部作用域的变量;它能让变量长期驻留,所以常被用来封装私有状态;但如果使用不当,也可能造成内存泄漏或循环引用。
2.2 this指向:不要背口诀,要会看调用方式
this指向问题是前端笔试的常青树,卷二里有个很经典的嵌套函数版本:
var name = 'window'; var obj = { name: 'obj', getName: function() { return function() { return this.name; }; } }; console.log(obj.getName()());这段代码最终打印的是window,而不是obj。原因是函数调用时,this指向取决于调用者。嵌套的那个匿名函数是独立调用的,没有对象前缀,所以this指向全局对象(浏览器环境就是window)。这类题目经常搭配三种修法来考:把外层this保存到一个变量里,比如var self = this;改用箭头函数,因为箭头函数没有自己的this,它从定义时的外层作用域继承;或者在返回函数时用bind(this)。
很多同学喜欢死背“谁调用就指向谁”的口诀,但它的例外实在太多。我建议用更可靠的判定方法,也就是看函数的调用表达式属于哪一种:
fn()直接调用,非严格模式下this指向全局对象。obj.fn()方法调用,this指向obj。new fn()构造调用,this指向新创建的对象。fn.call(obj)/fn.apply(obj)/fn.bind(obj)显式绑定,this指向指定的obj。
把这四种情况分清楚,再遇到任何this题都能慢慢推出来。卷二里还常把this和原型链结合,出一道输出题,所以先复习好调用方式,后面再遇到更复杂的场景也不容易翻车。
2.3 作用域提升与块级作用域:最容易被忽略的送分题
除了闭包和this,作用域提升也是一道常见的选择题。典型的题目是:
console.log(a); var a = 1;控制台输出的是undefined,而不是报错。原因在于var声明会被提升到当前作用域顶部,但赋值并没有被提升,所以执行到console.log(a)时,a已经存在,值却是undefined。这里需要注意,如果换成let a,控制台就会直接报错,因为let存在暂时性死区,在声明之前访问变量是不允许的。
这些知识点单独拎出来都不难,但在限时笔试的高压环境下,很多同学会凭第一印象选出错误答案。我自己的习惯是,遇到这类题先在草稿纸上把“声明提升、赋值执行、作用域链查找”三个过程画出来,顺序捋清楚再选,正确率会高很多。
3. 浏览器与网络:从渲染原理到跨域方案
3.1 渲染流程、重排重绘与性能优化
浏览器原理中,渲染流程绝对是高频考点。简化的过程是:HTML解析成DOM树,CSS解析成CSSOM树,两者结合生成渲染树,然后计算布局、绘制合成。这个流程本身不难背,难的是把它和相关问题串起来。比如卷子里可能问“为什么操作DOM会影响性能”,答案不是简单说“会重排重绘”,而是要说明:任何改变DOM大小、位置、结构的行为都可能触发重新布局,而布局计算会阻塞渲染线程,所以频繁操作DOM累积下来就会非常卡顿。
更深入的追问可能是“如何减少重排”。比较靠谱的答案包括:批量修改DOM,可以用documentFragment先积攒再一并插入;用CSS类一次性替换多个样式,而不是逐条修改内联样式;对动画元素使用transform或opacity,让它独立成合成层,减少对主线程布局的触发。这些实战技巧在笔试里很加分,因为阅卷人一眼就能看出你是不是真的处理过性能问题。
3.2 跨域的源头与主流解决方式
跨域在卷二里出现频率很高,很多考生能列举JSONP、CORS、postMessage,但一到简答题就说不清为什么会有跨域。它的根源是浏览器的同源策略,目的是防止一个源随意读取另一个源的数据。同源要求协议、域名、端口三者完全一致,任何一项不同都算跨域。
理解了这个前提,再去区分方案就清晰很多:
- JSONP利用的是script标签不受同源限制的特性,只能发GET请求,适合接口比较简单的老系统。
- CORS是通过后端在响应头里声明允许的源,这是现代Web应用的标准方案。需要区分简单请求和非简单请求,非简单请求会先触发OPTIONS预检。
- 代理转发适合开发环境和同源部署场景,让前端请求打到同源后端,再由后端转发到目标接口,这样浏览器层面就不存在跨域问题。
选择题里常见的坑是混淆“跨域能不能发请求”和“跨域能不能读到响应”。实际上非简单请求会先发出预检请求,真正的报错往往发生在响应被拦截阶段,而不是请求没有发出去。只要把这一点理解到位,跨域选择题的准确率能提升不少。
3.3 事件循环与微任务宏任务:别在嵌套里算错顺序
浏览器部分大概率会有一道事件循环题。核心知识点是:JavaScript是单线程的,同步代码先执行,遇到异步任务会交给浏览器其他线程处理;异步任务完成后,回调会被放进任务队列;在执行栈清空后,浏览器先处理微任务队列,再取出一个宏任务执行,循环往复。
经典的输出题形如:
console.log(1); Promise.resolve().then(() => console.log(2)); setTimeout(() => console.log(3), 0); console.log(4);结果是1、4、2、3。原因就是Promise的then属于微任务,setTimeout的回调属于宏任务,微任务会在下一个宏任务之前先执行。如果再混入async/await,会更容易算错:
async function test() { console.log(1); await Promise.resolve(); console.log(2); } console.log(3); test(); console.log(4);这里的输出顺序是3、1、4、2。注意看,调用test()后,函数体里的console.log(1)是同步执行的,遇到await才把后续代码异步化,所以主线程继续往下执行了console.log(4),然后才轮到微任务里的console.log(2)。我的笨办法是拿到题目先标记同步代码,再把宏任务和微任务分别列成小队列,最后按顺序执行,基本不会错。
3.4 HTTP缓存与存储,选择题里的隐形考点
除了渲染和跨域,浏览器部分还经常穿插HTTP缓存和存储相关的选择题。缓存方面要能清晰区分强缓存和协商缓存:强缓存直接通过Cache-Control或Expires判断缓存是否有效,有效期内不会发请求;协商缓存则需要带上Last-Modified或ETag去服务器验证,服务器返回304则继续用缓存。存储方面,cookie、localStorage、sessionStorage、IndexedDB四者的区别也是高频题,考试时经常拿“容量限制”“是否随请求发送”“是否自动过期”这几个维度做混淆选项。这些知识看起来零散,实际上背清楚一张对比表就能稳稳拿到分。
4. CSS与布局:BFC、flex与经典布局实战
4.1 BFC:一个独立渲染小世界
CSS部分,卷二对BFC的偏爱非常明显。BFC全称是块级格式化上下文,你可以把它理解为一个独立的渲染小世界,在这个小世界里,元素的布局不会影响外部,外部也不会影响它内部。哪些情况会触发BFC?浮动、绝对定位、inline-block、overflow不为visible的块级元素、flex容器等都会触发。
卷子里常考的题目是“如何清除浮动”,答案里一定会提到overflow: hidden。但你要能解释它背后的原理:给父容器创建了一个BFC,让浮动元素也参与父容器的高度计算,从而把子元素“包”起来。另一个高频考点是“如何防止margin合并”,同样可以通过在父容器上创建BFC,让相邻元素的垂直margin被隔离在独立的上下文中。
这里我把容易混淆的触发条件整理成一个清单:
float不是none。position是absolute或fixed。display是inline-block、table-cell、flex或grid。overflow不是visible。
考试时问到某个元素是否创建了BFC,直接按这几个条件套就能判断。
4.2 盒模型:一个必拿的基础分
盒模型虽然简单,但错的人非常多。标准盒模型里,width只包含内容区,而IE盒模型(box-sizing: border-box)里,width包含了内容、padding和border。卷二选择题特别喜欢给一组场景,比如“给一个元素设置width: 100px; padding: 20px; border: 5px,标准模型里的真实占位宽度是多少”,很多人一紧张就算错。
其实答案很简单:标准模型下真实占位宽度是100 + 20*2 + 5*2 = 150px;如果是border-box,真实占位宽度就是100px。这类题没有任何难度,只要在读题时先确认一下是否设置了box-sizing,就能稳拿分。
4.3 垂直居中与两栏布局,高频编码实操
布局题也是卷二的高频题,尤其是“垂直居中”和“两栏/三栏布局”。垂直居中看似简单,但不同场景对应不同最佳解。父容器宽高未知,子元素尺寸也未知,最推荐flex方案:
.parent { display: flex; justify-content: center; align-items: center; }如果要求兼容老浏览器,可以用绝对定位加负margin,但前提是子元素尺寸已知。还有基于transform: translate(-50%, -50%)的方案,它的百分比是相对于元素自身尺寸计算的,所以不需要知道具体宽高,这也是它适合当作通用方案的原因。
两栏布局里,经典做法是左侧固定宽度,右侧自适应。用flex实现很简单:左边flex: 0 0 240px,右边flex: 1。三栏布局则常考圣杯布局和双飞翼布局,目的是让中间栏先渲染,再通过负margin或padding把两侧让出来。要理解这类布局,核心是吃透“负margin会改变元素在文档流中的占位位置”,剩下就是组合游戏。死记代码很容易忘,但把原理想明白之后,任何变体都能应对。
5. 框架知识:Vue/React 核心机制
5.1 Vue的响应式原理:从Object.defineProperty到Proxy
2020年的笔试题,框架部分默认你已经会Vue或React。Vue高频题是“响应式原理是什么”,要答清楚它不是魔术,而是数据劫持加发布订阅。2.x版本用Object.defineProperty遍历对象属性并定义getter/setter,页面读取数据时收集依赖,数据变化时派发通知,触发重新渲染。3.0版本改成用Proxy,它能拦截更多操作,比如属性的新增和删除,同时性能更好。
卷子里如果出这类题,很可能搭配一个实操问题:给Vue 2的数组新增元素,为什么页面不更新?原因在于Object.defineProperty无法拦截通过索引设置新值这种操作,所以需要借助$set或者重写过的数组方法来触发更新。很多同学只背了“数组要用$set”这个结论,却不知道背后的边界在哪里。我的建议是,复习时亲手写一个小例子,在Vue 2和Vue 3里分别改数组,亲眼看看输出差异,这比背十篇面经都更牢固。
5.2 虚拟DOM与diff:为什么key不能乱用index
React和Vue都会聊到虚拟DOM。它到底是什么?可以理解为用JavaScript对象来描述DOM结构,一次界面渲染对应一棵虚拟节点树。为什么需要它?因为直接操作真实DOM成本高,而操作JavaScript对象是纯内存操作,非常快;再加上diff算法能找出前后两棵树的差异,只更新真正变化的部分,整体性能体验更好。
卷子里常考的并不是diff的每行代码,而是它的基本策略:同层比较、tag不同直接替换、key用于尽可能复用节点。关于key,高频追问是“为什么列表渲染时不能滥用index作为key”。因为在数组头部插入或删除元素时,index会整体变化,导致原本被复用的节点错乱,轻则渲染状态不对,重则出现难以排查的交互问题。这个场景不需要高深技术,用一个todo列表自己操作一遍就能理解。
5.3 生命周期与组件通信
框架部分的简答题,还经常围绕生命周期出题。Vue里最常见的追问是created和mounted的区别:created时数据已经初始化,但DOM还没挂载,适合在这里发请求或初始化数据;mounted时DOM已挂载,适合做依赖真实DOM的操作。React那边则常问函数组件里useEffect如何对应类组件的生命周期,以及为什么要清理副作用。
组件通信也是必考方向,Vue里的props/emit、provide/inject、Vuex,React里的状态提升、Context、Redux,都要能说出各自的适用场景。答这类题不需要长篇大论,核心点出“单向数据流”和“状态管理解决什么问题”就够了。
6. 编码题实战:手写实现与算法基础
6.1 手写防抖与节流:三个细节决定分数
编码题部分,手写防抖和节流几乎是保留节目。防抖的核心思想是:事件触发后等待一段时间再执行,如果在这个等待时间里再次触发,就重新计时。节流的核心思想是:在一段时间内只允许执行一次。它们分别适合什么场景?输入框实时搜索适合防抖,因为用户连续输入时不需要频繁请求;滚动监听和按钮防连击适合节流,因为需要保证一定的执行频率。
防抖的经典实现是这样的:
function debounce(fn, delay) { let timer = null; return function(...args) { const context = this; clearTimeout(timer); timer = setTimeout(() => { fn.apply(context, args); }, delay); }; }这里有三个细节很容易丢分。第一,要保留this指向,所以用了apply;第二,要透传参数,所以用了...args;第三,是否能说出“什么是立即执行版本、什么场景需要立即执行”,这决定了你是背代码还是真掌握了原理。节流的实现也有时间戳和定时器两种常见版本,考试时能说出两者区别(第一次是否立即执行、最后一次是否补执行)会非常加分。
6.2 数组去重与扁平化:从一行API到进阶追问
数组去重是编码题的另一个常客,最简单的版本是:
const unique = arr => Array.from(new Set(arr));一行代码就能解决。但你能讲讲它的局限吗?Set去重使用类似全等的规则,对基本类型没问题,对对象类型则只能比较引用,所以两个内容相同但引用不同的对象都会被保留。如果要求对象数组按某个字段去重,就需要用Map来记录。再往下考,数组扁平化也有多层考法:基础版是直接调用flat()方法,进阶版是手写递归,再进一层是支持指定深度。这些题目表面看是考API,实际是考你能否在特定限制下写出清晰可靠的代码。
6.3 深拷贝、手写call/apply/bind
卷二的手写题还可能涉及深拷贝和手写call/apply/bind。深拷贝的面试题很考验功底,基础版是递归遍历属性,进阶版需要处理数组、Date、RegExp等特殊类型,还要用WeakMap缓存引用,避免循环引用导致死循环。手写call的思路则是利用临时属性改变this指向:
Function.prototype.myCall = function(context, ...args) { context = context || window; const fn = Symbol('fn'); context[fn] = this; const result = context[fn](...args); delete context[fn]; return result; };这里用Symbol是为了避免覆盖context上已有的同名属性。其实这类手写题对日常业务开发帮助不大,但它在笔试里能快速检验一个人对JavaScript底层机制的理解程度。准备方式是理解思路后亲手敲三遍,而不是把代码背下来。
6.4 算法题策略:别输在“看得懂却写不出”
算法题在卷二里的难度一般不会达到LeetCode困难题,更多是链表反转、二叉树层序遍历、最长不重复子串这类中等偏下题目。很多同学的问题不是没有思路,而是没有把边界条件、空值处理、循环终止条件写完整。我建议刷题时不要只看题解,要动笔把主路径写出来,再补边界。笔试时间有限,遇到算法题先花两分钟确认输入输出,再写一个能跑通的主路径,最后补边界,这个顺序比追求最优解更实际。
7. 常见问题与排查技巧实录
7.1 考生最容易丢分的三个习惯
我翻过不少同学的笔试复盘,发现丢分点往往不在知识本身,而在表达和代码习惯。第一个坏习惯是简答题写太短,只给结论不给过程,比如“跨域用CORS”,却没有说明场景和限制。第二个坏习惯是代码题一上来就封装各种工具函数,结果主功能反而没写完,正确策略是先实现核心流程,保证基本用例通过,再考虑封装扩展。第三个坏习惯是不检查边界,比如防抖函数没有处理this、数组去重没有考虑空数组,这些小问题会让阅卷人对工程素养打个问号。
7.2 时间分配与答题顺序建议
根据卷二这类基础综合卷的题量和难度,我的建议是:拿到卷子先快速扫一遍全部题目,估算每种题型的大概耗时。选择题不要犹豫太久,先按第一直觉选,不确定的做标记;简答题控制在每题十分钟以内,写出核心逻辑链;编码题给足时间,最好留出二十分钟做检查。做题顺序上,建议先做自己有把握的模块,把分数先拿到手,再回头啃硬骨头。笔试往往按总分筛选,稳拿的分比钻牛角尖更有价值。
7.3 常见问题速查表
| 问题 | 常见原因 | 解决办法 |
|---|---|---|
| 事件循环输出顺序算错 | 混淆微任务与宏任务 | 先列同步代码,再分别收集两个队列 |
| 闭包打印全是循环末值 | 没理解var作用域与异步时机 | 改用let或IIFE |
| 垂直居中布局失效 | 父容器或子元素尺寸约束不一致 | 确认flex三要素,检查子元素是否被压缩 |
| 跨域请求报错 | 忽略预检请求或响应头配置 | 分清简单请求与非简单请求 |
| 数组增删后列表渲染错乱 | key用了index | 改用业务唯一id |
这张表是我根据同学常问的问题整理出来的,不一定覆盖全部,但能解决八成笔试现场出现的低级失误。
7.4 手写题的一个小技巧
最后分享一个我自己用过很多次的小技巧:手写题如果一时想不起具体API,不要空着。先把函数签名写出来,把注释写好,再用伪代码表达思路。阅卷者通常会给“思路正确但实现有瑕疵”的答案额外同情分。比如防抖你忘了clearTimeout,那就写“清除之前的定时器”这种注释,至少能让阅卷人知道你的方向是对的。卷面整洁、命名规范,在同等条件下真的能拉开差距。
小红书这套2020年的前端笔试题卷二,我后来给身边准备跳槽的同事也用过。大家的共同感受是:题目不算难,但很能暴露基本功。闭包、事件循环、BFC、跨域这些知识点,平时写业务的时候感觉都用不上,可一旦被问到,才发现自己只是眼熟而不是掌握。准备这类笔试,最忌讳的就是只看面经背答案。我的体会是,把每个考点都自己动手验证一遍,比如在浏览器控制台跑一遍this指向的代码,看看输出到底是什么;把防抖节流的调用方式写进一个真实的小项目里,感受它们在网络请求上的区别。这样学一遍,比考前突击三十道题都管用。希望这篇拆解能帮你在下一次笔试里,少踩几个我已经替你踩过的坑。