以下是腾讯及腾讯音乐娱乐(TME)前端岗位高频手撕题目详解,结合真题及考察要点整理,覆盖面试核心考点:
⚙️ 一、核心手撕题(腾讯/TME 必考)
1. Promise 并发控制(90%场次出现)
- 题目:手写
Promise.all
(需处理错误短路、位置保持) - 核心思路:
- 校验输入为数组,空数组直接
resolve([])
。 - 遍历 Promise 数组,用
Promise.resolve
包装非 Promise 值。 - 计数完成量,全部成功时返回结果数组;任一失败立即
reject
。
- 校验输入为数组,空数组直接
- 边界处理:
Promise.myAll = (promises) => { if (!Array.isArray(promises)) return Promise.reject(new TypeError('Argument must be an array')); let count = 0, results = []; return new Promise((resolve, reject) => { promises.forEach((p, i) => { Promise.resolve(p).then(res => { results[i] = res; // 保持结果位置 if (++count === promises.length) resolve(results); }).catch(reject); // 短路逻辑 }); if (promises.length === 0) resolve(results); }); };
2. 数组扁平化(80%场次出现)
- 题目:实现多层嵌套数组降维(如
[1, [2, [3]]] → [1, 2, 3]
) - 方案对比:
- 递归法:深度优先遍历,遇到数组则递归展开。
flat
API:直接调用arr.flat(Infinity)
(需注意浏览器兼容性)。- Reduce 递归:
const flatten = (arr) => arr.reduce((pre, cur) => pre.concat(Array.isArray(cur) ? flatten(cur) : cur), []);
3. 深拷贝(70%场次出现)
- 考点:处理循环引用、特殊对象(Date/RegExp)
- 代码关键点:
- 使用
WeakMap
缓存已拷贝对象,避免循环引用导致的栈溢出。 - 特殊对象单独处理(如
new Date(obj)
)。
function deepClone(obj, map = new WeakMap()) { if (obj === null || typeof obj !== 'object') return obj; if (map.has(obj)) return map.get(obj); const clone = obj instanceof Date ? new Date(obj) : obj instanceof RegExp ? new RegExp(obj) : Array.isArray(obj) ? [] : {}; map.set(obj, clone); Reflect.ownKeys(obj).forEach(key => { clone[key] = deepClone(obj[key], map); }); return clone; }
- 使用
⚡️ 二、特色场景题(腾讯音乐TME高频)
1. 页面通信与崩溃监控
- 题目:从页面A打开页面B,B关闭(含崩溃)时通知A
- 解决方案:
- 正常关闭:在B的
window.onbeforeunload
中通过localStorage
或postMessage
传参,A监听storage
或message
事件。 - 崩溃监控:
- B页面定时(5s)向Service Worker发送"心跳"。
- Service Worker检测超时(15s无心跳)判定崩溃,通知A页面。
// B页面心跳发送 setInterval(() => navigator.serviceWorker.controller.postMessage({ type: 'heartbeat' }), 5000); // Service Worker检测逻辑 if (Date.now() - lastHeartbeat > 15000) reportCrash();
- 正常关闭:在B的
2. 大数相加(校招重点)
- 题目:实现超过JS精度限制的数字加法(如
"9999999999999999" + "1"
) - 思路:
- 字符串反转,按位相加并处理进位。
- 注意高位补位(如最终进位不为0)。
function addBigNumbers(a, b) { const arr1 = a.split('').reverse(), arr2 = b.split('').reverse(); let result = [], carry = 0; for (let i = 0; i < Math.max(arr1.length, arr2.length); i++) { const sum = (parseInt(arr1[i] || 0) + parseInt(arr2[i] || 0) + carry); result.push(sum % 10); carry = Math.floor(sum / 10); } if (carry) result.push(carry); return result.reverse().join(''); }
3. 二叉树遍历(基础算法)
- 题目:实现二叉树前序/中序/后序遍历(递归与非递归)
- 递归示例:
const preorder = (root, res = []) => { if (!root) return res; res.push(root.val); // 前序:根左右 preorder(root.left, res); preorder(root.right, res); return res; };
💡 三、答题技巧与避坑点
- 原理深挖:
- 腾讯必问实现逻辑(如
Promise.all
的并发控制、深拷贝的循环引用处理)。 - 避免只答API用法(如被追问“
flat
的内部实现”)。
- 腾讯必问实现逻辑(如
- 边界处理:
- 空输入、极端用例(如大数相加的进位溢出)需显式处理。
- 工程化思维:
- 结合业务场景(如页面崩溃监控需说明Service Worker的独立线程特性)。
腾讯系面试注重原理实现深度与场景落地能力,建议优先掌握以上高频题,并扩展练习虚拟DOM Diff、响应式原理(Proxy/defineProperty)等进阶题。
JavaScript 算法详解
1. 最大公共前缀
/**
* 查找字符串数组中的最长公共前缀
* @param {string[]} strs 字符串数组
* @return {string} 最长公共前缀
*/
function longestCommonPrefix(strs) {
let res='';
if(strs.length===0)return res;
const val=strs[0];
for(let i=0;i<val.length;i++){
let curChar=val[i];
for(let j=0;j<strs.length;j++){
if(strs[j][i]!==curChar)return res;
if(j===strs.length-1) res+=curChar;
}
}
return res;
}
// 示例
console.log(longestCommonPrefix(["flower","flow","flight"])); // "fl"
console.log(longestCommonPrefix(["dog","racecar","car"])); // ""
算法思路:
- 如果数组为空,直接返回空字符串
- 以第一个字符串作为初始公共前缀
- 遍历数组中的每个字符串,与当前公共前缀进行比较
- 如果不匹配,则缩短公共前缀,直到匹配或变为空字符串
- 返回最终的公共前缀
2. 最大子序列和
/**
* 查找数组中连续子序列的最大和(Kadane算法)
* @param {number[]} nums 数字数组
* @return {number} 最大子序列和
*/
function maxSubArray(nums) {
let max=Math.max(...arr);
let curSum=0;
for(let i=0;i<arr.length;i++){
curSum+=arr[i];
max=Math.max(curSum,max);
if(curSum<0)curSum=0;
}
return max;
}
// 示例
console.log(maxSubArray([-2,1,-3,4,-1,2,1,-5,4])); // 6 (对应子序列 [4,-1,2,1])
console.log(maxSubArray([-1,-2,-3])); // -1
算法思路(Kadane算法):
- 初始化当前最大值和全局最大值为第一个元素
- 遍历数组:
- 对于每个元素,决定是将其加入当前子序列还是开始新的子序列
- 更新全局最大值
- 返回全局最大值
3. 重复子字符串
/**
* 判断字符串是否可以由它的一个子串重复多次构成
* @param {string} s 输入字符串
* @return {boolean} 是否可以由子串重复构成
*/
function repeatedSubstringPattern(s) {
// 将字符串与自身拼接,然后去掉首尾字符
const doubled = s + s;
const sliced = doubled.slice(1, -1);
// 如果原字符串出现在拼接后的字符串中,则可以由子串重复构成
return sliced.includes(s);
}
// 示例
console.log(repeatedSubstringPattern("abab")); // true (可由 "ab" 重复构成)
console.log(repeatedSubstringPattern("aba")); // false
console.log(repeatedSubstringPattern("abcabcabc")); // true (可由 "abc" 重复构成)
算法思路:
- 将原字符串与自身拼接
- 去掉拼接后字符串的首尾字符
- 如果原字符串出现在处理后的字符串中,则说明可以由子串重复构成
- 这种方法利用了字符串旋转和模式匹配的原理
数学解释:
- 如果一个字符串S可以由子串重复构成,那么S = n*sub
- 将S+S = 2n*sub
- 去掉首尾字符后,中间至少包含一个完整的S = n*sub
- 因此S会出现在处理后的字符串中
这三个算法分别展示了字符串处理和动态规划的经典问题解决方案。