这份试卷我后来反复看了好几遍,也拿给身边正在准备秋招的朋友们做过。说实话,它不算刁钻,但覆盖面很广,JavaScript基础、浏览器原理、框架认知、工程化都有涉及,有些题表面在考API,实际在考你对运行机制的理解。如果你正打算投小红书的前端岗位,或者想摸底一下自己在校招水平线上处于什么位置,这份卷子的参考价值是很高的。这篇复盘我不会只贴答案,而是把每道题背后的考察意图、常见的错误思路、以及我当时是怎么一步步推理的过程都写出来,尽量让每个结论都有据可循。
1. 整体风格:这份卷子想筛选什么样的人
看一套笔试题,先别急着做题,要站在出题人的角度想一个问题:这家公司希望招到什么样的前端?从这个角度切入,整张卷子的逻辑会清晰很多。
小红书的前端技术栈以React为主,页面偏重交互体验和内容展示,对动画、性能、渲染效率的要求都不低。所以这套笔试题明显不是那种"刷完题库就能过"的类型,它更关注三件事:
- 基础是否扎实:JS核心概念、异步机制、作用域、闭包这类底层知识占比很大,说明他们默认你必须有扎实的语言功底。
- 有没有真实项目经验:部分题目不会直接问"怎么做",而是给你一个场景,让你判断哪种方案合理。这种题靠背概念是答不好的,必须真的在项目里踩过坑才能给出有说服力的答案。
- 代码风格和工程意识:手写题不只看你写不写得出来,还看你的变量命名、边界处理、注释习惯,这些细节在批卷时其实很加分。
我估了一下,整份卷子如果正常发挥,难度属于中等偏上。不算难,但想拿高分需要复习得比较系统,靠临时抱佛脚或者只刷 Vue 相关题的话会有些吃亏。
2. 代码输出题:JavaScript 运行机制的"照妖镜"
这类题几乎是所有前端笔试题的标配,但每个公司考的角度不太一样。小红书这份卷子里的代码输出题,给我的感觉是:它们不考冷门语法,而是考你平时写代码时最容易想当然的地方。
2.1 典型题一:var、let 与闭包的组合拳
题目大概是这样的:
for (var i = 0; i < 5; i++) { setTimeout(function() { console.log(i); }, 100); }这道题已经被写烂了,但我发现每次校招都还有不少人掉坑里。输出结果是 5 个 5,而不是 0、1、2、3、4,原因是var声明的i是函数作用域,循环结束时i已经变成了 5,而setTimeout的回调在未来某个时刻才执行,此时读取的i自然就是 5。
但小红书这道题没有止步于此,它接着问了一个跟进题:如果换成let,输出会是什么?为什么?
for (let i = 0; i < 5; i++) { setTimeout(function() { console.log(i); }, 100); }答案是 0、1、2、3、4。关键点在于let在每次迭代时会创建一个新的绑定,也就是说每一轮循环的i都是独立的一份副本,闭包捕获的是当轮的值,而不是共享同一个变量。这个区别本质上不是"let 更聪明",而是 JS 引擎在底层为let做了类似"每轮新建作用域"的处理。
我当时做题时习惯把这个过程画出来:每轮循环相当于一个独立的小房间,房间里放着一把写着当前i值的椅子,setTimeout的回调只认自己房间里的那把椅子。var版本则是一间大房子,所有人共享一把椅子,最后椅子上的数字被改成了 5,所以大家看到的都是 5。
2.2 典型题二:事件循环与 Promise 的执行顺序
这道题我印象很深,因为它的答案具有迷惑性,很多人第一眼会想当然。
console.log('script start'); setTimeout(function() { console.log('setTimeout'); }, 0); Promise.resolve() .then(function() { console.log('promise1'); }) .then(function() { console.log('promise2'); }); console.log('script end');正确输出顺序是:
script start script end promise1 promise2 setTimeout这里牵扯到宏任务、微任务的优先级。简单说,JS 引擎在处理完当前宏任务后,会先把微任务队列清空,再去取下一个宏任务。Promise.then注册的是微任务,setTimeout注册的是宏任务,即使setTimeout延迟设为 0,也要排在微任务后面。
但我在复盘时想到一个更值得聊的问题:微任务里如果又产生了微任务呢?比如:
Promise.resolve() .then(function() { console.log('promise1'); Promise.resolve() .then(function() { console.log('promise3'); }); }) .then(function() { console.log('promise2'); });这个输出的顺序是 promise1、promise3、promise2。原因是第一个.then回调执行完后,会返回一个新的 Promise,而在这个回调内部又注册了一个新的微任务,它会被放到当前微任务队列的末尾,但依然排在下一个宏任务之前。理解这个点之后,再看复杂的异步流程就不会乱了。
提示:这类题建议大家平时用
node --trace-events或者浏览器 Performance 面板观察任务队列,直观看到宏任务和微任务的调度过程,比自己空想要清楚得多。
3. 手写题:从 API 调用到原理实现
手写题在一套前端笔试题里通常占 30% 左右的分数,也是最容易拉开差距的部分。小红书这份卷子的手写题没有故意刁难人,没有让你从零实现一个 React,而是挑了几个工程中常用的方法,让你写出一个"能在生产环境用"的版本。
3.1 手写防抖函数:比你想象的更讲究细节
题目要求实现一个防抖函数,并说明它适用的场景。
基础写法很多人都会,闭包加定时器:
function debounce(fn, delay) { let timer = null; return function(...args) { const context = this; if (timer) clearTimeout(timer); timer = setTimeout(() => { fn.apply(context, args); }, delay); }; }但很多人写到这个程度就停了。我当时的做法是继续追问自己一个问题:如果用户希望第一次点击立即执行呢?这就引出了immediate参数。
function debounce(fn, delay, immediate = false) { let timer = null; let invoked = false; return function(...args) { const context = this; if (immediate && !invoked) { fn.apply(context, args); invoked = true; } if (timer) clearTimeout(timer); timer = setTimeout(() => { invoked = false; timer = null; }, delay); }; }这个版本的处理逻辑是:首次触发时立即调用一次,然后开始计时,在delay时间内再次触发不会执行,计时结束后重置状态,下一次触发会再次立即执行。这个场景在搜索框输入建议时非常常用——用户第一次输入时,先展示热门数据,后面的输入才走防抖逻辑。
我在批注里会特别强调一个坑:手写防抖时要记得处理this指向。如果直接用箭头函数写回调,里面的this会是定义时的上下文而不是调用时的上下文,很容易在 React 组件里出 bug。用function加apply的方式是最稳妥的。
3.2 手写深拷贝:一层一层剥开复杂数据类型
这道题有个常见的误区:很多人一上来就写JSON.parse(JSON.stringify(obj)),但这道题在批改时会明确扣分,因为:
undefined、函数、Symbol 会被吃掉Date会被转成字符串RegExp、Map、Set会变成空对象- 循环引用会直接报错
NaN会变成null
我当时的实现分了好几步,保证覆盖大多数情况:
function deepClone(target, map = new WeakMap()) { if (target === null || typeof target !== 'object') { return target; } if (target instanceof Date) return new Date(target); if (target instanceof RegExp) return new RegExp(target.source, target.flags); if (map.has(target)) { return map.get(target); } const cloneTarget = Array.isArray(target) ? [] : {}; map.set(target, cloneTarget); Reflect.ownKeys(target).forEach(key => { cloneTarget[key] = deepClone(target[key], map); }); return cloneTarget; }这里有两个容易被忽略的点。第一是WeakMap的引入,它专门用来解决循环引用问题,而且WeakMap的键是弱引用,不会造成内存泄漏,比普通Map更合适。第二是Reflect.ownKeys可以取到包括 Symbol 在内的所有键,比Object.keys更全面。
笔试的时候我不会写这么完整的版本,但会把关键点列出来:基本类型直接返回、引用类型递归、循环引用要用数据结构记录、特殊对象要单独处理。这样即使代码不够完整,也能让批卷人看到你的边界意识。
4. 框架与工程化:题目背后是业务现场的浓缩
小红书的前端业务有一个特点:C 端页面多,内容分发逻辑复杂,列表页和详情页的渲染性能会直接影响用户留存。所以框架和工程化的题目不是泛泛地问"虚拟 DOM 是什么",而是把许多真实场景中冒出来的问题直接搬到了卷子上。
4.1 关于虚拟 DOM 的"应用层"考察
题目描述大致是这样:一个长列表页面,每次数据更新都会造成明显卡顿,你如何分析和优化,并说明你在优化过程中对虚拟 DOM 和 Diff 算法的理解。
这道题没有一个标准答案,它是开放式的,核心在于你有没有真正处理过类似的渲染性能问题。我当时的分析思路分成几条线:
第一,先判断卡顿发生在哪个阶段。如果网络请求回来后数据量大,可能是 JS 主线程执行时间过长;如果是页面滚动卡顿,可能是频繁触发重排重绘;如果每次交互都明显延迟,可能是 Diff 过程过重,或者组件更新范围太大。
第二,针对更新范围过大的问题,React 的key是否正确设置是非常关键的前提。key的作用是让 Diff 算法可以复用已有节点,而不是把整个列表推倒重建。我之前在项目里遇到过一个问题,因为列表项用了数组下标作为key,结果删除中间某一项后,后面的每一项状态全部错乱。所以我在回答里专门写了一句:key不是写给开发者看的,是写给 Diff 算法看的,它的选择必须保证节点的唯一性和稳定性。
第三,如果 Diff 本身没问题,可以考虑从渲染机制上做优化。React 的memo能阻止不必要的子组件重渲染,useMemo和useCallback可以缓存计算结果和函数引用。但这些 API 不是无脑使用,过度 memo 反而会导致比较开销大于重新渲染的开销,这种时候要用性能分析工具先量化一下,再决定要不要加。
4.2 构建与部署:一场关于静态资源的"省钱"题
另一道工程化题目让我印象很深,它问:一个页面首次加载时,静态资源加载过慢,首页白屏时间长,你会从哪些方面优化?
这道题涵盖的面很广,如果只答"CDN、缓存、压缩"三个词,得分会很低。我当时的回答分成了四个层次:
一是资源体积层面。代码分割、按需加载、压缩混淆,这个大家都能想到,但还有一个细节容易被忽略——路由级别的懒加载。如果首屏只用到了首页的组件,就不应该一次性把整站 JS 都打包下来。
二是请求链路层面。CDN 的边缘节点选择、HTTP 缓存策略里的Cache-Control和ETag配合使用、雪崩和穿透场景下的处理方案,这些都属于链路优化。
三是渲染层面。SSR 或预渲染、骨架屏、关键 CSS 内联,这些手段能极大缩短用户感知到的白屏时间。
四是网络协议层面。HTTP/2 多路复用、资源预加载preload和预连接preconnect,这些是更进阶的优化点,提出来会让人觉得你不只是在背八股文,而是真的了解现代 Web 性能优化体系。
我记得当时还加了一个很实际的建议:在项目里接入性能监控平台,把真实用户的首屏时间、白屏时间、资源加载耗时都量化出来。没有数据支撑的优化都是盲目的,这个意识在面试里很加分。
5. 选做题与开放题:拉开差距的关键战场
这套试卷最后有一两道开放性问题,具体形式记不太清了,但类型我记得很牢:一类是设计类,给一个场景让你设计方案;另一类是观点类,让你谈谈对某个前端趋势的看法。这类题没有标准答案,但恰恰是最能体现功底的。
5.1 设计方案题:先定目标,再谈架构
我当时遇到的设计题核心是:如何设计一个支持多端展示的内容卡片组件,要求同时满足 Web 端和移动端的布局需求,并且能方便地扩展新卡片类型。
这道题如果直接从"我要用一个大的 JSON 配置驱动渲染"开始答,方向没错,但会被认为思考太浅。我是这样拆解的:
第一步,明确边界。卡片组件管理的核心是结构与样式解耦。结构上,一张卡片由封面区、标题区、摘要区、交互区四块组成;样式上,不同端的差异(密度、字号、间距)应通过设计令牌来控制,而不是在每个卡片里写死。基于这个思路,再进一步拆分技术方案:卡片类型通过注册机制维护一个映射表,新增类型时只需注册对应的渲染器,而不需要改动卡片容器代码。
第二步,数据层设计。考虑服务端下发的数据字段可能不统一,需要做一层适配器将不同源的数据标准化,避免组件内部写大量兼容逻辑。
第三步,做性能预判。长列表滚动场景下,卡片组件需要配合虚拟滚动使用,图片懒加载要按真实滚动位置触发,而不是简单地用 loading="lazy" 糊弄过去。
设计题最忌讳的是"一锅端"。把一个方案铺得很满,看起来面面俱到,实际没有重点。正确的做法是先定义系统边界,再突出核心难点,最后给出可以落地的细节。
5.2 观点题:技术选型背后的思考深度
观点题常见的是"你如何看待 Vite 和 Webpack 的优劣""你如何看待微前端""你如何看待 Server Components"这类问题。这种题有个高分秘诀:不要说"某个工具更好",而是要说"在什么条件下,某个工具为什么更合适"。
比如 Vite 和 Webpack 这种对比,我答题时的逻辑是:Vite 的开发体验好,靠的是原生 ESM 和按需编译,不用像 Webpack 那样先全量打包再启动 dev server,所以大型项目冷启动很快。但 Vite 在生产构建时底层用的还是 Rollup,在代码分割和长缓存策略上,生态成熟度和稳定性还比不上 Webpack 深耕多年的插件体系。所以结论不是"用 Vite 更好",而是"如果你的项目是全新的、团队对 Vue/React 的新工具链接受度高,Vite 是更好的选择;如果你在面对复杂的既有工程和大量老插件依赖,Webpack 的稳妥性更有价值"。
这种回答方式能透露出一个信息:你对技术有判断力,不会盲目追新,清楚技术在业务里的适用边界。对校招生来说,这比堆砌一堆新名词更打动人。
6. 复习路线:从这套卷子倒推备战清单
如果你准备参加下一次校招,我建议不要只盯着笔试题本身,而是从这套卷子倒推出一份复习路线。我把大部分过来人的共同经验整理在这里,可以直接对照查漏补缺。
第一板块是 JavaScript 核心。重点复习执行上下文、作用域链、闭包、原型链、Event Loop、Promise、async/await、模块机制。这一块没有捷径,需要把每个概念用代码验证过一遍,而不是停留在"我知道"的层面。比如你可以自己写代码测试一下 Promise 构造函数里的代码是同步执行还是异步执行,实测之后对很多调度问题的理解会彻底打通。
第二板块是浏览器与网络。渲染流程、回流重绘、缓存机制、HTTP 请求头、跨域方案、Web 安全(XSS、CSRF)都需要了解。我的建议是打开 DevTools 的 Performance 和 Network 面板,找几个真实页面录制分析一下,比背图有效果得多。
第三板块是框架与工程化。不要只停留在"会写组件"的阶段,至少要把 React 的 Diff 策略、状态更新机制、Hooks 的设计动机、代码分割、懒加载这些主题过一遍。工程化方面,熟悉 Webpack 核心概念(loader、plugin、tapable)、Vite 的构建链路、CI/CD 的基本概念就够应付笔试了。
第四板块是手写代码。常见的手写题包括防抖节流、深拷贝、Promise 系列、数组去重与扁平化、发布订阅、LazyMan 等。我建议准备一个自己的代码仓库,把这些实现分门别类整理好,并附上注释和测试用例。这个过程能帮你把手写题从"背答案"变成"真的理解"。
我还建议大家做一件事:把做错的题整理一个错题本,按主题归类。等考前一周,只看错题本就好,不再刷新题。这个习惯在校招期间帮我省了很多时间,推荐给你。
这套卷子最值得琢磨的,不是某一题的解法,而是它揭示了一条完整的能力链路:从语言底层机制,到浏览器运行环境,再到框架与工程化决策,每一环都需要扎实的积累。如果你能按照这个链路补齐自己的短板,那这份笔试题的价值就远远超过了一场考试本身。