我投的是2023年腾讯音乐春招前端开发岗,简历筛过之后收到了第二批笔试的通知。说实话,腾讯音乐的笔试在互联网大厂里不算最变态的,题目比腾讯集团统一批次的要收敛一些,但覆盖面依然很广——从CSS基础到JS手写题,再到工程化和一道业务相关的编程题,味道很正。这篇文章不是面经,更像是一次完整的笔试复盘,把题目背后的考点、我当时为什么这么答、哪些地方会丢分都拆开讲清楚。如果你是准备投前端开发岗的同学,不管是盯哪家公司,这份拆解应该都能帮你少走不少弯路。
1. 笔试整体拆解:题型分布与考察逻辑
1.1 笔试流程与时间分配:90分钟怎么花
收到笔试邮件之后,邮件里会给出一个链接,一般是在牛客网或赛码网上面做,进到房间之后会有摄像头监控、屏幕录制,甚至防切屏提示。腾讯音乐这场笔试总时长是120分钟,题型分两大块:一块是客观选择题,数量大概二十来道,涵盖HTML/CSS、JavaScript、浏览器与网络、框架,还有少量工程化的题;另一块是编程题,一共两道,一道偏算法/数据结构,一道偏业务场景模拟。
很多同学习惯先做选择题再做编程题,我复盘之后其实不建议这样。120分钟看着不少,但要留足编程题的时间。我自己的节奏是:先花大概15分钟把选择题快速过一遍,遇到拿不准的不要死磕,先标记下来,等编程题写完了再回头纠结。编程题两道,一般要给自己留够40到50分钟,因为总有边界情况要处理、测试案例要跑。
当时我实际的时间分配是:选择题25分钟,第一道算法题25分钟,第二道场景题40分钟,剩下30分钟回去补选择题、检查编程题的边界情况。最后差不多压哨提交,时间刚刚够用。如果选择题死磕某道题,后面编程题很容易翻车。
1.2 考点地图:从笔试内容反推的能力要求
这套笔试题做下来,我把它涉及的知识点按优先级和出现概率整理成了一个表,基本能代表一线大厂前端开发岗笔试的考察范围:
| 考点分类 | 高频子主题 | 考察深度 | 出现概率 |
|---|---|---|---|
| HTML/CSS | 盒模型、层叠上下文、Flex布局、BFC、选择器优先级 | 中,偶尔出刁钻细节 | 很高 |
| JavaScript核心 | 事件循环、闭包、原型链、Promise、this指向、ES新特性 | 高,经常出代码输出题 | 极高 |
| 浏览器与网络 | 渲染进程、缓存策略、HTTP状态码、跨域 | 中,选择形式为主 | 高 |
| 框架 | Vue响应式原理或React生命周期、虚拟DOM、Hooks | 中,取决于你简历里写什么 | 高 |
| 前端工程化 | Webpack/Vite构建流程、模块化、代码规范 | 低到中,选择形式为主 | 中等 |
| 手写/编程题 | 数组去重、深拷贝、节流防抖、并发控制、业务统计类 | 高,开放性较强 | 极高 |
腾讯音乐的笔试很典型的风格就是:选择题里有一部分是“代码输出题”,给你一段JS代码,让你选控制台输出结果。这种题特别能区分选手的功底。面试可以背八股,代码输出题背不了,它考的是对语言机制的真正理解。
所以我给准备笔试的同学一个建议:别只看八股文,要动手去写。哪怕每天就写一道手写题,一个月之后手感完全不一样。笔试里被代码输出题坑过的人,应该都懂我在说什么。
2. 选择题核心考点深度复盘
2.1 HTML/CSS底子题:大多数人都能答,但细节题拉开差距
笔试第一部分的题不会太难,重点看你对基础掌握的牢不牢。这次印象比较深的有几道:
一道是给了一段CSS问最终元素的高度,涉及的情况是height、box-sizing和内部子元素的边距塌陷混在一起。核心考的就是一个点:在box-sizing: content-box下(默认),设了height之后再加padding会超出本身高度,整体元素的实际高度是height + padding + border。有些同学的惯性思维是设了height就一定能固定住,这就是典型的没被坑过。
另一道题给了多个DOM嵌套,问点击最内层元素时事件触发的顺序,分别考察了addEventListener默认的冒泡阶段,以及一旦在父元素上设置了stopPropagation()之后输出会变成什么样。这种题只要记住三条规则就不会错:事件传播分捕获、目标、冒泡三个阶段;addEventListener不加第三个参数就是冒泡阶段触发;冒泡可以从里往外,但一旦stop,就彻底断了。
第三道是Flex布局,问的是容器设了flex-wrap: wrap之后,三个子项每个flex-basis50%,问最终排列是几行、对齐方式如何。这里要掌握的不仅是flex-wrap的换行规则,还有当子项宽度超过容器宽度时,剩余空间如何分配,以及flex-shrink默认值为1这个容易被忽略的细节。
这些题目看答案都觉得简单,但错就错在平时写页面根本不关心这些细节。做笔试之前建议把Flex和Grid的常用属性拉一遍,浏览器开发者工具的排版面板也多看一看。
2.2 JS核心机制:事件循环、闭包、this绑定是重头戏
选择题里分值最大的板块就是JavaScript。我这次遇到的好几道题都围绕同一个核心:事件循环。
有一道题考的是Promise、setTimeout、async/await混在一起时的执行顺序。大概长这样:
console.log('A'); setTimeout(() => { console.log('B'); }, 0); Promise.resolve().then(() => { console.log('C'); }); console.log('D');输出顺序是A D C B。这里只要你记得一条主线就能推理:先执行同步代码,再执行微任务队列(Promise.then),最后才是宏任务(setTimeout)。笔试考到这个级别不算难,但有的题目会在微任务里再套微任务、在宏任务里注册新宏任务,那就需要你对每一轮事件循环的执行队列判断得非常精确。
还有一类高频题就是this指向。我记得有一道题给了const obj = { fn: function() { return this; } },然后用四种方式调用,问this分别指向谁:直接调用obj.fn()、解构后调用const fn = obj.fn; fn()、用fn.call(obj2)、用在箭头函数里。这种题其实就一句话:this指向调用者,箭头函数除外。但出题人绝对不会只考这一层,他会把new绑定、默认绑定、显式绑定混在一起,让你选出哪些情况会指向undefined(严格模式)、哪些指向全局对象。
闭包和原型链也考了,不过题型相对温和。一道是问输出一个使用闭包保存计数器的题,一道是考Function.prototype、Object.prototype、Array.prototype的实例关系。原型链你只要能画出一条从实例到Object.prototype的完整链,就能应付大多数题。
2.3 浏览器、网络与工程化:八股文的主场
这部分就是典型的“背了就会,不背就蒙”的区域,考察的范围很广,但不会太深。
HTTP状态码我记得考了301、304和403代表的含义。这类题本身很简单,但容易掉进一个坑:301是永久重定向,302是临时重定向,两者虽然都返回Location头,行为区别很大,很多同学混淆。浏览器缓存那题则围绕着缓存优先级展开:Service Worker缓存级别最高,其次是HTTP Cache,最后是本地存储。
工程化的选择题一般一两道。这次遇到的一道是关于Webpack打包流程的,选项里混淆了loader和plugin的作用。其实只要记住一句话就能做对:loader是让Webpack认识各种非JS文件、并对文件做转换;plugin是介入打包生命周期、做更复杂的操作(比如HTML模板生成、资源提取、压缩)。两者功能完全不同,很多选项都会在这点上做文章。
还有一道跟前端开发规范相关的题,说存在一个团队代码规范配置文件,问它是干什么的。这题考的是ESLint配合husky在提交阶段跑lint-staged的机制。现在大部分用Vue或者React的公司都会这套东西——husky在Git提交的pre-commit钩子阶段拦截,然后执行lint-staged,只对暂存的代码逐一做语法检查和不规范写法拦截。这道题其实不算难,但如果你平时没有在团队项目里配过这套流程,很容易在husky和lint-staged的职责上混淆。能答上来,说明你对工程化是有真实的项目经验的。
老实说,这一块如果纯粹没有项目经验,会吃力一些。建议至少在公司项目里跑一遍主流脚手架,知道依赖装在哪、构建脚本是什么、代码提交前经过了哪几道检查。这不只是为了应付笔试,面试阶段也会问。
3. 手写题解析:笔试中最容易丢分的部分
3.1 经典手写题:Promise.all、深拷贝和防抖节流
腾讯音乐笔试里有编程题,但有一类“手写题”会通过选择题或者代码题的第二道的形式来考。不过我的经验是:如果你把经典手写题都练过,代码题的通用能力也就有了。
经典中的经典是防抖节流。这题核心是考闭包和时间控制。我当时的实现是这样:
function debounce(fn, delay) { let timer = null; return function (...args) { if (timer) clearTimeout(timer); timer = setTimeout(() => { fn.apply(this, args); }, delay); }; } function throttle(fn, interval) { let last = 0; return function (...args) { const now = Date.now(); if (now - last >= interval) { last = now; fn.apply(this, args); } }; }这里有个细节很多人会漏:防抖函数里如果直接把fn作为回调传给setTimeout,会导致this丢失。所以必须用fn.apply(this, args)把当前执行环境的this透传过去。这也是面试官比较喜欢追问的点。当时笔试虽然没直接考,但我在准备时把它默写了很多遍。
深拷贝也是高频考点,考察的是递归、类型判断和循环引用处理。最小够用的版本是这样:
function deepClone(obj, map = new WeakMap()) { if (obj === null || typeof obj !== 'object') return obj; if (map.has(obj)) return map.get(obj); const clone = Array.isArray(obj) ? [] : {}; map.set(obj, clone); for (const key of Object.keys(obj)) { clone[key] = deepClone(obj[key], map); } return clone; }用WeakMap处理循环引用是这题的加分项。如果你写的是普通对象,循环引用时会无限递归直接爆栈。考虑到笔试环境一般不让打断点调试,你在写这种递归函数时一定要提前想到循环引用的场景,把“防御性编码”变成默认习惯。
手写Promise相关的题也是大热门,不过通常不是让你完整实现一个Promise(那个工程量太大了,大部分笔试试卷根本装不下),而是让你实现Promise.all、Promise.race或一个带并发限制的异步调度器。Promise.all的核心逻辑不复杂,但有几道边界要处理好:入参要兼容非Promise值、返回顺序要和传入顺序一致、任何一个reject都要立即返回。
3.2 你要掌握的通用手写技巧:边界处理优于炫技
我在准备阶段把各种手写题刷了不下几十道,最后的体会是:笔试里的手写题评分标准通常不是“写得多精妙”,而是“在有限时间里能不能写出一份能正常工作、边界清晰的代码”。面试官看的是你的工程思维,不是算法竞赛思维。
举几个实战中很容易被忽略的边界:数组方法处理空数组时返回值是什么;字符串处理时如果传入了负数索引会怎样;Array.prototype.reduce不给初始值时,空数组会直接抛TypeError。这些“死记硬背”的知识点平时用不到,但笔试里只要有一道题涉及,就能刷掉一批人。
另一个技巧是:遇到复杂的函数签名,先写上类型判断和默认值,不管题目有没有明确要求。因为阅卷的人会看你对防御式编程的理解。比如写防抖,要考虑调用时传入e事件对象,e在异步回调里会不会已被释放;写并发控制,要考虑任务列表为空时直接返回空数组;写深拷贝,要考虑Date、RegExp这类特殊对象。
3.3 熟记展开运算符的常见坑
热搜词里有一条“前端开发 函数 ...arg”,其实展开运算符在笔试里也是常客。展开运算符的优点是语法很短,但坑也不少。
比如函数调用时fn(...arr)和fn(arr)的区别,以及引用类型在浅拷贝时只能拷贝第一层。有些手写题会让实现一个merge函数,把两个对象浅合并,很多人在只用展开运算符时注意不到嵌套对象是共享引用的:
const obj1 = { a: 1, b: { c: 2 } }; const obj2 = { b: { d: 3 } }; const merged = { ...obj1, ...obj2 }; // merged.b 指向 obj2.b,obj1.b 的内容直接丢了还有一个高频考法是函数式编程里的柯里化,要求把f(a, b, c)转成f(a)(b)(c),里面就要用...args收集参数并递归返回新函数。这种题说难不难,但如果手不熟,很容易在参数收集和递归终止条件上卡壳。
4. 编程题完整复盘:从题干到AC代码
4.1 第一道题:模拟播放次数累加的统计逻辑
编程题第一道通常是偏业务的数据统计题。我抽到的那道大意是:给定一组播放记录,每条记录包含歌曲ID和播放时长是否完整听完的标记,要求统计出每首歌的有效播放次数,并按有效播放次数降序输出ID和次数,有效播放分钟数不低于某个阈值的才能计入。
这道题考的核心其实就是哈希表的应用。思路很直接:遍历记录,用Map存每个歌曲ID的计数,满足条件的加一,最后转换成数组按次数降序排序。写完之后,需要注意的地方有两个:一个是输入的记录格式可能是ID,时长,是否完整播放的字符串,需要自己拆分;另一个是排序如果次数相同要按ID升序输出,否则会有测试用例过不了。题目本身不涉及太深的算法,但你的代码能不能一步到位写对边界,就决定了这题能否全过。
我当时用的JavaScript示例大致是:
function countValidPlays(records, threshold) { const map = new Map(); for (const rec of records) { if (rec.duration >= threshold && rec.isFullPlayed) { map.set(rec.songId, (map.get(rec.songId) || 0) + 1); } } return [...map.entries()] .sort((a, b) => b[1] - a[1] || a[0] - b[0]) .map(([id, count]) => `${id} ${count}`); }拿到这种带业务背景的题,先别急着一通操作。先想清楚输出结果需要什么字段、排序主次条件是什么,再动手写。笔试里时间紧,但思路清晰写的代码反而更快。
4.2 第二道题:偏数据结构,考的是拓扑排序思想
第二道编程题就明显上强度了。题目大意是一个“歌单收藏依赖”问题:每个歌单可能收藏了其他歌单的合集,要求根据一部歌单的依赖关系确定一个合理的“发布顺序”,也就是被依赖的歌单必须先发布。
这个场景一听就是典型的拓扑排序。依赖关系建模成有向图,用邻接表表示,再通过统计每个节点的入度,使用队列做BFS:入度为0的节点先处理,处理完更新邻居的入度,把新的入度变成0的节点也入队。如果最终结果数量不等于节点总数,说明存在循环依赖,直接返回报错。
这是我提交的参考代码:
function resolveOrder(num, deps) { const indeg = new Array(num).fill(0); const graph = Array.from({ length: num }, () => []); for (const [a, b] of deps) { graph[a].push(b); indeg[b]++; } const queue = []; for (let i = 0; i < num; i++) { if (indeg[i] === 0) queue.push(i); } const order = []; while (queue.length) { const node = queue.shift(); order.push(node); for (const next of graph[node]) { indeg[next]--; if (indeg[next] === 0) queue.push(next); } } if (order.length !== num) { throw new Error('存在循环依赖,无法确定发布顺序'); } return order; }这里我要专门说一个很实用的优化:不要用queue.shift()来出队,因为数组的shift()底层是O(n)的,遇到大数据量很容易超时。更稳妥的做法是用头尾指针来模拟队列:
let head = 0; while (head < queue.length) { const node = queue[head++]; // ... }这种写法很多从刷题网站起步的同学不太习惯,但笔试环境里面数据规模不会特别大,用shift()一般也能通过。但如果你经常在一线做性能相关的开发,这种模拟队列的写法更符合生产环境的习惯。我当时用的就是头尾指针的方式,省去了后续可能出现的性能隐患。
另外,这道题我还在代码里主动做了参数校验,比如num是负数、依赖数组为空节点数不为0的情况。这个习惯在笔试里是加分项,因为系统评测通常不只跑样例,还会跑隐藏的边界用例。
4.3 编程题环境的调试技巧:在牛客/赛码平台怎么减少死磕时间
除了算法本身,我还要强调一下笔试平台的差异。牛客网和赛码网对输入输出处理的要求不一样,有的平台用Node.js的readline,有的直接用process.stdin,最稳妥的通用模板是先按行读入所有输入,再统一解析:
const readline = require('readline'); const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); const lines = []; rl.on('line', line => lines.push(line)); rl.on('close', () => { // 在这里面写主逻辑,所有输入已经都在 lines 里了 });先处理输入再写核心逻辑,能大大减少你在线调试时来回改代码的时间。如果你要输出多行结果,记得拼接成字符串后再console.log一次,不要每行都输出一次。各种平台的输出方式虽然都支持多次输出,但在某些老的评测机上,频繁console.log会有微小的性能损耗。笔试考场不差这点性能,但“一次输出”的习惯对排查Bug更有帮助——你的日志不会被切分成多行,整个结果看起来更连续。
提示:写代码前我习惯先把样例输入手动抄到本地,手动跑一遍,再提交到线上。笔试界面和本地IDE的差异容易让人焦虑,这个动作能帮我快速冷静下来。
5. 笔试踩坑实录与系统复盘
5.1 环境与流程类的坑:设备准备不能临时抱佛脚
这次笔试有一个环节让我印象很深:考试前需要做环境检测,要保证摄像头能看到你本人、屏幕共享权限开了、浏览器版本符合要求。我的摄像头一开始显示黑屏,折腾了十几分钟才恢复正常,比预计的考试开始时间晚了大概5分钟。
所以笔试前一天,一定要做一次完整的模拟测试。考试平台一般会在邮件里提供一个测试链接,你提前把浏览器装好、摄像头驱动更新好、网络切到稳定的有线连接,甚至把防切屏的信息弹窗先关闭,别让操作系统通知在答题时跳出来干扰你。再提醒一句:手机也提前设为免打扰,不然来一通电话直接把页面切走,系统可能判你作弊。
5.2 答题策略与时间管理:选择题别恋战,编程题先求稳再求优
我在第1章已经说了总体时间分配,这里再展开讲几个具体的决策原则。
第一个原则:选择题不要超过两分钟。任何一道选择题超过两分钟还没确定答案,就标记一个你觉得最靠谱的选项,往后走。因为你在一道两分的选择题上纠结十分钟,后面一道编程题可能就因为没时间少考虑一个边界情况,直接丢了二十分,这买卖太亏了。
第二个原则:编程题先写暴力解,再优化。笔试不是竞赛,不要求你在30分钟内写出最优解,而要求你写的代码能通过所有测试用例。如果第二道编程题没有很快想到拓扑排序,完全可以先写一个带访问标记的DFS暴力版本,至少把能过的分都拿到。等代码通过了,再考虑用队列优化。很多同学卡在“想写出完美最优解”的心理,导致最后连暴力解都没提交。
第三个原则:测试用例一定要自己在本地加几个。笔试系统一般只提供一两个样例,而隐藏的边界情况可能占一半分。写完之后自己模拟几组输入:空输入、单节点、两个节点互相依赖、数据量最大时会不会爆栈。这些测试用例其实大部分都可以在编译/提交前就本地跑一遍。
5.3 考后复盘:我究竟在哪类题上丢了分
考完笔试到出结果之间有几天时间,我照着回忆把每道题分类整理了一遍,做了一个典型的错题记录表:
| 丢分题型 | 具体表现 | 根本原因 | 改进策略 |
|---|---|---|---|
| 事件循环代码输出 | 微任务和宏任务的嵌套顺序判断反了 | 没有系统梳理任务队列的每个阶段 | 每天做3道执行顺序题,直到100%正确 |
| CSS层叠上下文 | Flex子项在z-index上的表现判断错误 | 只有模糊认知,没有查过规范 | 手写一遍层叠上下文的创建条件 |
| HTTP缓存 | 对Cache-Control中no-cache和no-store区分不清 | 背了概念但没有实际抓包验证 | 用浏览器DevTools配合本地服务实际验证一次 |
| 拓扑排序实现 | 忘了处理循环依赖的报错分支 | 图遍历题刷得少 | 把LeetCode经典图论题全部过一遍 |
整理完之后再往前端开发的整个知识体系看一眼:丢分点几乎都集中在那些“听说过、背过、但没动手验证过”的内容上。这给我的教训是——信息输入不等于能力提升,实践验证才是把知识内化的关键一步。
6. 笔试之后:从这场考试反推备考方向
6.1 把考点地图变成自己的知识体系
笔试结束了,但如果你是想长期在前端开发方向发展,这张考点地图丢掉就太可惜了。我建议你把它当成一个知识清单,每周挑一个模块,用公司项目或者自己写demo的方式去验证:如果这周学了HTTP缓存,就去DevTools里看看CDN资源的Cache-Control长什么样;如果学了事件循环,就找一段复杂的async代码,亲手把打印顺序推演一遍再验证。
题库会一直变,但核心知识体系基本稳定。把JS核心机制、浏览器渲染流程、CSS基础、框架原理、网络协议这五个方向搞扎实,任何一家大厂的笔试你都能有底气。我曾见过有人靠刷题进了笔试,但一面二面聊项目时露馅,最后还是被刷。笔试只是敲门砖,不是能力证明。
6.2 前端AI开发对笔试的影响:新的消息
2023年这个时间点,“前端AI开发”已经逐渐从概念走向落地。如果你研究过岗位要求你会发现,部分公司开始把AI辅助代码生成当成默认的编码方式之一,笔试题目方向也出现了微妙的变化:有些场景题开始考察“如何设计一个支持实时音视频交互的播放器页面”“如何用WebSocket做增量数据推送”这类更贴近AI应用交互形态的题目。
我的判断是:未来的前端开发笔试会越来越侧重综合工程能力,而不只是单纯算法题。你要能理解接口设计、数据流管理、性能优化,甚至要懂一点基础的AI模型调用逻辑。腾讯音乐自身业务跟音视频、AI推荐强相关,笔试里出现这类题目其实很正常。所以备考时别只刷算法,还要关注前端如何与AI能力结合——至少你要知道怎么封装一个异步请求、处理流式输出、管理长连接状态。
6.3 如果笔试通过,下一步怎么衔接面试
从笔试到面试之间一般会有几天到一周的空窗期。不要干等结果,我当时是把笔试里没答对的知识点全部重新过了一遍,并准备了两个真实项目作为面试讲演的素材。提前预演了“介绍一下你的项目”“你在项目里遇到了什么难点”“怎么排查性能问题”这几个几乎必问的问题,每个准备了两套讲法。一套是给非技术面试官听的,讲业务价值;另一套是给技术面试官听的,讲设计取舍。实践证明,这两套讲法在后面的面试中帮了我大忙。
写在最后:这场笔试教给我的东西
一次笔试能考出什么?表面上是分数和进不进面试,但对我来说,它更像是一次对前端开发知识体系的全面体检。你会发现哪些知识点以为自己懂了、实际上一推就倒;也会发现哪些能力因为平时写得足够多,考场上完全是肌肉记忆。
我个人体会最深的一点是:笔试真正的难点不在题目本身,而在于你能否在有限的时间里稳定输出。代码题不像八股文,没有“背下来”这种捷径。它考察的是你过去的每一次实践积累、每一回调试排错、每一段代码重构沉淀下来的手感。这种手感只能靠自己一行一行写出来。
最后再分享一个实际的小技巧:笔试前一周,把所有经典手写题默写一遍,不看笔记。不要用IDE的自动补全,就当自己是在白板上写代码。这个过程会帮你暴露出大量“眼睛会了但手不会”的知识盲区,考前发现总比考场上发现好得多。
祝每一个正在准备前端开发岗笔试的同学,都能拿到心仪的offer。