搜“Shopee FE”的时候,其实会搜出来不少让人困惑的东西——有人聊的是电商前端岗位,有人问的是Doris数据库里FE(Frontend节点)和BE(Backend节点)到底怎么分工,还有人在讨论“shopee逆向”这种偏灰产方向的词。这里先把话说明白:我聊的是2024年秋招Shopee前端工程师(FE)提前批笔试,纯粹站在求职者角度,复盘这次笔试的考察思路、题型分布、做题策略,以及我考后总结出的备考盲区。
先说结论:提前批笔试不像很多人想的那样“随便试试、反正还有正式批”,恰恰相反,提前批往往是最能反映团队当前技术口味的场次。题目不多,但每一道都能看出出题人希望你具备什么能力。这篇内容主要给正在准备前端秋招的同学参考,尤其是目标锁定大厂、跨境电商或高并发业务场景的人。我会把笔试中遇到的典型题目、我的作答思路、踩过的坑、考后反思全部摊开来讲,尽量让后面的人少走弯路。
1. 笔试前的情报收集:从JD和面经里逆向出考点
1.1 先把“逆向”这件事做在正经事上
“shopee逆向”这个热搜词,我猜大多数人搜的是技术上的逆向工程,但我更想把“逆向”用在信息收集上——在参加笔试之前,逆向分析公司想招什么样的人,比盲目刷题重要得多。
我当时做的事很简单:把Shopee前端岗位的JD逐条拆开,再对照近两年牛客、脉脉、一亩三分地上已有的面经帖,把高频考点整理成一张清单。JD里出现最多的关键词是“JavaScript基础扎实”“理解浏览器渲染机制”“有性能优化经验”“熟悉React或其同类框架”“具备良好的工程化意识”。这些关键词直接决定了笔试题的出题方向:不会考你背面试题,而会考你是否真的理解前端运行时的底层逻辑。
这里有一个关键判断:Shopee的业务特点是跨境、多语言、多币种、网络环境复杂,前端在真实业务里要面对的是弱网、大量列表渲染、国际化文案管理、监控上报等场景。所以笔试题目一定不是单纯的LeetCode刷题,而是会把基础题、场景题和算法题混在一起,考察你在真实业务环境下的综合能力。
1.2 明确考察范围,圈定复习优先级
根据情报整理,我给自己划了四个复习优先级,这对于时间有限的秋招党特别重要:
| 优先级 | 考点 | 理由 |
|---|---|---|
| P0 | JavaScript核心机制(事件循环、作用域、闭包、原型链) | 笔试和面试最高频,一题就能筛掉很大一批人 |
| P0 | 浏览器HTTP缓存、渲染流程 | 跨境电商场景网络复杂,缓存策略直接决定页面性能 |
| P1 | CSS布局边界情况(Flex/Grid),响应式方案 | 前端基本功,但很多人只会用不会说原理 |
| P1 | 算法与数据结构(数组、字符串、链表、二叉树) | 一般两道编程题,难度中等,但边界条件很细 |
| P2 | React或Vue的源码级理解(Fiber、diff、响应式依赖收集) | 视岗位方向而定,提前批不一定深挖 |
| P2 | 工程化(Webpack/Vite、CI/CD、代码规范) | 简答题容易出,需要能讲出完整逻辑 |
这个清单帮我避开了两个坑:一是没有把所有时间花在刷困难算法题上,因为前端笔试的算法题通常卡在“中等偏下”难度,更看重边界条件的严谨性;二是没有忽略基础知识的深度理解,因为简答题和不定项选择最喜欢在这些地方埋坑。
2. 题型全景与做题顺序:先保住确定性的分数
2.1 实际遇到的题型构成
整套笔试题量不大,但时间也不宽裕,整体分为三块:单选题/不定项选择题、简答题、编程题。
选择题大概有10道左右,覆盖JavaScript语法细节、CSS布局、浏览器工作原理、网络协议。这里面有不少题是“看起来会,一做就错”的类型,尤其是不定项选择,多选、少选、错选都不得分,对概念的精确度要求很高。
简答题一般有2道,一道偏工程化(比如“如何设计一个前端监控系统”或“谈谈你对前端工程化的理解”),一道偏场景设计(比如“页面加载速度慢,如何定位和优化”)。
编程题一般是2到3道,可以在线IDE里写,支持JavaScript/TypeScript/Python/Java等主流语言,需要自己处理输入输出。难度梯度比较明显:第一道相对基础,第二道就开始上强度了。
2.2 我采用的答题顺序策略
我的做题顺序不是从第一题往后做,而是先花2分钟把整张卷子扫一遍,做三件事:
- 确认编程题有几道,大致属于什么类型;
- 标记出选择题里哪些是知识点盲区,先跳过;
- 估一下简答题需要多少文字量,不要写到一半发现时间不够。
然后按照“编程题优先、选择题次之、简答题最后”的顺序来做。原因很简单:编程题分值最高、区分度最大,而且如果最后写容易因为时间紧张导致思路变形。选择题哪怕靠排除法也能拿一部分分,简答题的主观性较强,写够要点就能拿基础分,放在最后相对安全。
实际做题过程中,我发现这套策略确实有效。因为第一道编程题虽然基础,但需要仔细处理输入解析和边界条件,至少花了15分钟;第二道编程题需要设计数据结构,又花了20分钟。如果先做选择题再做简答,很可能编程题会来不及写完,那样分数就真的悬了。
3. 基础题复盘:JS异步顺序、CSS边界布局和缓存判断
3.1 JavaScript事件循环与Promise执行顺序的连环坑
选择题里印象最深的一道题,是关于“async/await、Promise、setTimeout混在一起时输出顺序”的问题。题目大意如下:
async function async1() { console.log('async1 start'); await async2(); console.log('async1 end'); } async function async2() { console.log('async2'); } console.log('script start'); setTimeout(() => { console.log('setTimeout'); }, 0); async1(); new Promise((resolve) => { console.log('promise1'); resolve(); }).then(() => { console.log('promise2'); }); console.log('script end');这是一道非常经典的事件循环题,很多人在第一次做的时候都会错。我这里不直接给答案,而是分享我的分析思路:先区分同步代码和微任务/宏任务。整个script是一个宏任务,同步代码按顺序执行,遇到await时,右侧表达式会立即执行,函数剩余部分被包装成微任务;遇到Promise的then也是注册微任务;setTimeout注册宏任务。所以同步输出的顺序一定是script start、async1 start、async2、promise1、script end,然后执行微任务队列中的async1 end、promise2,最后执行宏任务setTimeout。
这类题目的关键在于:你必须要知道await后面的代码等同于Promise.then的微任务,而不是同步执行。很多人在第一次做的时候会认为async1 end在async2之后立刻输出,这就是最大的认知偏差。我在复习时专门把微任务和宏任务的注册时机、执行时机画在一张表里,反复推演了三遍才彻底清楚。
还有一道选择题考了“闭包和var/let的区别”,核心是for循环里用var声明的变量在setTimeout回调中的输出结果,以及改成let之后的结果差异。这种题考的不是会不会写闭包,而是知不知道var是函数级作用域、let是块级作用域,以及闭包捕获的是变量本身而不是值。
3.2 CSS布局题:Flex和Grid的边界情况
CSS部分让我意外的是,没有考简单的垂直水平居中,而是考了“Flex布局中,子元素在容器空间不足时的收缩行为”和“Grid网格中隐式轨道的尺寸如何确定”。
先说Flex收缩问题。题目给了一个容器,宽度固定,里面有三个子元素,分别设置了flex: 1 1 200px、flex: 2 1 200px、flex: 0 1 200px,问在容器总宽小于600px时,三个子元素各占多少宽度。
这道题表面考的是flex-grow、flex-shrink、flex-basis三个属性的综合作用,实际上考的是flex-shrink的计算规则。当容器宽度不足时,flex: 0 1 200px这个子元素不参与收缩,保持200px;其余两个子元素按照各自的flex-shrink比例分摊剩余空间。但要注意,缩小的基础是flex-basis,而不是子元素的内容宽度。如果对flex缩写三个值的作用不熟,这道题基本靠蒙。
Grid隐式轨道这道题也很有代表性:定义一个三列网格,但放入了四个子元素,问第四个元素放在哪里,以及隐式行的高度如何确定。很多人只记得grid-template-columns定义了显式轨道,却忽略了一旦内容超出显式轨道就会创建隐式轨道,隐式行高默认是auto。这个知识点在中后台表格类业务场景里非常常见,考得一点也不超纲。
3.3 浏览器缓存的优先级判断
网络和性能这块考了HTTP缓存,但问得很细:一个带Cache-Control: no-store的响应,同时响应头里带了ETag和Last-Modified,浏览器下次请求时会不会走协商缓存?
答案是不会。no-store表示完全不缓存,那么ETag和Last-Modified根本没有机会发挥作用。与之容易混淆的是no-cache,它表示可以存储但不能直接使用,每次需要回源校验。很多人把这两个单词搞混,认为no-cache就是不缓存,实际上恰恰相反。
还有一道概率较大的题是强缓存与协商缓存的组合判断,给了一个带Cache-Control: max-age=600和ETag的响应,问10分钟内再次刷新页面,网络请求是否会被发起。答案是不会,强缓存命中时,浏览器直接从本地读取,不发请求。但如果用户手动刷新(F5),行为又会不一样,浏览器可能会带上If-None-Match头进行协商缓存。这些细节如果只看概念不实际抓包验证,很难真正理解。
3.4 简答题的答题框架
简答题我遇到的一道是关于“首屏性能优化”,这不是一道有标准答案的题,而是看你能不能把问题拆解完整。我的作答思路是:先定义问题边界——“首屏”到底是页面完全渲染,还是用户可交互,还是主要视觉区域出现;接着从指标定义出发(FCP、LCP、TTI),再按“服务端优化、网络优化、资源加载优化、渲染优化”四个维度展开。
这里有一个很重要的经验:简答题不要只写要点,一定要有“为什么”和“怎么做”。比如“资源加载优化”不能只写“开启CDN、开启Gzip”,要补充为什么CDN对跨境电商场景尤其重要——因为用户分布在多个国家,CDN边缘节点可以大幅降低跨国链路的RTT;Gzip对文本类资源压缩比很高,能减少传输体积。把这些讲清楚,简答题分数就不会低。
4. 编程题全记录:从滑动窗口到缓存设计
4.1 第一道算法题:滑动窗口求无重复字符的最长子串
这道题应该是LeetCode原题“无重复字符的最长子串”,难度中等,很经典,但笔试环境里写和本地IDE写完全是两回事。一是不能自动补全,二是要自己处理输入输出,三是有时间压力。
我的解题思路是滑动窗口加哈希集合。维护一个左指针和一个右指针,右指针不断向右扩展,把字符加入集合;当遇到重复字符时,移动左指针,不断从集合中删除左指针指向的字符,直到重复字符被移除。每一步都更新一次最长长度。
function lengthOfLongestSubstring(s) { const set = new Set(); let left = 0; let max = 0; for (let right = 0; right < s.length; right++) { while (set.has(s[right])) { set.delete(s[left]); left++; } set.add(s[right]); max = Math.max(max, right - left + 1); } return max; }这道题的关键不是记代码,而是理解为什么left要逐字移动而不是直接跳到重复位置的下一位——用Set时没法精确知道重复字符的位置,所以只能逐字缩。如果追求更低的时间复杂度,可以用Map记录每个字符最近一次出现的位置,left可以直接跳跃。笔试时能用最稳妥的Set解法写出来并正确通过测试用例,比追求最优解更重要。
我在本地测试时花了几分钟处理边界情况:空字符串返回0、单字符串返回1、全是重复字符返回1。这些看起来简单,但在线编译环境里一旦少了一个边界判断,就会导致部分测试用例不过,丢分非常可惜。
4.2 第二道算法题:实现一个带过期时间的缓存
这道题不是纯算法题,而是偏向设计题,很符合业务场景。题目要求实现一个类,支持get和put两个方法,每个key可以设置过期时间,过期后get返回-1,缓存容量有限,超出容量时按照某种策略淘汰(题目里指定了LRU)。
看到这道题时我意识到,它考察的真不是LRU本身,而是“如何在O(1)时间内访问和淘汰数据”。标准的做法是哈希表加双向链表。哈希表负责O(1)查找,双向链表负责维护访问顺序,每次访问一个节点就把它移动到链表头部,淘汰时从链表尾部移除。
class LRUCache { constructor(capacity) { this.capacity = capacity; this.cache = new Map(); this.expire = new Map(); } get(key) { const now = Date.now(); if (this.cache.has(key)) { if (this.expire.get(key) < now) { this.cache.delete(key); this.expire.delete(key); return -1; } const value = this.cache.get(key); this.cache.delete(key); this.cache.set(key, value); return value; } return -1; } put(key, value, ttl = 0) { const now = Date.now(); if (this.cache.has(key)) { this.cache.delete(key); this.expire.delete(key); } if (ttl > 0) { this.expire.set(key, now + ttl); } else { this.expire.set(key, Infinity); } this.cache.set(key, value); if (this.cache.size > this.capacity) { const oldestKey = this.cache.keys().next().value; this.cache.delete(oldestKey); this.expire.delete(oldestKey); } } }这里有一个必须在笔试中注意的点:题目里的过期时间和LRU淘汰机制是有冲突的。比如一个key还没过期但很久没被访问,它是应该被LRU淘汰的;而一个key即将过期但刚被访问过,它又应该被保留到过期时间再失效。处理这种冲突时,我是按照“先判断过期,过期则删除;未过期则更新LRU顺序”的顺序来的,这样语义最清晰。
而且,JavaScript的Map在插入新元素时会把键放在迭代顺序的末尾,删除再重新插入同一个键,也能把它移到末尾。这就是为什么用Map也能模拟LRU的原因。面试官如果追问底层实现,我就再讲双向链表,但笔试阶段Map方案已经足够。
4.3 笔试环境中的输入输出与边界处理
在线编程和本地写的最大区别,是输入解析。很多前端同学平时写算法题直接写函数体,到了笔试平台发现还要自己拼解析逻辑,一下子慌了。
我的习惯是先把输入读完,用fs.readFileSync('/dev/stdin', 'utf8')或readline模块处理多行输入,然后把每行数据转成目标类型,先跑一个最小测试样例确认解析无误,再写核心逻辑。千万不要一上来就写function xxx()然后发现根本没把输入接进去。
另一个经验是:如果题目没有明确说输入一定是合法的,就要在代码里加上防御判断。比如判断数组越界、判断空字符串、判断数字范围。这些代码看似多余,但在笔试自动化判题里,多一行判断可能就多过几个测试用例。
5. 考后的诚实复盘:失分点、盲区和下一次改进
5.1 选择题里我真正失分的点
考后我第一时间把选择题里拿不准的题目记下来,回到电脑前逐题查资料验证。发现失分主要集中在两个方向:一是ES Module和CommonJS的差异细节,二是React合成事件与原生事件的执行顺序。
ES Module和CommonJS的差异,我知道一个是静态导入、一个是动态加载,知道同步和异步的区别,但笔试里考到了“在CommonJS模块内修改module.exports对象的内容对调用方的影响”以及“ES Module的实时绑定意味着什么”,这两个细节我都答得不够精确。实时绑定说直白点就是导入方拿到的不是值的副本,而是一个指向导出模块内部绑定的引用,导出方后续修改的值,导入方也能读到。这个特性和CommonJS的复制导出值有本质区别。
React合成事件和原生事件同样容易踩坑。React 17之后事件不再挂载到document上,而是挂载到根容器上,并且合成事件的执行顺序是在原生事件冒泡到根容器之后。笔试里问了一个场景:在某个DOM节点上同时绑定了React合成事件的onClick和原生addEventListener的click,点击该节点时哪个先触发。很多人想当然认为原生事件先执行,但补了React 17的变化之后,这个结论就要分情况讨论。这类细节不实际写demo验证,真的容易记错。
5.2 简答题的篇幅控制和时间分配失误
我在简答题上犯了一个几乎所有人都容易犯的错:写得太长了。第一道简答题我为了体现专业性,写了完整的性能优化方案,包括CDN配置策略、图片压缩方案、懒加载实现、骨架屏设计,甚至还画了文字版的流程图。结果第二道简答题只写了大纲级别的回答,显得很单薄。
考后反思,简答题的评分大概率是踩点得分,而不是按字数给分。写得再长,如果后面的题没写或者写得很浅,总分反而会受影响。正确的做法是:每道简答题控制在300到500字,用小标题或者序号把要点列清楚,每个要点一到两句话解释,不要展开成长篇大论。如果时间充裕,再在最后补一个“如果有需要,我可以进一步展开”的说明,给面试官留出追问空间。
5.3 如果让我重来一次,我会调整什么
第一,我会在复习阶段增加对“设计类编程题”的专项训练。这次的LRU缓存题就是一个信号,前端笔试已经不只是考纯算法了,而是开始结合业务设计中高频使用的数据结构。像“实现一个带防抖和节流功能的函数”“实现一个可取消的Promise”“实现一个支持并发限制的请求池”,这类题目以后会越来越多。
第二,我会花更多时间在HTTP缓存的实际验证上,而不是只看文档。用Node起一个本地静态服务,在响应头里分别设置Cache-Control: max-age、no-cache、no-store,再到浏览器Network面板里观察请求和响应,亲手验证强缓存命中、协商缓存304、完全不缓存这三种状态的差异。记忆的牢固程度和只看文章完全不一样。
第三,我会把“表达训练”纳入复习计划。笔试里简答题的最终目的是让阅卷人看到你有清晰的思路,这不是临时发挥能写好的,需要平时就练习用简洁的语言概括技术方案。我在这次笔试里就发现,有些技术点我心里明白,但用文字表达出来就变得啰嗦且没重点。
最后分享一点个人体会
整个2024年Shopee FE提前批笔试给我的感觉是:不偏不怪,但也不轻松。所谓的“提前批”,题目反馈出来的信息是团队希望找到基础扎实、能处理真实业务复杂度的人,而不是刷题机器。三轮题做下来,最核心的能力其实是“在有限时间内把你已经掌握的东西稳定地表达出来”。如果你正在准备下一年的笔试,我的建议是:与其猜题,不如把自己当成一个要解决真实前端问题的工程师,多用“为什么这样设计”的视角去审视每一个知识点。这次笔试里让我卡住的,从来都不是那些冷门偏题,而是我自认为早就掌握、但从来没往深想过的日常概念。把这些日常概念真正吃透,比多刷一百道题更有用。