ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

腾讯音乐秋招前端笔试复盘:JS基础、Vue原理与手写代码全解析

腾讯音乐秋招前端笔试复盘:JS基础、Vue原理与手写代码全解析 1. 笔试整体画像与复盘思路1.1 这场笔试考了什么为什么值得复盘2023年腾讯音乐秋招前端岗的笔试我是在一个周六下午完成的全程90分钟题目数量和难度都控制得比较克制但覆盖面很广。整张卷子大致可以分成四个部分JS语言基础与进阶、Vue框架与工程化实践、手写代码题、以及两道算法题。没有网络题没有CSS布局题这个分配本身就能看出TME前端团队对候选人能力模型的取向语言功底要扎实框架原理要理解代码能力要过关算法底子不能太弱。先说结论这份笔试卷子的难度属于大厂校招的中等偏上水平比常规的“背八股”卷子要难一些但没有到拼竞赛题的程度。它更看重的是你能否在限定时间内把一个功能点在语言层面和框架层面都实现得干净利落。换句话说它考的不是“你知不知道这个API”而是“你用这个API能不能写出不埋坑的代码”。我复盘这份试卷的时候最大的感受是题目本身并没有特别偏门的知识点很多都是平时开发中天天见的东西但它的问法很“绕”比如给你一段看似正常的代码问输出的顺序或者给你一个常见的业务场景让你用Vue实现并解释原理。这其实比单纯考概念要难得多因为它考察的是你在真实项目里有没有踩过坑、有没有想过“为什么”。1.2 时间分配策略与整体作答节奏90分钟做完整张卷子时间其实并不宽裕。我的策略是先把所有题浏览一遍按分值和时间成本排了个优先级手写代码题分值最高也最容易通过准备拿分优先做算法题分值次之但思路清晰的话可以快速写出来放在第二位语言基础题和应用题虽然分值不高但数量多需要确保正确率放在第三位最后剩时间再回头补细节。实测下来这个节奏是对的。手写题我大概花了20分钟算法题花了15分钟剩下的时间全部用于选择题和应用题。有一个比较幸运的地方是这次笔试的选择题不算多大概15道左右而且没有多选题这让我在时间上从容了很多。如果你准备参加类似的笔试我建议你提前了解清楚题型分布如果是代码题占比高那就把刷题重心放在手写实现上而不是死磕选择题。另外说一句笔试环境用的是牛客网支持自选语言JavaScript和Python都可以我选了JavaScript。如果你也准备走前端方向我强烈建议用JavaScript作答因为后续面试的考察重心和笔试是连贯的提前在笔试中熟悉用JS写算法题的节奏对你只有好处。2. 语言基础与应用题藏在细节里的考点2.1 函数展开运算相关的考核点...args的前世今生这一部分让我印象最深的是一道和剩余参数、展开运算相关的题。题目大概是这样的给你一段代码要求写出fn调用后的返回结果或输出顺序。这个考点在热词里也反复出现说明它确实是大厂高频考点。很多人看到...args会下意识觉得这只是“把剩下的参数收进数组”但它背后的机制远不止于此。展开语法和剩余参数其实是同一个运算符的两种使用场景。在函数参数位置使用...args它表示剩余参数在调用或数组字面量位置使用...arr它表示展开运算。关键点在于剩余参数会把“没有被命名的参数”收集成一个真实的数组而arguments对象只是一个类数组它没有map、filter这些数组方法。这是笔试里经常出题的一个坑。我当时遇到的一道题是这样的function foo(a, ...rest) { return rest; } foo(1, 2, 3, 4); // 输出什么答案是[2, 3, 4]。这道题本身很简单但它后面还问了几个变体如果foo写成箭头函数arguments还能不能用如果...rest放在参数列表中间会怎样这些变体才是真正拉开差距的地方。箭头函数不绑定arguments它继承的是外层作用域的arguments。如果你在箭头函数里使用arguments实际访问的是外层普通函数的arguments。这在笔试中非常容易出错因为很多人的直觉是“我在函数里用arguments一定没问题”但它和this一样是箭头函数的“不绑定项”。还有一个容易踩坑的点是剩余参数必须放在参数列表的末尾。你写function foo(...rest, a)会直接报语法错误。因为剩余参数的特性就是“收集剩下的所有参数”如果它后面还有参数JavaScript 引擎无法判断从哪里开始截断。这个知识点在ES6刚出来的时候就是面试热门现在依然是笔试的基础考点但出题人会把它包装成各种花式问法。2.2 闭包与作用域链异步输出顺序的经典陷阱另一道让我印象深刻的基础题是关于闭包和异步执行顺序的。题目给了一段经典的for循环加setTimeout的代码问输出结果。这种题从2015年考到现在依然是笔试常客因为它涉及三个核心知识点块级作用域、闭包捕获、事件循环。for (var i 0; i 3; i) { setTimeout(() console.log(i), 0); }这段代码的输出是3, 3, 3不是0, 1, 2。原因是var声明的变量是函数作用域循环结束后i已经变成了3而三个定时器回调在宏任务阶段执行时访问的是同一个i。如果你把var换成let输出就变成了0, 1, 2因为let为每次循环创建了独立的块级绑定。这道题的变体是在循环里用立即执行函数创造闭包或者用forEach代替for。我建议你在准备这类题时不要只记住“用let就行”这个结论而是真想清楚闭包捕获的是“变量”而不是“变量的值”。箭头函数和普通函数在这道题里没有区别区别只在于变量声明的关键词。还有一个延伸考点是事件循环的任务优先级Promise的回调属于微任务setTimeout属于宏任务。如果代码里同时存在Promise.resolve().then(...)和setTimeout(...)微任务会先执行。笔试里经常会把这两者混在一段代码里让你写输出顺序。这类题的解题思路是先扫一遍代码把微任务和宏任务分别列出来再按执行顺序排。不要靠直觉一定要在纸上画出队列。2.3 this 指向与箭头函数的应用场景接下来是this指向问题。笔试题目给了一段对象方法调用的代码问你输出什么。这道题本身不难但它把this和call、apply、bind三个方法放在了一起问哪个方式可以改变this指向。做题的时候我先排除了call和apply立即执行的场景剩下的就是bind了。this指向这一块我觉得与其背规则不如理解规则背后的逻辑。在普通函数中this的值取决于“调用方式”直接调用时指向undefined或全局对象作为对象方法调用时指向该对象通过call或apply调用时指向指定的对象。在箭头函数中this不是调用时确定的而是在定义时捕获外层作用域的this。这就是为什么在Vue项目里methods中的函数用箭头函数会有问题因为这样this就不会指向组件实例了。这个知识点在日常开发中很容易踩坑。我在项目里曾经写过类似这样的代码methods: { handleClick: () { this.visible true; // this 不是组件实例 } }原因就是箭头函数捕获的this是外层作用域的this而在Vue的methods定义中这个外层作用域是模块作用域并不是组件实例。这个问题在笔试中很容易被包装成“为什么会报错”的形式你要能准确解释出“箭头函数没有自己的this”这一层。3. Vue 框架与工程化从使用到原理的进阶考察3.1 Vue 响应式原理Object.defineProperty 与 Proxy 的取舍TME的笔试题对Vue的考察深度比我想象中要深。它不满足于让你写出“安装依赖、创建组件”这种层面而是直接问响应式原理的底层实现。有一道题问的是Vue 2.x的响应式系统基于什么实现Vue 3.x又换成了什么两者在监听新增属性和数组操作时的表现有什么区别。Vue 2.x 用的是Object.defineProperty它只能监听对象属性的get和set无法检测新增属性和delete。所以Vue 2专门提供了Vue.set和Vue.delete来弥补这个缺陷。数组方面Vue 2重写了七种变更方法push、pop、shift、unshift、splice、sort、reverse所以通过索引直接修改数组元素是无法触发视图更新的。Vue 3.x 换成了Proxy它可以代理整个对象支持新增属性和删除属性的拦截数组的索引操作和长度变化也能被代理。这是一次根本性的提升。我建议你在准备这类题时要能说出为什么Proxy可以做到而Object.defineProperty做不到因为Proxy代理的是对象本身而不是对象的某个属性它天然就能感知对象结构的变化。这道题还延伸问了一个关于组件通信的问题在Vue中父组件和子组件之间通过props传递数据时如果子组件修改了props会发生什么。这其实是在考察通信规范和数据流设计。Vue的props是单向数据流子组件不应该直接修改props而应该通过触发事件让父组件来修改。这个设计理念和响应式原理是有关联的如果子组件能随意修改props那数据流就会变得混乱难以追踪。3.2 Vue 事件总线的坑与组件通信的几种方式笔试里有一道题是考察Vue 2中事件总线Event Bus的使用。题目给出了一段代码要求找出其中的错误或不当之处。典型的问题有两个一是事件总线的$on监听函数在组件销毁时没有通过$off解除导致内存泄漏二是组件之间通过名字耦合太重后期维护困难。事件总线本身是Vue 2中一个很轻量级的通信方案适合兄弟组件之间传消息但它有两个天然缺陷一是不利于多人协作因为大家都是通过事件名通信事件名的管理容易失控二是不利于调试事件发生时没有清晰的调用栈出错后不好排查。笔试中问到事件总线往往是想引出“你们项目里有没有更好的替代方案”这个延伸问题。现在Vue 3 在选项API中还保留了事件总线官方为了社区平滑过渡通过mitt这样的三库也能实现同样的功能但在组合式API中我更倾向于直接用provide/inject或者一个轻量的状态管理库来解决跨层通信需求。组件通信的选择其实是开发规范的一部分。热词里提到的“前端开发规范 vue”指的就是这个层面在组件设计时要明确数据的流向什么时候用props下传什么时候用emit上抛什么时候用slot分发什么时候用provide/inject进行跨层级通信。如果没有一个统一的约定项目规模一大各种通信方式混用维护成本会成倍上升。我当时在这道题上的经验是回答组件通信时不要只罗列方式要从“场景匹配”的角度来说。父子之间用props和emit跨多级用provide/inject或者事件总线全局共享状态用Vuex或Pinia无关组件之间用事件总线或状态管理。笔试和面试官想听的不是“你知道多少种方式”而是“你在实际项目中能根据场景选择最合适的方式”。3.3 生命周期与异步请求的时机选择又一道让我印象深刻的题是关于生命周期的在Vue中为什么“数据请求”要放在created或mounted中放在beforeCreate中可以吗答案是beforeCreate阶段连data和methods都还没有初始化你无法访问this.data和this.methods所以不能发数据请求。这道题的考点在于你对生命周期钩子顺序的掌握beforeCreate→created→beforeMount→mounted→beforeUpdate→updated→beforeDestroy→destroyed。在created时可以访问data和methods但DOM还没挂载所以不能做DOM操作在mounted时DOM已经挂载完毕可以操作DOM了。实际开发中我一般会把数据请求放在created把需要操作DOM的逻辑放在mounted。这不是死规矩而是基于对生命周期的理解做出的合理选择。笔试中的选择题可能会以“哪个生命周期适合做XXX”的形式出现你需要理解每个阶段的“能力边界”。还有一个相关的延伸考点是nextTick。Vue 更新DOM是异步的当你修改了数据后不会立即更新DOM而是等到事件循环的微任务阶段统一更新。如果你想在数据变化后立即访问更新后的DOM就需要用this.$nextTick。笔试中有可能会问为什么Vue.js使用异步更新队列背后的原因是性能优化——将多次数据变化合并成一次DOM更新减少不必要的渲染计算。这个知识点面试也经常问建议你深入理解一下。4. 手写代码题框架之外的真功夫4.1 手写深拷贝从浅拷贝到完整的深拷贝手写题第一道就是深拷贝。这道题看起来年年考但出题人的要求逐年提高。这次的要求是“实现一个深拷贝要求支持对象、数组、Date、RegExp并能够处理循环引用。”如果只写一个简单的递归拷贝很容易在遇到循环引用时栈溢出。我当时的实现思路是这样的function deepClone(target, map new WeakMap()) { if (target null || typeof target ! object) { return target; } if (map.has(target)) { return map.get(target); } const clone Array.isArray(target) ? [] : {}; map.set(target, clone); if (target instanceof Date) { return new Date(target.getTime()); } if (target instanceof RegExp) { return new RegExp(target.source, target.flags); } for (const key in target) { if (Object.prototype.hasOwnProperty.call(target, key)) { clone[key] deepClone(target[key], map); } } return clone; }这个实现的核心是WeakMap的使用。WeakMap的键是弱引用不会被垃圾回收机制阻止这在深拷贝这种场景下是很合适的选择。每次拷贝一个对象前先检查这个对象是否已经在map中如果是就直接返回避免循环引用导致无限递归。同时这个WeakMap只用于拷贝过程中的临时记录不会对原始对象产生额外影响。还有几个细节要注意typeof null object所以要先判断nullDate和RegExp需要单独处理因为直接遍历会把它们变成普通对象for...in会遍历原型链上的可枚举属性所以要用hasOwnProperty过滤。这些都是笔试中容易被忽视的细节。另外如果你用的是Map而不是WeakMap虽然功能上差不多但在内存管理上不如WeakMap优雅面试时可以主动说明这个选型理由。4.2 手写防抖与节流从实现到适用场景第二道手写题是防抖和节流。这道题我自己平时项目里写过多遍所以答得比较顺。防抖和节流的核心区别是防抖是“触发完后等一段时间再执行”节流是“固定时间内只执行一次”。在笔试中建议你把两个函数都写出来并且说清楚各自的适用场景。function debounce(fn, delay 300) { let timer null; return function(...args) { clearTimeout(timer); timer setTimeout(() { fn.apply(this, args); }, delay); }; } function throttle(fn, interval 300) { let last 0; return function(...args) { const now Date.now(); if (now - last interval) { last now; fn.apply(this, args); } }; }这里有两个容易踩坑的地方。第一返回的函数必须是普通函数不能是箭头函数否则this无法正确绑定到调用方。第二调用fn时要通过apply或call把this传进去否则在对象方法中使用防抖或节流时this会丢失。我在项目里见过不少人在这一步写错导致事件回调里的this指向了undefined。笔试中还会追问防抖和节流分别适合什么场景我的回答是搜索框的远程查询用防抖等用户停止输入后再发请求滚动事件或页面resize用节流保证固定频率内执行一次避免高频触发导致性能问题。如果你能把实现和应用场景结合起来讲得分会高很多。4.3 手写 Promise 与异步流程控制这次笔试还考了一道手写Promise相关的基础题虽然不是让你写完整的PromiseA 规范实现但要求实现一个简单的Promise类支持then和resolve。这道题如果你对Promise的内部机制理解不深很难在限定时间内写出来。我的建议是准备这类题时不要背代码而是想清楚Promise的三个核心概念状态机pending、fulfilled、rejected、异步链式调用、微任务的时机。一个简化版的手写Promise核心思路是这样的class MyPromise { constructor(executor) { this.state pending; this.value undefined; this.reason undefined; this.onFulfilledCallbacks []; this.onRejectedCallbacks []; const resolve (value) { if (this.state pending) { this.state fulfilled; this.value value; this.onFulfilledCallbacks.forEach(fn fn()); } }; const reject (reason) { if (this.state pending) { this.state rejected; this.reason reason; this.onRejectedCallbacks.forEach(fn fn()); } }; try { executor(resolve, reject); } catch (err) { reject(err); } } then(onFulfilled, onRejected) { if (this.state fulfilled) { onFulfilled(this.value); } if (this.state rejected) { onRejected(this.reason); } if (this.state pending) { this.onFulfilledCallbacks.push(() onFulfilled(this.value)); this.onRejectedCallbacks.push(() onRejected(this.reason)); } } }这个实现省略了链式调用的细节但已经能体现Promise的核心状态流转。关键点在于构造函数中的resolve和reject是闭包内部的函数它们只能执行一次因为状态一旦从pending变成fulfilled或rejected后续的resolve或reject调用会被忽略。准备这类手写题我建议你把代码拆成三个层次来记第一层是状态机第二层是回调数组的管理第三层是链式调用的返回值处理。先想清楚第一层再往上层扩展。如果笔试时间紧能写出前两层也已经能拿到大部分分数了。5. 算法题与后续准备从笔试到面试的进阶之路5.1 两道算法题的解题思路复盘这次笔试的算法题一共两道。第一道是求“给定一个字符串找出其中不含有重复字符的最长子串的长度”。这是 LeetCode 第 3 题的变体属于滑动窗口的经典题型。我用的是双指针加哈希表的方法function lengthOfLongestSubstring(s) { const map new Map(); let max 0; let left 0; for (let right 0; right s.length; right) { const char s[right]; if (map.has(char) map.get(char) left) { left map.get(char) 1; } map.set(char, right); max Math.max(max, right - left 1); } return max; }这里有一个细节值得注意map.get(char) left这个判断是为了防止窗口收缩后哈希表里残留的旧索引被错误使用。如果map中字符的索引已经小于left说明这个字符在当前窗口之外可以忽略。这个细节在笔试中容易被遗漏导致答案错误。第二道算法题是“给定一个整数数组求连续子数组的最大和”也就是 LeetCode 第 53 题。这道题可以用动态规划可以用 Kadane 算法。核心思路是维护一个“以当前元素结尾的最大子数组和”要么是当前元素本身要么是当前元素加上前一个“以当前元素结尾的最大子数组和”。function maxSubArray(nums) { let cur nums[0]; let max nums[0]; for (let i 1; i nums.length; i) { cur Math.max(nums[i], cur nums[i]); max Math.max(max, cur); } return max; }这两道算法题难度都不大属于前端岗位笔试的正常范围。我建议前端方向的求职者在准备算法时优先刷“滑动窗口、双指针、动态规划、哈希表、链表操作”这五类题它们是出现频率最高的。5.2 从笔试题看腾讯音乐前端团队的技术倾向复盘完整张试卷我发现腾讯音乐的笔试题有几个明确的技术倾向。第一非常重视JavaScript语言基础特别是ES6的新特性剩余参数、展开语法、箭头函数、Promise、解构赋值这些都是高频考点而且会放在具体的代码场景中来考。第二非常重视Vue框架的原理理解尤其是响应式原理和组件通信这意味着他们的业务中Vue是主力框架候选人需要具备用Vue解决实际问题的能力。第三手写代码题占比高说明他们很看重候选人的代码能力不满足于“纸上谈兵”式的八股背诵。这个判断也符合我对大厂前端岗位的认知。现在的招聘趋势是基础题越来越“实用化”不再只是考记忆而是考理解。比如“说说你对闭包的理解”已经升级成了“给一段代码问输出顺序并解释为什么”“Vue响应式原理是什么”已经升级成了“用代码实现一个迷你响应式系统”或者在具体的场景题里考察。5.3 后续面试准备建议与学习路线笔试只是第一关如果通过了接下来大概率是两到三轮技术面试。从笔试的考点分布来看面试环节你可能需要重点准备以下几个方面第一JavaScript语言基础要能讲透不能只知道概念要能应对面试官的连续追问。比如“闭包是什么”可能会追问到“闭包在Vue源码中的使用场景”或者“闭包导致的内存泄漏如何排查”。第二Vue框架的源码级理解是加分项。建议把Vue 3 的响应式源码、运行时编译、虚拟DOM diff流程都过一遍做到能画图、能讲清主流程。第三手写题要练到“不用想就能写”的熟练度因为面试现场紧张熟练度不够容易卡壳。我准备面试时用过的资料包括Vue 3官方文档、几个优秀博主的源码解析文章、以及一些总结好的前端面试题库。题库的价值不在于背答案而在于帮你发现知识盲区。每道题先自己尝试回答再对照答案找出“我以为我知道但实际说不清楚”的地方那才是真正需要投入时间的地方。如果你正在准备大厂秋招我的建议是每天保持半小时的手写题练习保持对代码的敏感度。算法题不用贪多把常见题型吃透比刷题数量更重要。笔试的临场发挥也很重要遇到不会的题不要慌先跳过把会做的题全部做完再回头思考。另外还有一点笔试时的代码风格也很重要。虽然只要答案正确就能得分但如果你能在代码中体现出对工程化的理解比如写防抖节流时把this处理好写深拷贝时把WeakMap用上这会让阅卷人对你的代码能力有更高的评价。这些细节不一定直接影响笔试成绩但好的代码习惯会通过答题反映出来。
返回列表