腾讯音乐前端笔试复盘:考点拆解与手写题实战解析 我投的是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.bobj1.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。