先聊个实在的:腾讯音乐秋招前端岗的笔试,到底在考什么?
我前后带过不少新人,也帮人改过简历、做过模拟面试。2023年腾讯音乐秋招第二批前端开发岗的笔试题,整体风格和往年一脉相承——不搞偏题怪题,但非常考验基础功底和代码熟练度。如果你准备过一段时间八股文,看到题会觉得很亲切;但如果你只是背过概念、没怎么手写过代码,这次笔试会给你一个比较直接的提醒:前端这个岗位,光会“说”是不够的,必须能“写”。
这篇文章,我按自己的复盘思路把整张卷子拆开揉碎,从题型分布、核心考点、手写代码题的解法和踩坑点、再到笔试之后的面试衔接,一条线捋清楚。不管你是准备秋招、春招,还是单纯想检验一下自己的前端基本功,这篇内容都值得花二十分钟认真看一下。尤其是那些准备投递大厂中厂前端岗的朋友,腾讯音乐的这套题,基本可以当作前端笔试的“标尺”来对待。
1. 腾讯音乐笔试的整体风格与题型分布
先说结论:腾讯音乐前端岗的笔试,不是那种“故意刁难人”的卷子。它考察的核心就三件事——JavaScript语言功底、前端框架(尤其是Vue)的实际运用能力、以及你用代码解决真实问题的工程思维。
1.1 题型构成与时间分配
我记得很清楚,整张卷子大概分四个部分:单选题、多选题、填空题、编程题。单选和多选覆盖的知识面很广,从ES6语法、浏览器缓存机制、HTTP状态码到Vue响应式原理、组件通信方式都有涉及。填空部分偶尔会出一些让你写输出结果的题,比如this指向、闭包、事件循环输出顺序之类的。编程题通常是两道,一道偏算法/逻辑,一道偏业务/组件实现。
整场笔试的时间一般是一个半小时到两个小时。我见过不少同学在选择题上纠结太久,导致最后编程题没时间写,或者写得非常仓促。这里先给个建议:前面选择填空部分,会的题快速过,拿不准的题先凭第一印象选,然后标记出来,等编程题写完了再回来慢慢斟酌。千万不要在一道纠结题上耗掉十分钟,得不偿失。
1.2 和行业其他大厂笔试的横向对比
如果横向对比一下,腾讯音乐的笔试难度在互联网公司里属于中等偏上一点点。比字节、快手的部分岗位要温和,比很多中小厂的卷子要扎实。它的特点是:不考ACM竞赛那种高难度算法,更偏实际开发中能用到的逻辑题,比如数组处理、字符串解析、树结构转换这类。Vue相关的题目占比明显高于React,这点和腾讯音乐的技术栈有直接关系,比如QQ音乐、全民K歌等产品线大量使用Vue。后面聊到题型细节,大家就能感受到这个倾向。
注意:腾讯音乐和腾讯集团虽然有关系,但校招是独立进行的,笔试系统、投递渠道、招聘流程都单独走。准备时别混淆了,尤其不要拿腾讯集团笔试的题量和难度来直接对标腾讯音乐。
2. 选择与填空:高频考点拆解
选择题和填空题占的分值并不低,而且往往是拉开差距的地方。编程题大家都会写一些,但基础题的正确率才是真正决定你能不能进面试的关键。我把自己印象里比较常考的几类知识点整理了一下,这些都是我在辅导新人时反复强调的重点。
2.1 JavaScript语言基础:闭包、this、事件循环
JavaScript基础这一块,闭包几乎是必考的。常见考法是给你一段代码,问输出结果,或者问闭包可能带来的内存泄漏问题。
举个例子,经典的for循环中用var声明和用let声明的区别:
for (var i = 0; i < 5; i++) { setTimeout(() => { console.log(i); // 输出 5 5 5 5 5 }, 0); } for (let i = 0; i < 5; i++) { setTimeout(() => { console.log(i); // 输出 0 1 2 3 4 }, 0); }var没有块级作用域,循环结束后i已经变成5,所以定时器回调读到的都是5。let有块级作用域,每一轮循环的i都是独立的绑定,所以能正确输出0到4。如果笔试里出了这个题,不要只答对结果,最好能解释清楚背后的作用域机制,这对后面的问答题可能有帮助。
事件循环也是高频中的高频。尤其是宏任务和微任务的执行顺序,我建议你把下面这个经典案例吃透:
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。原理就是:同步代码先执行,微任务队列在处理宏任务之前会被清空,Promise的then回调属于微任务,setTimeout属于宏任务。这个点不仅笔试会考,面试时也经常拿出来聊。
2.2 浏览器原理与网络基础
浏览器缓存策略绝对是选择题里的常驻嘉宾。强缓存和协商缓存的区别、Cache-Control和Expires的区别、ETag和Last-Modified的区别,这些都要弄得清清楚楚。
简单梳理一下:
- 强缓存:浏览器直接从本地缓存读取资源,不发请求到服务器。相关字段是Cache-Control(比如max-age=3600)和Expires。
- 协商缓存:浏览器带着缓存标识向服务器发起请求,由服务器判断缓存是否可用。相关字段是ETag/If-None-Match和Last-Modified/If-Modified-Since。
这里有个容易被忽略的小细节:如果同时存在Cache-Control和Expires,以Cache-Control为准。因为HTTP/1.1的Cache-Control优先级高于HTTP/1.0的Expires。
HTTP状态码也是选择题重灾区。我印象里腾讯音乐考过301和302的区别、403和404的区别、500和502的区别。这里总结一下:
| 状态码 | 含义 | 典型场景 |
|---|---|---|
| 301 | 永久重定向 | 网站域名变更,旧地址永久跳转到新地址 |
| 302 | 临时重定向 | 登录后跳转、活动页面跳转 |
| 304 | 未修改,协商缓存命中 | 服务器告诉浏览器用本地缓存 |
| 403 | 服务器拒绝请求 | 没有权限访问资源 |
| 404 | 资源不存在 | 访问了不存在的URL |
| 500 | 服务器内部错误 | 后端代码抛异常 |
| 502 | 网关错误 | 反向代理服务器收到无效响应 |
2.3 Vue核心原理与常见API考察
Vue在腾讯音乐的笔试中占比很大,从选项题的分布来看,Vue2和Vue3都会涉及,但Vue3的比重在逐年增加。尤其是2023年的这批题,composition API相关的考点已经非常多了。
响应式原理是必考的。Vue2用Object.defineProperty,Vue3用Proxy。这里常考的坑是:Vue2无法检测对象属性的新增和删除,需要用Vue.set或Vue.delete;数组的index直接赋值也无法触发更新,要用splice。Vue3的Proxy则解决了这些问题,可以拦截更多操作。
组件通信方式也是高频考点。父子组件用props和$emit,父传子用props,子传父用$emit自定义事件;跨层级的可以用provide/inject;任意组件通信用事件总线(Vue2的EventBus)或者Vuex/Pinia。有一年笔试还出了道多选题,问“以下哪些可以用于Vue组件通信”,选项里混着$refs、$children、$parent这些,其实都能用,就是适用场景不同。
computed和watch的区别也值得好好整理一下。computed是基于依赖的响应式缓存,只有在依赖变化时才会重新计算;watch是监听某个值的变化,然后执行回调函数。笔试里可能给一个具体的业务场景,问应该用computed还是watch。比如“根据firstName和lastName拼接fullName”,用computed更合适;比如“监听路由变化然后发起请求”,用watch更合适。
2.4 高频手写题:不只是背答案
腾讯音乐笔试题里有一类“伪手写题”——就是填空形式给你一段不完整的代码,让你补全某一行。这种比直接让你从零写要简单,但前提是你真的理解代码的逻辑,而不是背答案。
比如我印象里考过这样一道题:给你一个防抖函数,中间挖掉一行让你补:
function debounce(fn, delay) { let timer = null; return function(...args) { // 请补全这里 const context = this; clearTimeout(timer); timer = setTimeout(() => { fn.apply(context, args); }, delay); }; }补全的内容就是clearTimeout(timer),但如果你不理解防抖的逻辑,你可能不知道怎么填。这种题其实比让你独立写一个防抖更考验理解深度,因为你要根据上下文猜出缺的是什么。
不过,我还是建议你把防抖、节流、深拷贝、数组去重、发布订阅、Promise.all这类经典手写题全部背熟、写熟。因为在笔试的编程题环节,这些工具的变体出现概率极高。下面我详细说说编程题。
3. 编程题实战:思路与完整实现
编程题是整个笔试的压轴,一般两道题分布在两个不同的难度段位。第一题通常比较温和,考察基本的编码能力;第二题会稍微复杂,涉及一些业务逻辑或数据处理。我把腾讯音乐常考的几类编程题整理了一下,每道题都给出完整思路和解题代码。
3.1 大数相加:笔试经典题,必须拿分
大数相加是一道非常有代表性的题。JS的Number类型有安全整数范围,超过Number.MAX_SAFE_INTEGER(9007199254740991)的整数运算就会丢失精度。实现大数相加的思路是把数字转成字符串,然后按位相加,处理进位。
function addBigNumbers(num1, num2) { let i = num1.length - 1; let j = num2.length - 1; let carry = 0; let result = ''; while (i >= 0 || j >= 0 || carry > 0) { const digit1 = i >= 0 ? parseInt(num1[i]) : 0; const digit2 = j >= 0 ? parseInt(num2[j]) : 0; const sum = digit1 + digit2 + carry; result = (sum % 10) + result; carry = Math.floor(sum / 10); i--; j--; } return result; } console.log(addBigNumbers('9007199254740991', '9007199254740991')); // 输出:18014398509481982重点有两个:一是从低位往高位算,所以要倒序遍历字符串;二是进位处理,sum大于等于10时要向下一位进1。这道题我至少在三家公司的笔试题里见过,属于必会的基础题。
3.2 树结构转换:从业务场景中抽象出来的难题
有一年腾讯音乐的编程题考了“将扁平数组转换成树形结构”的题目。这道题很经典,因为它直接对应前端实际开发中的场景——比如把后端返回的菜单列表、组织架构数据渲染成树形组件。
题目大概长这样:给定一个扁平数组,每个元素有id、parentId、name等字段,要求转换成树形结构。
const input = [ { id: 1, parentId: 0, name: '首页' }, { id: 2, parentId: 0, name: '关于我们' }, { id: 3, parentId: 1, name: '产品中心' }, { id: 4, parentId: 3, name: '产品A' }, { id: 5, parentId: 3, name: '产品B' }, ]; function buildTree(arr) { const map = new Map(); const roots = []; // 第一轮遍历:把所有节点存入Map arr.forEach(item => { map.set(item.id, { ...item, children: [] }); }); // 第二轮遍历:把节点挂到对应的父节点下 arr.forEach(item => { const node = map.get(item.id); if (item.parentId === 0) { roots.push(node); } else { const parent = map.get(item.parentId); if (parent) { parent.children.push(node); } } }); return roots; } console.log(JSON.stringify(buildTree(input), null, 2));这里推荐用Map来实现,因为Map查找是O(1)复杂度,整体时间效率为O(n)。如果你用数组的find方法去找父节点,那复杂度就变成O(n²)了,数据量一大性能就会有问题。这个思路差异在笔试里不一定能体现出来,但在面试时如果被追问复杂度分析,能答上来会很加分。
3.3 版本号排序:字符串处理与比较逻辑的经典题
这道题也很有代表性。给一组版本号,例如["1.0.2", "1.0.1", "2.0.0", "1.1.0", "2.1.0"],要求按版本号从小到大或从大到小排序。
版本号的比较规则是:从左到右逐段比较数字大小。直接的sort方法不行,因为版本号是字符串,默认按字典序排序,会出现"1.10.0"排在"1.2.0"前面的问题——字典序比较时"10"小于"2",但版本号规则里10大于2。
解题思路是:把版本号按"."分割成数组,然后把每一段转成数字再比较。
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; if (num1 < num2) return -1; } return 0; } const versions = ["1.0.2", "1.0.1", "2.0.0", "1.1.0", "2.1.0"]; const sortedVersions = versions.sort(compareVersions); console.log(sortedVersions); // 输出:["1.0.1", "1.0.2", "1.1.0", "2.0.0", "2.1.0"]这里有个关键细节:parts1[i] || 0。当两个版本号的段数不一致时,比如"1.1"和"1.1.0",缺省的那一段按照0处理。这个逻辑很重要,很多人写的时候容易忽略。
3.4 虚拟滚动实现:考察框架业务能力的高级题
有一年笔试的编程题出了一道让人印象深刻的业务题:实现一个虚拟滚动列表,渲染长列表时只渲染可视区域内的数据。虽然题面给的是React或Vue任选,但我印象里绝大多数人会选Vue,毕竟腾讯音乐本身是Vue技术栈。
虚拟滚动的核心思想是:只渲染可视区域的列表项,而不是渲染全部数据。具体做法是:外层容器固定高度并滚动,内层用一块高度等于全部数据总高度的“垫片”来撑起滚动条,然后根据scrollTop计算可视范围的起始索引和结束索引,只渲染这个范围内的项。
这里我给出一个Vue3的实现思路:
<template> <div class="virtual-scroll" @scroll="handleScroll"> <div class="virtual-scroll__phantom" :style="{ height: totalHeight + 'px' }"></div> <div class="virtual-scroll__content" :style="{ transform: `translateY(${startOffset}px)` }"> <div class="virtual-scroll__item" v-for="item in visibleList" :key="item.id" :style="{ height: itemHeight + 'px' }"> {{ item.text }} </div> </div> </div> </template> <script setup> import { ref, computed } from 'vue'; const props = defineProps({ list: { type: Array, required: true }, itemHeight: { type: Number, default: 50 }, containerHeight: { type: Number, default: 400 } }); const scrollTop = ref(0); const totalHeight = computed(() => props.list.length * props.itemHeight); const visibleCount = computed(() => Math.ceil(props.containerHeight / props.itemHeight)); const startIndex = computed(() => Math.floor(scrollTop.value / props.itemHeight)); const endIndex = computed(() => startIndex.value + visibleCount.value); const startOffset = computed(() => startIndex.value * props.itemHeight); const visibleList = computed(() => props.list.slice(startIndex.value, endIndex.value)); function handleScroll(e) { scrollTop.value = e.target.scrollTop; } </script> <style scoped> .virtual-scroll { position: relative; height: 400px; overflow-y: auto; background: #fff; } .virtual-scroll__phantom { position: absolute; top: 0; right: 0; left: 0; } .virtual-scroll__content { position: absolute; top: 0; right: 0; left: 0; } .virtual-scroll__item { display: flex; align-items: center; border-bottom: 1px solid #eee; } </style>这个实现的核心知识点有三个:总高度用假想高度撑开滚动条、内容区通过translateY位移实现“视口跟随”、可视数据通过slice裁剪。只要把这三个点讲清楚,面试官通常会认可你的理解深度。
我见过很多人能写出虚拟滚动的大致结构,但一追问“为什么用translateY而不是top?”就卡住了。这里顺便说一句:用transform不会触发浏览器的重排(reflow),性能更好;用top会触发布局计算,滚动时会卡顿。
4. 实战避坑:我在复盘腾讯音乐笔试时总结的教训
这些内容是我接触大量真实笔试复盘案例后,总结出来最有共性、也最容易被忽略的“坑”。如果你正在准备校招,下面每一条都值得认真对待。
4.1 选择题的“陷阱”设计思路
很多同学以为自己挂在了编程题上,其实真正的失分大户是选择题。腾讯音乐的选项设计很讲究,它不会把错误的选项写得很离谱,而是故意写得“看起来有道理”。举几个常见的陷阱类型:
- 把Object.defineProperty和Proxy的差异混淆,选项里故意把Vue3的Proxy写成“无法监听嵌套对象变化”,其实Proxy仍然需要递归代理才能实现深层响应式,但它监听的是对象整体,而不是属性。
- 把强缓存和协商缓存的触发时机搞混,选项里故意说“Cache-Control是服务器返回响应时设置的首部,优先级低于Expires”,这个说法就是错误的,因为实际是Cache-Control优先级更高。
- 把HTTP 301和302的场景互换,选项里说“301是临时重定向,302是永久重定向”,正好反了。
所以做选择题时,如果一个选项看起来很合理不太对劲的,先想想它是不是反着说的。腾讯音乐出题很喜欢用“颠倒法”来测试你对概念的真实掌握程度。
4.2 编程题最容易丢分的三件事
第一,审题不清。很多人都栽在这里。比如题目要求返回一个数组,你却返回了字符串;题目要求改变原数组,你却生成了新数组。建议拿到题先花30秒看清楚:输入是什么、输出是什么、有没有要求原地修改、有没有要考虑边界条件。
第二,边界处理不全。比如大数相加的输入可能包含空字符串,版本号排序的输入可能有空数组,树结构转换的数组可能没有根节点(parentId等于0的节点)。如果你没有做防御性校验,代码一跑就崩,用例直接挂零。
第三,忘记复杂度分析。笔试系统一般不会单独考复杂度,但如果你在面试中被问到自己写的算法复杂度是多少、能不能优化,回答不出来是很减分的。平时练题时要有意识地分析自己代码的时间复杂度和空间复杂度。
4.3 时间分配策略为什么重要
我复盘过不少同学的笔试记录,发现一个规律:能进面试的人,基本都会留出至少30到40分钟写编程题。如果你前面选择题花了太多时间,编程题只能草草收尾,那笔试成绩基本无望。
建议的分配策略是:
- 前10分钟:快速扫描全卷,标注出选择题里的难题和不确定题。
- 中间20到30分钟:先写编程题,因为编程题分值高、得分确定性也高。
- 剩余时间:回头处理选择题里的难题,确保在提交前没有空白题。
这个策略的核心逻辑是:选择题再怎么纠结,正确率也不会从30%提升到90%,但编程题只要写出来,拿到的分数是实打实的。把时间花在回报更高的题目上,是最理性的选择。
4.4 笔试后的复盘思路比分数更重要
很多同学笔试结束后就彻底放松了,等结果出来才慌。我的建议是:笔试结束的24小时内,立刻复盘。把编程题重新在自己电脑上实现一遍,把选择题里不确定的题目查清楚,整理成错题集。因为这时候记忆最清晰,复盘效果最好。
更重要的是,笔试里出现过的知识点,经常会在面试里被追问。比如你笔试遇到了虚拟滚动,面试时面试官可能会问:“虚拟滚动时如果列表项的高度不固定怎么办?”“如果滚动容器里还有其他的内容,怎么处理?”这些问题如果你在笔试后的复盘里想过,面试时就能从容应对。
5. 从笔试题反推大厂前端团队的人才标准
聊完具体的题目,我更想聊一个从这套笔试题里折射出来的东西——腾讯音乐这样的团队,到底在筛选什么样的人。
5.1 扎实的计算机基础仍然不可替代
从选择题的分布可以看出来,浏览器缓存、HTTP状态码、事件循环、作用域链这些计算机基础和网络基础知识依然占据很大的比重。有些同学总觉得这些“太底层”“过时了”,喜欢去追新的框架和工具。但笔试的题量分布说明,团队在筛选候选人时,首先看重的是基础是否扎实。
这背后是有道理的:基础扎实的人,学习新框架的成本低,排查问题的能力强。框架更新换代很快,今天学Vue2,明天Vue3,后天又有新工具,但JavaScript语言的核心机制、浏览器的渲染原理、网络协议的基本逻辑是稳定的。团队需要的是能长期成长的人,而不是只会某一种框架的“工具人”。
5.2 业务理解与代码实现同等重要
编程题里出现树结构转换、虚拟滚动这类题目,说明团队不是在招“刷题机器”,而是在招能解决真实业务问题的工程师。树结构转换对应权限菜单、组织架构等业务场景,虚拟滚动对应长列表性能优化,这些业务场景在QQ音乐、全民K歌、酷我音乐等产品中都是真实存在的。
所以如果你能在笔试中写出代码,并且通过注释或在面试时说明“这段代码用在什么业务场景”“解决了什么性能问题”,会比单纯写出正确答案更打动人。因为笔试系统只能看到你的代码,但面试时你能展示出自己的业务思考,这是加分项。
5.3 代码风格体现工程素养
还有一个小细节:笔试的时候尽量保持代码风格干净规范。变量命名用有意义的英文名,而不是a、b、c;函数内部逻辑用空行分隔;关键步骤加注释。虽然笔试系统不会因为命名不规范扣分,但你的代码会被面试官看到。在面试前,面试官通常会调阅你的笔试代码。整洁的代码风格和混乱的代码风格,给面试官留下的第一印象是完全不同的。
我记得有个真实的例子:有个同学笔试编程题完全做对了,但代码命名全是xxx1、xxx2、xxx3,中间夹杂着大量调试用的console.log。面试官看到之后第一句话就是“你这代码风格不太行啊”。就因为这种小事,最后offer就没发。这不是危言耸听,代码风格在面试里的重要性,比很多人想象的要高得多。
6. 给你的最后建议
我聊了这么多,最后再分享几个比较实际的建议。这些建议不是我凭空想出来的,是我在辅导那么多人准备校招笔试之后,发现的最有效的提升路径。
第一,把基础打牢永远是第一优先级。不要觉得ES6语法、事件循环、闭包这些太简单就不屑于复习,恰恰是这些“简单”的东西决定了你的下限。我的建议是每天花一个小时过一遍JavaScript基础,尤其是手写Promise、手写防抖节流、手写深拷贝这三件套,必须练到闭着眼都能写出来的程度。
第二,Vue一定要动手写项目,而不是只看视频。腾讯音乐笔试里的Vue相关题目,如果你的Vue是“看的”而不是“写的”,很容易在细节上翻车。自己搭一个简单的后台管理系统,把组件通信、路由守卫、状态管理、自定义指令这些全过一遍,比看十遍教程都管用。
第三,笔试前一定要上牛客网、力扣把这些经典题刷透。大数相加、数组转树、版本号排序、虚拟滚动,这些题目在牛客网和力扣上都有原题变体。刷题的时候不要只看思路,一定要自己在编辑器里敲一遍,跑通为止。只看不写的人,上了考场一定会发现手生。
第四,笔试结束后立刻复盘,把错题整理成文档。这个习惯会让你在面试环节受益无穷。我见过太多人笔试过了,但面试时被问到笔试的编程题,支支吾吾说不清楚当时的思路,显得像网上抄的答案。如果你能在笔试题基础上说出“如果让我重新写,我会在某个地方做优化”,面试官会立刻对你好感大增。
从笔试到面试,从面试到offer,看起来每一步都很难,但其实每一步都有迹可循。技术岗位的招聘,本质上是在筛选“基础扎实、能干活、有潜力”的人。只要你有方向、肯下功夫、愿意不厌其烦地打磨自己的代码能力,拿到心仪的offer只是时间问题。我想说的就这些,希望对正在准备秋招的你有所帮助。