小红书前端秋招笔试复盘:从数组排序到大文件上传的实战解析

发布时间:2026/9/1 15:31:22
小红书前端秋招笔试复盘:从数组排序到大文件上传的实战解析 小红书前端岗的秋招笔试我印象里一直是“看着不难拿分不易”的类型。第三批笔试那天晚上我做完出来就一个感觉这卷子不是在考你会多少API而是在考你作为一个前端遇到一个从来没准备过的题目时能不能用最朴素的方式把问题解出来。这篇文章我就以第三批笔试为样本把当时的题目、我的解题过程、以及交卷之后复盘出来的教训全部摊开讲。写给正在准备秋招的各位也是给自己留一份完整的记录。1. 这场笔试的整体观感一场“广而深”的前端能力体检1.1 题型构成与答题界面第三批笔试是在牛客网系统上完成的整体界面和主流笔试平台没太大区别左边是题目列表右边是代码编辑区选择题可以直接点选编程题需要自己手写完整代码并运行测试用例。题型分布大概是这样的题型数量分值占比主要考察方向单选题15道约30%HTML/CSS、事件循环、作用域闭包、浏览器缓存、flex布局多选题5道约15%工程化、HTTP缓存、原型链、组件通信编程题2道约40%一道纯算法、一道结合前端场景的算法前端场景设计题1道约15%大文件上传、并发控制的方案设计这里有个很明显的信号它把“编程能力”和“前端专业能力”拆成了两条独立线来考察。纯算法题考察的是数据结构和逻辑基本功前端场景题考察的是你在真实业务里的工程判断。两条线任何一条太弱总分都会很难看。我记得当时选择题里有几道是老八股比如typeof null输出什么、Array.prototype.map第二个参数的作用、和的隐式转换规则这类题只要刷过面试题都能秒答。但多选题就比较阴了很多选项设计得模棱两可比如“关于事件委托的描述正确的是”A选项“事件委托利用了事件冒泡”B选项“stopPropagation可以阻止事件委托继续冒泡”你说B错吗它描述的是stopPropagation本身的行为但并不是事件委托的特性。这种题就是用来筛掉那些只背结论、不理解原理的人。1.2 题量与时间分配的实战印象整场笔试时间是90分钟。说实话如果选择题都靠条件反射来答编程题一次通过不调试时间是够用的但只要有一道编程题卡住后面就会非常紧张。我当时的节奏是这样先跳过所有需要动脑的题目用大约20分钟把选择题全部过了一遍能秒答的直接选拿不准的先标记。然后用了大约15分钟把第一道编程题写出来并通过测试再用20分钟写完第二道。最后留了约15分钟做场景设计题剩下的时间用来检查选择题和补充代码注释。这个时间分配的缺点是场景设计题只有15分钟写出来的答案比较紧凑很多细节没能展开。但优点是确保了40%分值的两道编程题稳妥拿到手。事后我复盘觉得这个策略还是对的因为编程题有测试用例能明确看到过没过而场景设计题主观性很强写得再多也不一定踩在考察点上。1.3 做题优先级选择题先扫编程题先想这里分享一个我在多次笔试里练出来的操作习惯选择题先做但不恋战编程题先看完所有题再动手。选择题里经常会有几道题非常偏比如CSS某个冷门属性的默认值、某个API在IE下的兼容行为。遇到这种题不要心疼直接选一个最像的答案然后标记把时间留给会做的题。因为笔试是算总分的一道选择题两三分一道编程题二三十分这个账要算清楚。编程题先看全部题目的意义在于你的大脑在写第一题的时候潜意识里其实已经在处理第二题的思路了。我自己很多次都是这样写完第一题切到第二题时脑海里已经有了一个大致的方案雏形省了不少读题时间。2. 试题逐题复盘算法题与前端场景题的双线考察2.1 第一道编程题数组排序去重不只考API这道题看起来非常朴素甚至有点让人怀疑是不是发错卷子了。原题大概是给定一个可能包含重复元素的整数数组要求返回去重并排序后的数组不能用Set和sort。对就是这么直白直白到把“不要用内置方法”几个字加粗放在题目里。我理解这个题的真实意图是判断一个人是只会调用API还是真的理解排序和去重的底层逻辑。我当时没有直接写快排而是先写了一个基于对象键值对的去重版本然后再实现排序。去重的核心思路是用一个对象或者Map记录每个数字是否出现过遍历数组把没见过的元素push进新数组这样可以做到时间复杂度O(n)。排序我选了快排因为手写起来结构清晰不容易出错。下面是我当时的写法做了些整理function uniqueAndSort(arr) { // 去重利用对象键唯一性避免使用 Set const seen {}; const uniqueArr []; for (let i 0; i arr.length; i) { const item arr[i]; if (!seen[item]) { seen[item] true; uniqueArr.push(item); } } // 排序手写快速排序 function quickSort(list) { if (list.length 1) return list; const pivot list[Math.floor(list.length / 2)]; const left []; const right []; const equal []; for (let i 0; i list.length; i) { if (list[i] pivot) { left.push(list[i]); } else if (list[i] pivot) { right.push(list[i]); } else { equal.push(list[i]); } } return [...quickSort(left), ...equal, ...quickSort(right)]; } return quickSort(uniqueArr); }这个写法不算最优但胜在稳定、不会写出边界bug。如果追求更好的性能去重可以用Map来避免对象原型链上的坑排序可以用更规范的原地快排。但笔试场景下代码可读性和正确性比极端性能重要得多。交卷后我复盘觉得这道题真正的坑点在于题目要求“不能用Set和sort”但没说不能用对象和数组内置方法。有人会用Array.from(new Set(arr)).sort((a,b)a-b)直接挂掉。所以这个题的核心不是考你会不会用API而是考你能不能在限制条件下另起炉灶。这对平时习惯sort一把梭的人很不友好。2.2 第二道编程题版本号比较排序第二道题给我的印象比第一道深得多因为它很贴近工程场景。题目描述大概是给定一组形如“x.y.z”的版本号字符串数组要求按从新到旧降序的顺序排序比较时每一位都按数字大小比较不能转换成浮点数比较。这种题在后端或运维场景里更常见但现在前端项目里同样会遇到比如版本更新提示、依赖版本对比、灰度策略里的版本区间判断。可以说是一个很有实际背景的题目同时也是一道典型的字符串处理 自定义排序规则题。我的第一版解法先拆后比function compareVersions(v1, v2) { const parts1 v1.split(.).map(Number); const parts2 v2.split(.).map(Number); const len Math.max(parts1.length, parts2.length); for (let i 0; i len; i) { const num1 parts1[i] || 0; const num2 parts2[i] || 0; if (num1 num2) return -1; // v1 更新 if (num1 num2) return 1; // v2 更新 } return 0; } function sortVersions(versions) { return versions.sort(compareVersions); }核心点在两个地方。第一版本号长度可能不一致比如“1.0”和“1.0.0”理论上它们是相等的。我使用Math.max拿到较长的那个长度然后把缺失位补0来处理这样“1.0”会等于“1.0.0”合理。第二不能直接转浮点数因为“1.10.2”转成浮点数会变成“1.102”而“1.9.1”是“1.901”看起来“1.902”比“1.901”大但真实比较时第二段9大于10所以“1.9.1”应该排在“1.10.2”前面。这个点如果没意识到写的代码在测试用例里必挂。这道题算是纯算法里的“应用题”不考什么高深算法考的是对字符串处理、边界情况、自定义排序规则的掌握。我在做的时候特意多测了几个边界用例包括“0.1”和“0.0.1”的比较、空字符串的情况确保没有遗漏。2.3 前端场景题大文件分片上传的完整设计这道场景题是整场笔试里最有“小红书风格”的一道。题目说现在要做一个支持上传大文件比如单个视频500MB以上的前端功能请设计技术方案。要求写出核心流程、涉及到的主要技术点以及断点续传的处理思路。说实话这种题平时如果没有做过类似功能非常容易抓瞎。因为你说不出“分片后每片多大”“如何计算文件指纹”“如何与服务端对接续传信息”这些细节就只能写“将文件切割成小块逐个上传”这种空话拿不到分。我当时的设计思路大概分五步。第一步是文件预处理。先读取文件的大小、类型、文件名前端就根据大小决定是否走分片流程。比如小于100MB的直接走普通上传大于100MB的走分片。分片大小我设定为10MB一片这样500MB的文件有50个分片既能保证单请求体积适中又不会因为分片太多导致请求数爆炸。第二步是计算文件唯一标识。利用File对象的lastModified和size拼接成一个字符串作为文件指纹如果后端需要更精确的校验可以用SparkMD5之类的库计算整个文件的MD5。不过计算大文件MD5会耗时所以更合理的方案是后端通过分片内容来确认文件唯一性前端只需要提供一个足够强的key即可。第三步是并发控制上传。因为浏览器的HTTP并发数是有限的你不能一次性把50个分片全发出去。我用了一个简单的线程池思路每次最多同时上传5个分片每完成一个就从任务队列里补一个。核心逻辑如下async function uploadWithConcurrency(slices, limit 5) { const queue [...slices]; const workers []; for (let i 0; i limit; i) { workers.push(consumeQueue()); } await Promise.all(workers); async function consumeQueue() { while (queue.length) { const slice queue.shift(); await uploadSlice(slice); } } }第四步是断点续传。核心思路是在第一步先向服务端发送一个“注册上传任务”的请求服务端返回一个uploadId。上传的过程中每当一片上传成功前端就把这片的状态记录下来。如果用户刷新页面、网络断掉重新进入页面上传同一文件时先通过文件指纹查询服务端拿到已经上传成功的分片序号只上传缺失的部分。第五步是服务端合并校验。所有分片上传完成后前端主动调用“合并分片”的接口服务端将所有分片按序号拼接成完整文件然后计算整个文件的hash和前端声明的hash做比对。如果不一致说明有分片损坏返回错误让前端重新上传损坏的分片。我当时在答题卡上还画了一个简版的时序说明把“获取uploadId→分片上传→完成合并”三步写清楚。这类题目不光要写结论更要写明白“为什么这样设计”——比如为什么分片大小选10MB而不是5MB或50MB为什么用5并发而不是直接全部并行。这些细节才是区分度所在。2.4 场景题里的隐藏知识点并发控制的具体实现做完上面的方案后其实题目还有一个延伸问如果服务端要求最多同时只能有3个分片在途请实现一个通用的并发控制函数。我当时因为时间不够只写了核心骨架function limitConcurrency(tasks, limit) { let running 0; let index 0; const results []; let completed 0; return new Promise((resolve) { function next() { if (completed tasks.length) { resolve(results); return; } while (running limit index tasks.length) { const current index; running; tasks[current]().then((res) { results[current] res; running--; completed; next(); }); } } next(); }); }这个实现是一个典型的窗口滑动式并发控制。它的关键在于用index维护任务的推进顺序用running维护当前在途任务数while循环保证只要还有空位就继续启动任务。Promise在最外层等待所有任务完成后再resolve。后来我再看这类题发现它的核心难点其实不在代码本身而在于两个容易被忽略的点一是越界处理也就是tasks[current]的current不能超过tasks.length二是对任务执行顺序的维持并发执行时先启动的任务不一定先结束所以results[current]这种按下标写入的方式比push更稳妥。这种场景题如果平时没有自己写过并发池仅靠背Promise.all的用法肯定写不出来。所以我在准备秋招后期专门花时间把“异步并发控制”从手动实现到各种变形练了一遍这个时间花得非常值。3. 最容易翻车的非技术细节环境、题型与时间分配的隐形坑3.1 牛客网编辑器提示弱手写代码别慌牛客网在线的代码编辑器体验和IDE相比差距还是不小。第一个问题是没有代码提示比如你敲document.querySelector它不会自动补全后面的API。这意味着你平时如果在VSCode里靠插件提示写代码到这里就会突然觉得自己“不会写代码了”。尤其是写比较多的DOM操作或正则表达式时函数名容易卡壳。这个只能靠平时多不依赖提示地写代码来弥补习惯就好。第二个问题是控制台报错信息不够友好。本地开发时Chrome的devtools会把报错位置、调用栈、变量值都标得很清楚但牛客网只显示一个简单的错误信息有时候连第几行报错都不标。遇到这种情况千万别慌多在代码里加console.log分段打印把数据流打出来通常比瞎猜更快定位问题。第三个问题是测试用例是隐藏的你只能看到“通过率xx%”。如果通过率不是100%说明有边界情况没过。这个时候我会做两件事一是检查输入格式比如readline读进来的数据是不是多了一个空格二是检查数组长度为1、数组为空、包含负数这些边界。很多题挂就挂在边界上。3.2 选择题里的“八股文”隐蔽坑笔试里的选择题我把它分成两类送分题和送命题。送分题就是正儿八经的基础题比如事件循环的输出顺序、闭包的内存回收、flex的flex: 1到底代表什么flex-grow:1; flex-shrink:1; flex-basis:0%。这类题只要你平时刷过面经基本能一眼看出来。送命题是那些“看起来每个选项都对吧”的题目。它们往往不是考一个孤立的知识点而是把好几个知识点混在一句话里。举个例子我记得有一道题问“关于Map和Object的区别以下哪些说法正确”有个选项说“Map的键只能是字符串或SymbolObject的键可以是任意类型”。如果对Map不熟很容易觉得这个对因为平时用Map时键确实是字符串。但实际上Map的键可以是任意类型包括对象、函数而Object的键会被强制转成字符串。这就是“背结论”和“理解原理”的区别。多选题比单选题更危险因为它是“少选不得分”还是“部分给分”不同平台规则不一样牛客网通常是不定项选多选、少选、错选都不得分。这意味着你在犹豫一个选项时最好的策略是不选它。宁可少得一道题的分也不要因为多选一个错误选项丢掉整个题的分。3.3 编程题的输入输出处理最容易白给的坑编程题用牛客网时输入输出是用readline和console.log来处理的。很多同学刷题用LeetCode习惯了函数式写法到了笔试考场反而被输入输出卡住。我记得当时的编程题输入是一个数组格式是[1,2,3,4]实际上读进来的是一个字符串。你需要先解析字符串把[、]去掉然后按逗号分割。这个操作看起来简单但在紧张状态下很容易漏掉某个边界情况比如数据里有负数用split(,)时负号会不会受影响不会因为负号在数字内部但如果你用正则匹配数字时写得不严谨-1就可能被拆成空串和1。我自己总结了一套稳定的处理模板const readline require(readline); const rl readline.createInterface({ input: process.stdin, output: process.stdout, }); function parseArray(str) { return str .replace(/[\[\]]/g, ) .split(,) .filter((item) item ! ) .map(Number); } rl.on(line, (line) { const arr parseArray(line); // 业务逻辑 });这个模板虽然简单但可以保证在大部分输入场景下不报格式错误。如果题目是多行输入就用一个数组缓存所有行在close事件后统一处理避免一行一行处理导致状态错乱。3.4 做题顺序决定你的得分下限我参加了三场秋招笔试一个很明显的感觉是做题顺序直接决定你的心态心态决定发挥。我的推荐顺序是先快速做完所有“看一眼就有答案”的选择题 → 做编程题 → 做完编程题再做需要思考的选择题 → 最后做场景设计题。这样安排有几个好处。选择题前20分钟基本属于“热手”阶段能快速进入状态拿到基础分后心里有底。做编程题时大脑处于最清醒的阶段专注力最强。而场景设计题往往是开放性的没有标准答案就算最后时间紧张写几个关键技术点也比空白好。最忌讳的顺序是一上来就死磕一道编程题做不出来也不跳导致后面所有题都匆匆忙忙。一道编程题卡30分钟的情况基本就等于这场笔试放弃了。4. 复盘后的能力补全从笔试题反推小红书前端的考察逻辑4.1 为什么会有“大文件上传”这类工程题考完这套题后我一直在想一个问题为什么小红书的笔试里会出现大文件上传这种工程向的题目后来逛论坛、看面经有点明白了。小红书的核心业务是图文和视频笔记用户上传图片、视频是最高频的操作之一。一个前端面试者如果对“大文件上传”“分片上传”“断点续传”完全没有概念说明他对真实业务场景缺乏思考。这类题目不是要考你背了多少API而是看你在面对一个实际产品需求时能不能给出可落地的技术方案。这和“给你一个数组你会不会排序”是两种完全不同的能力。所以准备这类题目时不要只背方案最好自己动手做一个demo。用Node起一个简易服务端前端用input标签选文件做分片上传、并发控制、断点续传。做一遍之后你会发现很多细节是只靠背书永远学不到的比如FormData的字段组织、服务端如何接收流式数据、文件读取进度的监听方式。4.2 从真题看小红书前端团队的技术栈倾向从这套笔试题可以隐约看出一些技术导向。首先它的编程题不偏不倚地考了版本号比较这种题目在业务中用得不多但在工程化、构建发布、依赖管理方面很常见。说明这个团队对工程化基础是有要求的。前端发展到现在单纯的页面编写已经远远不够构建工具配置、依赖版本治理、CI发布链路这些都需要技术基础来支撑。其次场景题考了大文件上传这类操作涉及前端与后端的协同、浏览器的运行机制、网络请求的管理属于典型的复合型工程问题。如果笔试按这个方向出题面试大概率也会围绕这个方向深入问比如“某个分片上传失败了怎么办”“并发数怎么选最优”“如何计算文件指纹”。所以笔试完别急着扔草稿纸后面面试很可能追问。再就是多选题里频繁出现的HTTP缓存、事件循环、原型链说明JavaScript基础还是会被反复考察的。这些知识点没有捷径就是要把原理吃透尤其要理解“为什么”而不是只知道“是什么”。4.3 笔试题与面试题的双向联动一个很实用的经验把笔试题当作面试题来准备。笔试考过的每一道题都当作面试可能被追问的题来准备。比如版本号比较这道题面试官可能会追问如果版本号里有字母前缀比如“v1.2.3”该怎么处理如果版本号有“beta”“alpha”后缀怎么办这些追问都是在考察你的代码有没有扩展性。大文件上传的场景题更是如此。面试官可能会从“分片大小为什么选10MB”延伸到“如果网络极差怎么办”“如果用户中途切换网络怎么办”。这些追问没有标准答案考的就是你临场思考的能力。我在准备时会把每个方案里的关键选择都问自己一遍“为什么”答不上来的就立刻去查或动手验证直到能自圆其说。5. 给下一届秋招人的备考路线建议5.1 按“笔试清单”逐项自查根据这次笔试的经验我整理了一个前端岗笔试自查清单也可以当作准备方向参考[ ] 数组字符串常用方法能手写实现去重、扁平化、排序、防抖节流[ ] 排序算法至少能手写两种以上快排、归并[ ] 版本号比较这类“实际应用类字符串处理”能独立写出[ ] Promise基础使用熟练能手动实现并发控制[ ] HTTP缓存机制理解准确知道强缓存和协商缓存的区别[ ] 事件循环能分析复杂异步场景的输出顺序[ ] 大文件上传方案能画出完整流程图并说明每一步[ ] 理解作用域、闭包、原型链不能只背概念[ ] 熟悉flex布局和CSS优先级规则能直接口算这里每一项都可以展开成专题学习。我备考时发现很多知识点是相互耦合的比如版本号比较涉及字符串处理和排序而排序又涉及稳定性问题稳定性问题又和数据存储有关知识链是连续的。5.2 手写能力的专项训练方法笔试备考和面试备考最大的不同在于面试时你可以边说边想笔试时你只有代码能证明自己。我建议在准备后期每天都固定手写几个常用功能不需要依赖IDE。我当时会随机抽题目经典排序、防抖节流、事件总线、深拷贝、Promise的all和race、数组扁平化每个都手写到能背下来的程度。一开始很痛苦写一遍卡壳一遍但坚持两周之后手写代码的速度和准确度都有了非常明显的提升。另一个更接近实战的做法是在LeetCode上筛选“前端高频题”用在线编辑器刷题不跳坑。LeetCode的输入输出已经封装好了但牛客网没有所以我还专门去牛客网刷了十几道输入输出格式题适应了读行、解析、输出的过程。5.3 两场笔试复盘后我最大的感悟写这些东西的时候第三批笔试已经过去很久了但它对我的影响一直都在。如果说有什么最想分享给后来人的经验那就是大厂的校招笔试考察的不是你刷了多少道题而是你在完全没有准备过的限制条件下能不能靠已有的知识体系搭建出解决方案。版本号比较、大文件上传这些题都不是“背熟悉了就知道答案”的题它需要你真正理解编程语言的特性、数据结构和网络机制然后用最朴素的方式组合起来。这个能力不是考前突击能获得的需要靠平时多写代码、多思考方案来积累。如果你现在正准备秋招我的建议其实就一句话别把笔试当成一场考试把它当成一次能力体检。通过笔试找到自己的薄弱点然后有针对性地补强这个价值比单次笔试通过与否大得多。知识点漏洞是补不完的但解题的思维框架一旦建立起来无论遇到什么题你都能找到下手的角度。祝各位都能拿到心仪的offer。