ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

小红书前端笔试复盘:考点解析与编程题实战思路

小红书前端笔试复盘:考点解析与编程题实战思路 谁还记得2023年秋招那阵子大家一边刷八股一边焦虑“HC到底还有没有”的状态。我这篇想聊聊当时参加的小红书前端岗第三批笔试题目不算偏但考察角度挺有代表性——它不问你背得多熟而是看你在真实业务场景里怎么用这些基础能力。如果你正准备投前端岗或者想了解大厂笔试到底筛什么这篇应该能给你一个比较完整的参照。先说一下背景。小红书的前端笔试走的是牛客网在线笔试系统第三批大概是九月中下旬题型分三块单选、多选、两道编程题。考试时间一共90分钟其中有20道选择题每题大概2分编程题占大头。整体难度在所有互联网公司里算中上比字节稍微温和一点但比普通中厂要扎实不少尤其是编程题两道题都有明显的业务场景影子。1. 整体考点分布与笔试策略拆解1.1 选择题考点统计与出题逻辑我整理了一下当时记下来的考点给后来人做个参考考点方向具体内容出题方式JavaScript基础事件循环、闭包、this指向、原型链代码输出题CSS布局flex布局、BFC、层叠上下文概念判断浏览器原理缓存、渲染流程、跨域单选/多选React/VuesetState机制、diff算法、响应式原理代码分析网络基础HTTP缓存、TCP握手概念题工程化webpack打包过程、sourcemap基础概念这里有个很有意思的现象选择题基本没有那种纯背诵的题目比如“http状态码404代表什么”这种送分题基本不存在它喜欢把知识点藏在一小段代码里让你推导输出。比如事件循环那道题给了一段setTimeout、Promise、async函数混合的代码让你写出输出顺序。这种题网上八股一搜一大把但真正做起来还是容易栽因为考点一旦嵌套在async/await里很多人对微任务队列的执行时机就会搞混。CSS部分考的也不浅。有一道关于flex布局的题给了一个容器设置了flex-wrap: wrap子项分别设置了flex-basis和flex-grow问在不同宽度下的换行表现。看似简单实际上把flex-basis、flex-shrink、flex-grow三者的关系全考进去了。还有一道BFC的题问的是怎么触发BFC以及BFC能解决什么问题选项里既有float、overflow这些常规手段也有position: fixed这种容易让人犹豫的选项。浏览器和网络部分是另一块比重比较大的内容。HTTP缓存那一题给了两次请求第一次响应头里有Cache-Control: max-age600第二次请求时服务端把ETag改了问会走什么缓存流程。这里要是不理解强缓存和协商缓存的判定顺序很容易选错。React部分倒是没考太偏一道setState是同步还是异步的题目外加一道diff算法中key的作用都属于基础中的基础但越基础越容易说错尤其setState那道题很多人只背了“React18以后自动批处理”但没理解在原生事件和异步任务里到底怎么表现。1.2 编程题的题型风格与考察方向小红书这两道编程题不是典型的力扣风格而是在力扣的基础上套了一层业务场景。第一道大概意思是有一个异步请求函数要求实现一个带并发数限制的调度器保证同一时间最多只有N个请求在执行某个请求结束后自动补下一个。这题本质上考察的是Promise、异步队列、并发控制这三个核心能力属于前端工程师日常开发中真的会遇到的场景比如批量上传文件、并发请求接口限制等等。第二道题是数组处理相关给一个二维数组要求按指定规则进行扁平化处理同时还要去重和排序。表面上看是个工具类题目难度不大但它对时间复杂度和边界情况的考察很看重普通解法能跑通部分用例但要拿满分需要注意细节比如大数情况下用Set去重和用对象去重的区别扁平化时用递归和用迭代的性能差异这些细节会直接决定能不能通过全部测试用例。从出题逻辑来看小红书的笔试更看重候选人的工程思维而非纯粹刷题能力。第一题是典型的“工作中会用到的工具函数实现”第二题则是在考查代码严谨性。所以备考时不能只盯着力扣热门题手写Promise、手写防抖节流、手写并发控制、数组工具函数这一类的题一定要重视。2. 核心考点深度解析与易错点复盘2.1 事件循环与Promise执行顺序问题先拿事件循环这道题说吧。类似这样的代码几乎是前端笔试必出console.log(1); setTimeout(function() { console.log(2); Promise.resolve().then(function() { console.log(3); }); }, 0); new Promise(function(resolve) { console.log(4); resolve(); }).then(function() { console.log(5); }); async function test() { console.log(6); await 7; console.log(8); } test(); console.log(9);正确答案是1、4、6、9、5、8、2、3。这道题看似简单但很多人会栽在第8个输出上。关键在于await 7这一行await后面的表达式如果不是Promise会立即执行但await之后的代码会作为一个微任务被调度。所以console.log(8)不会立即执行而是被推进微任务队列。很多人会把8放在5之前输出就是因为没搞懂await在这里的具体行为。我当时的分析思路是同步代码依次执行1、4然后Promise的executor里面resolve之后then回调被推入微任务队列但还没执行。test()调用时同步输出6await 7因为7不是Promise所以将其转换为Promise.resolve(7)并等待后面的8被推入微任务队列。继续执行同步代码输出9。主线程同步代码执行完毕开始清空微任务队列先执行Promise的then回调输出5再执行await之后的代码输出8。最后执行宏任务setTimeout回调输出2紧接着回调里的Promise.resolve().then被推入微任务队列并立即执行输出3。注意在浏览器和Node.js较新版本中微任务队列执行时机都是“当前宏任务结束、下一个宏任务开始前”。只要记住这个原则事件循环题基本不会错。2.2 flex布局与BFC的隐藏考点flex那道选择题题干大概是这样的一个容器宽度为600px里面有三个子元素基础宽度分别是200px、150px、100px设置了flex-wrap: wrap问当容器宽度缩到500px和400px时子元素分别怎么排列。这道题真正想考的其实不仅仅是flex-wrap而是flex-shrink的默认行为。因为默认flex-shrink为1所以当宽度不足时子元素会按比例缩小而不是直接换行。只有当缩小到最小内容宽度仍然无法放下时才会触发换行。很多备考的同学容易忽略一个点flex-shrink的基数是“flex-basis设置的值”而不是“内容实际宽度”。比如flex-basis: 200px的子元素在容器宽度不足时它是从200px开始缩的。如果能算出这个逻辑选项就很容易排除。BFC那道题正确答案是overflow: hidden、float: left或right、display: inline-block、position: absolute或fixed都能触发BFC。不过严格来说position: fixed在CSS2.1里没有明确说能触发BFC但在实际渲染里它和absolute一样会创建新的格式化上下文。这里要看命题人的意图如果选项里同时有absolute和fixed一般会选absolute因为这是规范里明确提到的。遇到这种题不要慌优先选根正苗红的选项。2.3 HTTP缓存流程的完整推导网络部分的缓存题几乎是所有前端笔试的标配。考的是这样一条链路第一次请求资源响应头Cache-Control: max-age600ETag: abc123。10分钟后600秒内再次请求该资源浏览器判断缓存未过期直接使用本地缓存不发请求到服务器这是强缓存命中。600秒后再次请求浏览器发现缓存过期携带If-None-Match: abc123请求服务器。服务器比对ETag如果资源没有变化返回304 Not Modified浏览器继续使用本地缓存如果资源有变化返回200和新资源浏览器更新本地缓存。这是标准流程但笔试容易错的地方在第二步。很多人以为强缓存命中也发请求只是不下载body。不是这样的强缓存命中时终端根本不发起网络请求直接从本地读缓存。而协商缓存命中时会发起一个请求服务器返回304此时传输的只有头部没有body耗时通常更短。这道题如果变形可以问Cache-Control和Expires同时存在时以谁为准这时候答案是Cache-Control优先级更高。如果问no-cache和no-store的区别记住no-cache不是不缓存而是每次都要向服务器验证no-store才是不缓存。2.4 setState同步还是异步取决于什么React那道题具体是问下面这段代码中handleClick执行完后count最终会变成多少function Counter() { const [count, setCount] useState(0); function handleClick() { setCount(count 1); setCount(count 1); setCount(count 1); } return button onClick{handleClick}{count}/button; }答案是1不是3。虽然setState调了三次但因为count在本次渲染中没有更新所以三次都是基于同一个旧值最终合并成一次更新。这个题目属于React 18之前的基础题React 18的自动批处理在Promise、setTimeout等异步场景下也会合并更新了但函数组件里连续调用setCount这种场景结果依然是1。另一道diff算法题目问key的作用是什么。标准答案是key用于识别节点是否发生变化帮助React在diff时复用DOM节点从而减少渲染开销。错误选项往往说“key能提高diff算法的时间复杂度”这是不准确的key不改变diff算法的时间复杂度它是用来优化节点匹配效率的。3. 编程题完整思路与逐步实现3.1 带并发数限制的异步调度器这道题是笔试里的重点值得完整写一下实现思路。题干大意是实现一个Scheduler类它能往里面添加异步任务同时最多执行N个任务执行完自动取下一个任务执行。先看一个典型的调用示例const scheduler new Scheduler(2); const task1 () new Promise(resolve setTimeout(() { console.log(task1); resolve(); }, 1000)); const task2 () new Promise(resolve setTimeout(() { console.log(task2); resolve(); }, 500)); const task3 () new Promise(resolve setTimeout(() { console.log(task3); resolve(); }, 300)); const task4 () new Promise(resolve setTimeout(() { console.log(task4); resolve(); }, 400)); scheduler.add(task1); scheduler.add(task2); scheduler.add(task3); scheduler.add(task4);当并发数限制为2期望的执行顺序是最开始task1和task2同时执行task2先完成500mstask2结束后自动执行task3300mstask3先于task1完成task3结束后执行task4。所以输出顺序是task2、task3、task1、task4如果task1耗时1000mstask4耗时400ms那task4也会比task1早完成最终是task2、task3、task4、task1。这道题的经典解法是利用Promise和递归。核心思路是用一个计数器记录当前正在执行的任务数。用一个队列存储等待执行的任务。每次add任务时先入队然后调用一个run方法尝试执行。run方法里如果当前执行数没有达到上限就出队一个任务执行数加一任务完成后执行数减一再递归调用run。完整实现class Scheduler { constructor(limit) { this.limit limit; this.queue []; this.activeCount 0; } add(task) { return new Promise(resolve { this.queue.push({ task, resolve }); this.run(); }); } run() { if (this.activeCount this.limit || this.queue.length 0) { return; } const { task, resolve } this.queue.shift(); this.activeCount; Promise.resolve(task()).then(() { this.activeCount--; resolve(); this.run(); }); } }这里有个关键设计add方法返回一个Promise并在任务真正执行完成后才resolve。这个在笔试时可能不会直接要求但为了代码严谨建议加上。否则调用方没法知道任务什么时候完成。另一个细节是run方法里的Promise.resolve(task())要这么写因为task可能返回的不是Promise或者可能抛异常。用Promise.resolve包一层可以保证拿到一个Promise对象异常也能被catch。当然更严谨的写法还要加上catch避免任务出错导致后面的run无法执行。这个实现虽然简单但有几个容易犯的错在add里直接执行task忽略了并发上限判断。任务完成后减计数但没接着调用run导致队列后面的任务永远得不到执行。没有处理task本身抛出异常的情况。提示如果面试官让你扩展成支持“任务超时取消”或者“任务优先级”本质都是在队列和调度逻辑上做文章核心的调度器骨架不变。3.2 数组扁平化、去重与排序的组合题第二道编程题给一个嵌套数组比如[1, [2, 3], [4, [5, [6]]], 7, 8]要求扁平化后用某种规则去重可能是取绝对值去重或者按某个字段去重最后按升序排序返回新数组。这道题看似简单但考察的恰好是前端基础是否扎实。最简单的写法function flatten(arr) { return arr.flat(Infinity); } function unique(arr) { return [...new Set(arr)]; } function sort(arr) { return arr.sort((a, b) a - b); }如果就这道题本身来说这样写没有任何问题。但笔试容易设坑的地方在于如果数组里的元素是对象或者要求去重的规则是“绝对值去重”那new Set就没法直接用了需要自己维护一个有序结构或者用Map。我看过网上很多笔试复盘这道题常见的低级错误是用Array.prototype.flat()时不传参数flat()默认只扁平化一层嵌套两层的数组就处理不了。很多人以为flat()默认是无限扁平化这是个知识盲区。如果要求在面试中手写一个等效的flatten用递归的方式function flatten(arr) { const result []; for (let i 0; i arr.length; i) { if (Array.isArray(arr[i])) { result.push(...flatten(arr[i])); } else { result.push(arr[i]); } } return result; }去重的部分如果不想用Set也可以用reduce和includes但时间复杂度会高一些。笔试没有明确禁止内置API时尽量用简洁的写法反而更稳妥因为出错的概率低。这道题真正的拉分点在于对边界情况的处理。比如空数组、数组里还有null和undefined、字符串数字混在一起、巨大的数字导致排序结果异常。这些情况平时练习时不太会注意但在笔试用例里很可能出现。另外要注意sort()默认是把元素转成字符串再排序。[1, 2, 10, 3].sort()会得到[1, 10, 2, 3]所以无论数组里是数字还是字符串都建议显式传比较函数不然容易翻车。这个细节在笔试题里经常被拿来挖坑。4. 笔试中那些容易忽略的失分细节4.1 时间分配与做题顺序的建议90分钟做20道选择题加2道编程题时间其实不算充裕。我当时的时间分配是选择题控制在35分钟内编程题每道20分钟剩余15分钟检查。听起来很理想但实际操作中选择题很容易超时尤其是一些代码输出题一旦中间卡住10分钟就没了。我的建议是拿到卷子先花1分钟快速浏览编程题心里对难度有个底如果编程题第二道明显比第一道简单可以倒过来先做第二道。笔试的分数是按通过用例比例给的不是按提交顺序先拿容易拿的分永远是优先级最高的事。很多同学有一个习惯遇到不会的选择题就死磕结果编程题没时间做这是最亏的。选择题不会的可以先随便选一个标记一下等编程题做完再回头改。一道选择题2分但编程题一个用例可能就值好几分要算清这笔账。4.2 答题系统与代码环境的坑牛客网的笔试系统有个特点编程题支持JavaScript但不支持ES Module也不支持require代码直接以脚本方式执行。所以你在本地写的import、export语句在牛客上直接报语法错误。这一点一定要提前适应平时在力扣刷题时用的也是这种“纯函数输出”模式但如果你习惯在本地用webpack环境写代码笔试前最好专门在牛客网上练几道题感受一下这个环境。另一个需要注意的点是本地运行node.js和牛客网页端运行代码的宿主环境不同。牛客的JavaScript环境是Node.js还是浏览器环境要看考试说明但大多数情况下是Node.js所以不涉及DOM操作。如果题目给了明确的函数签名直接按签名实现函数体就行不要自己包一层多余的代码。我见过有人在笔试时把数组输入解析写在代码里结果题目其实已经帮你解析好了函数参数直接就是数组多此一举反而出了错。答题之前一定要先看题目给的模板和输入输出约定。还有一个非常现实的坑牛客的在线IDE没有自动保存功能。如果你快交卷时浏览器崩了或者网断了代码就没了。建议每写完一道题先手动复制代码到本地存一份再继续做下一道。我当时就是这样做的虽然不能保证万无一失但至少多了一道保险。4.3 选择题里的“文字陷阱”与排除法技巧选择题里最容易失分的不一定是知识点不会而是选项看错。举例来说有一道题问“下列哪种方式不能触发BFC”选项里有overflow: hidden、overflow: auto、overflow: scroll、float: left。看起来前三个都能触发BFC但仔细想想overflow属性的值只要是非visible就能触发BFC所以这三个都能这题就不能选它们正确答案可能是某个设置display: inline的选项。这类题考的就是一个“看仔细”的能力。排除法在这种场景下非常管用。先把明显正确的排除掉再把明显错误的排除掉剩下的选项里再仔细分辨。一般两道三个选项之间差别很微妙这个时候不要靠猜回看题干看它问的是“能”还是“不能”、“包含”还是“不包含”这种关键词往往是解题的关键。代码输出题的技巧是不要真的自己脑内执行太容易出错了。笔和纸或者在线IDE的草稿纸功能画一个执行时序表把同步任务、微任务、宏任务分三列列出来每遇到一个异步API就登记一下最后再按队列顺序输出。这个方法虽然笨但正确率极高尤其是在事件循环这种复杂顺序题上非常管用。4.4 考前复习建议与资料方向结合这次笔试的考点分布给准备前端笔试的同学一个相对高效的复习方向。算法方面不用贪多把常见的数组操作、字符串处理、Promise场景题、深拷贝、防抖节流、柯里化、并发控制吃透基本能覆盖大部分笔试需求。力扣上没必要把Hard题全刷完把热题HOT 100里的简单和中等题刷两遍性价比最高。八股文的部分重点抓JavaScript基础、浏览器原理、CSS布局、React/Vue原理这四块。不是死记硬背而是每个知识点都要能用自己的话说清楚最好能写一个小demo验证。比如事件循环你自己跑一遍代码和看别人写的解析记忆深度完全不一样。在小红书笔试里项目经验没有直接考但它会影响你对场景题的理解。比如并发控制这道编程题如果你平时做过接口请求限流的工具或者写过批量上传组件看到题目会感到亲切很多因为你已经知道它的应用场景了。这也提醒我准备笔试不能只看题平时工程实践里的积累会在无意间帮到你。5. 笔试之后复盘比分数更重要笔试做完不是结束复盘才是真正能拉开差距的部分。我当时考完把记得的题目全部列了出来然后一道一道查漏补缺尤其是那些做错的选择题不只是把不会的知识点补上还追了一下背后的原理。比如flex-shrink那道题我后来专门去翻了MDN的文档把flex-basis、flex-grow、flex-shrink三者的计算关系理清了。这种深入式的学习比刷三套模拟题都有效。复盘时有一个技巧把错题按“知识点不熟”“粗心看错题干”“代码边界情况没考虑”三类归档。这样你就能清楚自己在哪一类问题上失分最多下次复习时更有针对性。单纯把错题抄一遍是没用的你要分析错因而不是只看答案。编程题复盘的方式是先自己重新写一遍然后去看讨论区里别人的解法特别是有没有人用了更简洁的方式比如用async/await改造并发调度器、用reduce实现扁平化。学习不同的解题思路能帮助你在下一次遇到同类题时反应更快。这里再分享一个我在笔试过程中积累的小技巧在牛客系统的编辑器里如果你用了ES6语法尽量不用那些特别新的特性比如可选链、空值合并虽然牛客支持但某些老版本Node环境可能不认。选择题怎么考察浏览器兼容性是一回事自己写代码时一定要保证在中低版本环境里能跑。笔试环境的坑能少踩一个是一个。最后再补充一点笔试不是面试的全部它只是一个门槛只要过了面试环节才是真正展现能力的地方。所以如果某场笔试感觉不理想不用太纠结把精力放在复盘上下一次笔试把这次踩的坑都避掉就足够了。我这套复盘方法陪我走完了整个秋招。希望这次笔试复盘的经验总结也能在后续大家的求职路上帮上一点忙。
返回列表