Algorithm
1. Promise 与异步并发控制
Q1: 实现一个 Promise.all
Promise.myAll = function (promises) { return new Promise((resolve, reject) => { if (!Array.isArray(promises)) return reject(new TypeError('Argument must be an array')); let result = []; let count = 0; if (promises.length === 0) return resolve(result);
promises.forEach((p, index) => { Promise.resolve(p).then(res => { result[index] = res; // 保证返回顺序 count++; if (count === promises.length) resolve(result); }).catch(reject); // 任一失败则整体失败 }); });};Q2: 实现一个 Promise.allSettled
Promise.myAllSettled = function (promises) { return Promise.myAll(promises.map(p => Promise.resolve(p) .then(value => ({ status: 'fulfilled', value })) .catch(reason => ({ status: 'rejected', reason })) ));};Q3: 实现一个异步任务的调度器 & Q4: 用 JS 实现一个并发控制器
(这两题本质是同一种实现,这里提供一个最经典的并发控制器类)
class Scheduler { constructor(limit) { this.limit = limit; // 最大并发数 this.running = 0; // 当前执行数 this.queue = []; // 等待队列 }
add(taskCreator) { return new Promise((resolve, reject) => { this.queue.push({ taskCreator, resolve, reject }); this.run(); }); }
run() { if (this.running < this.limit && this.queue.length > 0) { this.running++; const { taskCreator, resolve, reject } = this.queue.shift();
taskCreator() .then(resolve) .catch(reject) .finally(() => { this.running--; this.run(); // 递归触发下一个 }); } }}2. 数据结构
Q5: 实现一个 LRUCache
class LRUCache { constructor(capacity) { this.capacity = capacity; this.cache = new Map(); // Map 维持了插入的顺序 }
get(key) { if (!this.cache.has(key)) return -1; const value = this.cache.get(key); // 刷新活跃度:先删后加 this.cache.delete(key); this.cache.set(key, value); return value; }
put(key, value) { if (this.cache.has(key)) { this.cache.delete(key); } else if (this.cache.size >= this.capacity) { // 淘汰最老的:Map.keys().next().value 是第一个插入的键 this.cache.delete(this.cache.keys().next().value); } this.cache.set(key, value); }}Q6: 实现一个深拷贝函数(兼容的数据结构越多越好,需要解决循环依赖情况)
function deepClone(target, map = new WeakMap()) { if(target === null || typeof target !== 'object') return target
if(target instanceof Date) return new Date(target) if(target instanceof RegExp) return new RegExp(target)
if(target instanceof Map) { const cloneMap = new Map() map.set(target, cloneMap) target.forEach((value, key) => { cloneMap.set(deepClone(key, map), deepClone(value, map)) }); return cloneMap }
if(target instanceof Set) { const cloneSet = new Set() map.set(target, cloneSet) target.forEach((value, key) => { cloneSet.add(deepClone(value, map)) }); return cloneSet }
if(map.has(target)) return map.get(target)
const cloneTarget = Array.isArray(target) ? [] : {} map.set(target, cloneTarget) Reflect.ownKeys(target).forEach(k => { cloneTarget[k] = deepClone(target[k], map) });
return cloneTarget}Q7: 如何判断一个单向链表是否存在环?(追问) 有没有别的方式?
// 方式 1:快慢指针 (空间复杂度 O(1),最佳实践)function hasCycle(head) { let slow = head, fast = head; while (fast && fast.next) { slow = slow.next; fast = fast.next.next; if (slow === fast) return true; } return false;}
// 方式 2:Set 缓存法 (空间复杂度 O(N),容易想到)function hasCycleWithSet(head) { const visited = new Set(); while (head) { if (visited.has(head)) return true; visited.add(head); head = head.next; } return false;}3. 数组/字符串与算法核心题
Q8: 算法题:进阶版两数之和。给定一个包含重复元素的数组 nums 和一个目标值 target,找出数组中和为 target 的所有不重复的组合。
function twoSumUnique(nums, target) { nums.sort((a, b) => a - b); // 必须先排序 const res = []; let left = 0, right = nums.length - 1;
while (left < right) { const sum = nums[left] + nums[right]; if (sum === target) { res.push([nums[left], nums[right]]); // 跳过重复元素去重 while (left < right && nums[left] === nums[left + 1]) left++; while (left < right && nums[right] === nums[right - 1]) right--; left++; right--; } else if (sum < target) { left++; } else { right--; } } return res;}Q9: (算法题)给定一个整数数组 nums 和一个整数 target,以及一个整数 k,编写一个函数 kSum,找出数组中所有满足元素个数为 k 且元素之和等于 target 的不重复组合。
function kSum(nums, target, k) { nums.sort((a, b) => a - b);
function nSum(start, target, n) { const res = []; if (n === 2) { // 降维到 2Sum let l = start, r = nums.length - 1; while (l < r) { let sum = nums[l] + nums[r]; if (sum === target) { res.push([nums[l], nums[r]]); while (l < r && nums[l] === nums[l + 1]) l++; while (l < r && nums[r] === nums[r - 1]) r--; l++; r--; } else if (sum < target) l++; else r--; } } else { // 递归拆解 for (let i = start; i < nums.length - n + 1; i++) { if (i > start && nums[i] === nums[i - 1]) continue; // 去重 const subRes = nSum(i + 1, target - nums[i], n - 1); for (let arr of subRes) { res.push([nums[i], ...arr]); } } } return res; } return nSum(0, target, k);}Q10: (算法题)一个整数数组和一个值 k,需要找到这个整数数组里面连续项加起来等于 k 的情况有多少种?
// 前缀和 + 哈希表 (LeetCode 560)function subarraySum(nums, k) { const map = new Map(); map.set(0, 1); // 初始前缀和为0的次数为1 let count = 0; let preSum = 0;
for (const num of nums) { preSum += num; if (map.has(preSum - k)) { count += map.get(preSum - k); } map.set(preSum, (map.get(preSum) || 0) + 1); } return count;}Q11: (算法题)找出一个整形数组里面第二小的元素,怎么做
// 口述逻辑:遍历一次数组。维护 min 和 secondMin 两个变量。function findSecondMinimum(nums) { let min = Infinity, secondMin = Infinity; for (let num of nums) { if (num < min) { secondMin = min; // 原本最小的退居第二 min = num; } else if (num > min && num < secondMin) { secondMin = num; // 介于最小和第二小之间 } } return secondMin === Infinity ? -1 : secondMin;}Q12: 算法题:买卖股票的最佳时机。(给定一个数组,代表每天的股票价格,求最大利润)。(贪心)
function maxProfit(prices) { let minPrice = Infinity; let maxProfit = 0; for (let price of prices) { if (price < minPrice) { minPrice = price; // 记录历史最低点 } else if (price - minPrice > maxProfit) { maxProfit = price - minPrice; // 更新最大利润 } } return maxProfit;}Q13: 算法题:有序数组的子集判断
function isSubsequence(sub, main) { let i = 0, j = 0; while (i < main.length && j < sub.length) { if (main[i] === sub[j]) j++; // 匹配上了,子集指针右移 i++; // 主数组指针永远右移 } return j === sub.length;}Q14: (算法题) LeetCode:3375. 使数组的值全部为 K 的最少操作次数
function minOperations(nums, k) { const set = new Set(nums); let hasSmaller = false;
for (let num of set) { if (num < k) return -1; // 只能减小,如果原本就有比 K 小的,永远无法全部变成 K }
// 需要操作的次数就是大于 K 的不同元素的种类数 return set.has(k) ? set.size - 1 : set.size;}Q15: 第一题:二叉树每层最右节点 (二叉树的右视图)
function rightSideView(root) { if (!root) return []; const res = []; const queue = [root];
while (queue.length > 0) { let size = queue.length; for (let i = 0; i < size; i++) { let node = queue.shift(); if (i === size - 1) res.push(node.val); // 记录该层最后一个节点 if (node.left) queue.push(node.left); if (node.right) queue.push(node.right); } } return res;}Q16: 第二题:实现一个 JSON.stringify,功能尽可能完善
function myStringify(data) { const type = typeof data;
if (type !== 'object' || data === null) { if (type === 'string') return `"${data}"`; if (type === 'function' || type === 'undefined' || type === 'symbol') return undefined; return String(data); }
if (data instanceof Date) return `"${data.toISOString()}"`;
if (Array.isArray(data)) { const arrStr = data.map(item => myStringify(item) || 'null'); return `[${arrStr.join(',')}]`; }
const objStr = Object.keys(data).reduce((acc, key) => { const valStr = myStringify(data[key]); if (valStr !== undefined) acc.push(`"${key}":${valStr}`); return acc; }, []);
return `{${objStr.join(',')}}`;}4. 组件实现 (核心逻辑层)
Q17: (手写代码)请编写一个倒计时组件/方法,按秒维度倒计时。
// 基于原生 JS 的核心计算逻辑,可在 React/Vue 中封装为 HookfunctionstartCountdown(endTimeStr, onTick) { const endTime = new Date(endTimeStr).getTime();
const timer = setInterval(() => { const now = Date.now(); const diff = Math.max(0, endTime - now); // 毫秒差 if (diff === 0) { clearInterval(timer); onTick({ h: 0, m: 0, s: 0, finished: true }); return; }
const h = Math.floor(diff / (1000 * 60 * 60)); const m = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60)); const s = Math.floor((diff % (1000 * 60)) / 1000);
onTick({ h, m, s, finished: false }); }, 1000); // 实际生产中推荐 requestAnimationFrame 以避免后台休眠导致的节流 return () => clearInterval(timer); // 返回清理函数}Q18: (手撕代码)JS 写一个搜索过滤组件 (带有防抖)
function debounce(fn, delay) { let timer = null; return function(...args) { clearTimeout(timer); timer = setTimeout(() => fn.apply(this, args), delay); };}
class SearchFilter { constructor(dataList, inputEl, resultEl) { this.dataList = dataList; this.resultEl = resultEl;
// 绑定防抖事件 inputEl.addEventListener('input', debounce((e) => { this.handleSearch(e.target.value); }, 300)); }
handleSearch(keyword) { if (!keyword.trim()) { this.render([]); return; } const filtered = this.dataList.filter(item => item.includes(keyword)); this.render(filtered); }
render(list) { if (list.length === 0) { this.resultEl.innerHTML = '<div>没匹配到了</div>'; } else { this.resultEl.innerHTML = list.map(i => `<div>${i}</div>`).join(''); } }}Q19: 设计一个通用的全屏覆盖层组件(Overlay/Dialog),怎么设计?需要管理生命周期吗?
// React 伪代码:考察 Portal、生命周期管理与事件拦截import { useEffect } from 'react';import { createPortal } from 'react-dom';
function Dialog({ isOpen, onClose, children }) { useEffect(() => { if (!isOpen) return;
// 生命周期管理 1: 阻止背景滚动 document.body.style.overflow = 'hidden';
// 生命周期管理 2: 监听 ESC 键 const handleEsc = (e) => e.key === 'Escape' && onClose(); window.addEventListener('keydown', handleEsc);
return () => { // 销毁时清理 document.body.style.overflow = 'auto'; window.removeEventListener('keydown', handleEsc); }; }, [isOpen, onClose]);
if (!isOpen) return null;
// 使用 Portal 挂载到 body 下,脱离当前 DOM 树的 CSS 层叠上下文限制 return createPortal( <div className="overlay" onClick={onClose} style={{position: 'fixed', inset: 0}}><div className="content" onClick={e => e.stopPropagation()}> {/* 阻止点击穿透 */} {children} </div></div>, document.body );}5. 其他编程题
Q20: (编程题)实现一个 Add 函数,运算只能通过调用 expensiveCall 实现
Q21: (编程题 2)监听列表元素曝光(可以查 API 文档)
function observeElements(selector, onVisible) { const observer = new IntersectionObserver((entries, obs) => { entries.forEach(entry => { if (entry.isIntersecting) { onVisible(entry.target); // 触发曝光上报逻辑 obs.unobserve(entry.target); // 上报后取消监听,节省性能 } }); }, { threshold: 0.5 }); // 露出 50% 算作曝光 document.querySelectorAll(selector).forEach(el => observer.observe(el));}Q22: (编程题 3)开放题 为了性能要求,我们页面经常会组装一个 combo 请求
Q23: (算法题)遇到中括号 [n] 重复对应次数,小括号 () 作为整体。
(此类题通常变种自 LeetCode 394 字符串解码,此处提供经典的栈解析法)
function decodeString(s) { let numStack = []; let strStack = []; let currentStr = ''; let currentNum = 0;
for (let char of s) { if (!isNaN(char)) { currentNum = currentNum * 10 + Number(char); } else if (char === '(' || char === '[') { numStack.push(currentNum || 1); // 默认倍数为 1 strStack.push(currentStr); currentStr = ''; currentNum = 0; } else if (char === ')' || char === ']') { const repeatTimes = numStack.pop(); const prevStr = strStack.pop(); currentStr = prevStr + currentStr.repeat(repeatTimes); } else { currentStr += char; } } return currentStr;}Q24: 第三题:微信团队组织员工去公园郊游,需要从租车点租赁自行车 (贪心双指针)
(背景:每车最多坐两人,有最大载重 limit)
function numRescueBikes(people, limit) { people.sort((a, b) => a - b); let left = 0; let right = people.length - 1; let bikes = 0;
while (left <= right) { // 如果最轻和最重的能拼一辆车 if (people[left] + people[right] <= limit) { left++; } // 无论是否拼车,最重的人都要消耗一辆车 right--; bikes++; } return bikes;}