前端算法与数据结构
What — 前端工程师为什么需要算法
前端工程师与算法的关系
前端工程师并非只需要掌握 CSS 和 DOM 操作。算法能力在以下场景中至关重要:
- 性能优化:列表虚拟化、搜索过滤、大量数据处理都需要算法思维
- 面试准备:大厂前端面试中算法题占比逐年上升,LeetCode 已成为标配
- 框架源码理解:Virtual DOM diff(树 diffing)、React Fiber 调度(链表遍历)、Vue 响应式收集(依赖图)等核心机制都依赖经典数据结构与算法
- 工程工具链:打包器模块图(图与拓扑排序)、路由匹配(Trie 树)、模板编译(栈解析)无一不需要算法支撑
算法复杂度基础
大 O 表示法描述算法运行时间/空间随输入规模增长的上界。
| 复杂度 | 名称 | 典型示例 |
|---|---|---|
| O(1) | 常数 | 哈希表查找、数组按下标访问 |
| O(log n) | 对数 | 二分查找 |
| O(n) | 线性 | 遍历数组、链表 |
| O(n log n) | 线性对数 | 快排、归并排序 |
| O(n²) | 平方 | 冒泡排序、双层循环 |
| O(2ⁿ) | 指数 | 暴力子集枚举 |
| O(n!) | 阶乘 | 全排列 |
面试技巧:先说思路,再写代码,最后分析时间/空间复杂度。复杂度分析是加分项。
前端相关数据结构
| 数据结构 | 前端应用场景 | JS 中的体现 |
|---|---|---|
| Array | 列表渲染、数据存储 | Array 原生支持 |
| Stack | 模板编译、括号匹配、浏览器历史 | 用数组模拟 |
| Queue | 任务队列、消息队列、BFS | 用数组模拟 |
| Linked List | React Fiber、LRU 缓存 | 手动实现 |
| Hash Map | 缓存、计数、映射 | Map / WeakMap / Object |
| Tree | DOM 树、Virtual DOM、组件树 | 嵌套对象 |
| Graph | 模块依赖图、社交关系 | 邻接表 / 邻接矩阵 |
| Heap | 调度器优先队列、Top-K | 手动实现 |
Why — 算法在前端中的真实存在
前端框架与工具中的算法映射
| 前端场景 | 涉及算法/数据结构 | 说明 |
|---|---|---|
| Virtual DOM diff | 树的 DFS、同层比较 | React/Vue 的 diff 算法本质是树的深度优先遍历 + 同层比对策略 |
| React Fiber | 链表(单链表 + 子节点指针) | Fiber 节点用 child/sibling/return 构成链表,支持可中断渲染 |
| React 调度 | 小顶堆(优先队列) | scheduler 包用最小堆按过期时间排序任务 |
| LRU 缓存 | 哈希表 + 双向链表 | Vue 的 keep-alive、浏览器缓存淘汰策略 |
| 防抖/节流 | 定时器 + 时间戳 | 限制高频事件的执行频率 |
| 路由匹配 | Trie 树(前缀树) | path-to-regexp、Radix Router 按前缀匹配路由 |
| 模板引擎 | 栈(括号匹配/标签解析) | Vue 模板编译器用栈解析 HTML 标签嵌套 |
| 模块打包 | 图 + 拓扑排序 | Webpack/Vite 构建依赖图,按拓扑序输出 |
| 事件循环 | 队列(宏任务/微任务) | 浏览器 Event Loop 本质是多优先级队列调度 |
| 虚拟列表 | 二分查找 | 根据滚动位置二分查找可见区间起始索引 |
前端 vs 后端算法关注点对比
| 维度 | 前端算法 | 后端算法 |
|---|---|---|
| 数据规模 | 中小(通常 < 10⁶) | 大(可达 10⁹+) |
| 核心关注 | 交互响应、渲染性能 | 吞吐量、并发处理 |
| 典型问题 | DOM diff、虚拟滚动、防抖节流 | 分布式一致性、索引优化、流式处理 |
| 复杂度要求 | O(n) ~ O(n log n) 为主 | 可接受 O(n²) 批处理,追求 O(1) 查询 |
| 数据结构偏好 | 树、栈、哈希表 | B+ 树、布隆过滤器、跳表 |
| 面试侧重 | 实现题(手写)、场景题 | 系统设计、高并发场景 |
核心认知:前端算法更侧重”实现能力”和”场景落地”,而非极致优化。面试中手写实现题(如 LRU、深拷贝、EventEmitter)的频率远高于纯算法推导。
How — 前端算法学习与实践
1. 数组与字符串
核心技巧:双指针、滑动窗口、前缀和。
双指针
双指针适用于有序数组的查找、原地修改等场景。
// 合并两个有序数组(力扣 88)
function merge(
nums1: number[],
m: number,
nums2: number[],
n: number
): void {
let i = m - 1;
let j = n - 1;
let k = m + n - 1;
while (i >= 0 && j >= 0) {
nums1[k--] = nums1[i] > nums2[j] ? nums1[i--] : nums2[j--];
}
while (j >= 0) {
nums1[k--] = nums2[j--];
}
}
滑动窗口
滑动窗口适用于连续子数组/子串问题。
// 无重复字符的最长子串(力扣 3)
function lengthOfLongestSubstring(s: string): number {
const charIndex = new Map<string, number>();
let left = 0;
let maxLen = 0;
for (let right = 0; right < s.length; right++) {
const ch = s[right];
if (charIndex.has(ch) && charIndex.get(ch)! >= left) {
left = charIndex.get(ch)! + 1;
}
charIndex.set(ch, right);
maxLen = Math.max(maxLen, right - left + 1);
}
return maxLen;
}
数组扁平化
// 递归实现
function flatten(arr: unknown[]): unknown[] {
const result: unknown[] = [];
for (const item of arr) {
if (Array.isArray(item)) {
result.push(...flatten(item));
} else {
result.push(item);
}
}
return result;
}
// 迭代实现(栈)
function flattenIterative(arr: unknown[]): unknown[] {
const stack = [...arr];
const result: unknown[] = [];
while (stack.length) {
const item = stack.pop()!;
if (Array.isArray(item)) {
stack.push(...item);
} else {
result.push(item);
}
}
return result.reverse();
}
2. 栈与队列
核心技巧:栈处理”最近相关性”(括号、嵌套),队列处理”先进先出”顺序。
有效括号
// 力扣 20:有效的括号
function isValid(s: string): boolean {
const stack: string[] = [];
const pairs: Record<string, string> = {
')': '(',
']': '[',
'}': '{',
};
for (const ch of s) {
if (ch in pairs) {
if (stack.pop() !== pairs[ch]) return false;
} else {
stack.push(ch);
}
}
return stack.length === 0;
}
浏览器前进后退(栈)
class BrowserHistory {
private backStack: string[] = [];
private forwardStack: string[] = [];
private current: string;
constructor(url: string) {
this.current = url;
}
visit(url: string): void {
this.backStack.push(this.current);
this.current = url;
this.forwardStack = []; // 新访问清空前进栈
}
back(): string {
if (this.backStack.length === 0) return this.current;
this.forwardStack.push(this.current);
this.current = this.backStack.pop()!;
return this.current;
}
forward(): string {
if (this.forwardStack.length === 0) return this.current;
this.backStack.push(this.current);
this.current = this.forwardStack.pop()!;
return this.current;
}
}
事件循环模拟(队列)
class EventLoopSimulator {
private macroQueue: (() => void)[] = [];
private microQueue: (() => void)[] = [];
queueMacro(fn: () => void): void {
this.macroQueue.push(fn);
}
queueMicro(fn: () => void): void {
this.microQueue.push(fn);
}
async run(): Promise<void> {
while (this.macroQueue.length > 0) {
// 取一个宏任务执行
const macro = this.macroQueue.shift()!;
macro();
// 清空所有微任务
while (this.microQueue.length > 0) {
const micro = this.microQueue.shift()!;
micro();
}
}
}
}
3. 链表
核心技巧:哑节点(dummy head)、快慢指针、递归。
反转链表
interface ListNode {
val: number;
next: ListNode | null;
}
// 迭代反转
function reverseList(head: ListNode | null): ListNode | null {
let prev: ListNode | null = null;
let curr = head;
while (curr) {
const next = curr.next;
curr.next = prev;
prev = curr;
curr = next;
}
return prev;
}
检测环(快慢指针)
function hasCycle(head: ListNode | null): boolean {
let slow = head;
let fast = head;
while (fast && fast.next) {
slow = slow!.next;
fast = fast.next.next;
if (slow === fast) return true;
}
return false;
}
与 React Fiber 的联系
React Fiber 架构将组件树用链表重新组织:
Fiber 节点结构:
child → 第一个子节点
sibling → 下一个兄弟节点
return → 父节点
这种链表结构使得遍历可以被中断和恢复,是 Fiber 可中断渲染的基础。Fiber 的工作循环本质上就是对链表的深度优先遍历(用循环而非递归实现)。
// Fiber 遍历简化模型
function workLoop(root: FiberNode): void {
let current: FiberNode | null = root;
while (current) {
// 执行工作单元...
// 深度优先:优先 child,其次 sibling,最后 return
if (current.child) {
current = current.child;
} else if (current.sibling) {
current = current.sibling;
} else {
// 回溯到有未访问 sibling 的祖先
current = current.return;
while (current && !current.sibling) {
current = current.return;
}
if (current) current = current.sibling;
}
}
}
4. 哈希表
核心技巧:以空间换时间、键值映射、去重计数。
两数之和
function twoSum(nums: number[], target: number): number[] {
const map = new Map<number, number>();
for (let i = 0; i < nums.length; i++) {
const complement = target - nums[i];
if (map.has(complement)) {
return [map.get(complement)!, i];
}
map.set(nums[i], i);
}
return [];
}
LRU 缓存
LRU(Least Recently Used)是前端面试高频题,Vue 的 keep-alive 组件即采用此策略。
class LRUCache {
private capacity: number;
private map = new Map<number, DLLNode>();
private head: DLLNode; // 哨兵头
private tail: DLLNode; // 哨兵尾
constructor(capacity: number) {
this.capacity = capacity;
this.head = { key: 0, value: 0, prev: null, next: null };
this.tail = { key: 0, value: 0, prev: null, next: null };
this.head.next = this.tail;
this.tail.prev = this.head;
}
get(key: number): number {
if (!this.map.has(key)) return -1;
const node = this.map.get(key)!;
this.removeNode(node);
this.addToHead(node);
return node.value;
}
put(key: number, value: number): void {
if (this.map.has(key)) {
const node = this.map.get(key)!;
node.value = value;
this.removeNode(node);
this.addToHead(node);
} else {
const node: DLLNode = { key, value, prev: null, next: null };
this.map.set(key, node);
this.addToHead(node);
if (this.map.size > this.capacity) {
const removed = this.tail.prev!;
this.removeNode(removed);
this.map.delete(removed.key);
}
}
}
private removeNode(node: DLLNode): void {
node.prev!.next = node.next;
node.next!.prev = node.prev;
}
private addToHead(node: DLLNode): void {
node.next = this.head.next;
node.prev = this.head;
this.head.next!.prev = node;
this.head.next = node;
}
}
interface DLLNode {
key: number;
value: number;
prev: DLLNode | null;
next: DLLNode | null;
}
与 JS Map/WeakMap 的联系
| 特性 | Map | WeakMap | Object |
|---|---|---|---|
| 键类型 | 任意 | 仅对象 | 字符串/Symbol |
| 可遍历 | 是 | 否 | 是 |
| 垃圾回收 | 不会自动释放键 | 键可被 GC | 不会自动释放 |
| 前端用途 | 通用缓存、计数 | DOM 关联数据、组件实例缓存 | 配置对象、字典 |
WeakMap常用于将 DOM 节点与数据关联,当 DOM 节点被移除时,关联数据自动被 GC 回收,避免内存泄漏。
5. 树
核心技巧:递归(DFS)、层序遍历(BFS)、回溯。
三种遍历
interface TreeNode {
val: number;
left: TreeNode | null;
right: TreeNode | null;
}
// 前序遍历(根-左-右)
function preorder(root: TreeNode | null, result: number[] = []): number[] {
if (!root) return result;
result.push(root.val);
preorder(root.left, result);
preorder(root.right, result);
return result;
}
// 中序遍历(左-根-右)
function inorder(root: TreeNode | null, result: number[] = []): number[] {
if (!root) return result;
inorder(root.left, result);
result.push(root.val);
inorder(root.right, result);
return result;
}
// 层序遍历(BFS)
function levelOrder(root: TreeNode | null): number[][] {
if (!root) return [];
const result: number[][] = [];
const queue: TreeNode[] = [root];
while (queue.length) {
const level: number[] = [];
const size = queue.length;
for (let i = 0; i < size; i++) {
const node = queue.shift()!;
level.push(node.val);
if (node.left) queue.push(node.left);
if (node.right) queue.push(node.right);
}
result.push(level);
}
return result;
}
最大深度
function maxDepth(root: TreeNode | null): number {
if (!root) return 0;
return 1 + Math.max(maxDepth(root.left), maxDepth(root.right));
}
与 DOM 树 / Virtual DOM diff 的联系
DOM 本质是一棵树,Virtual DOM diff 算法就是在这棵树上做同层对比:
// 简化版 Virtual DOM diff
interface VNode {
tag: string;
props: Record<string, unknown>;
children: (VNode | string)[];
key?: string | number;
}
function diff(oldNode: VNode, newNode: VNode): Patch[] {
const patches: Patch[] = [];
// 1. 同层比较 tag
if (oldNode.tag !== newNode.tag) {
patches.push({ type: 'REPLACE', node: newNode });
return patches;
}
// 2. 比较属性
const propPatches = diffProps(oldNode.props, newNode.props);
if (propPatches) patches.push({ type: 'PROPS', props: propPatches });
// 3. 比较子节点(使用 key 优化)
const childPatches = diffChildren(oldNode.children, newNode.children);
patches.push(...childPatches);
return patches;
}
type Patch =
| { type: 'REPLACE'; node: VNode }
| { type: 'PROPS'; props: Record<string, unknown> }
| { type: 'REORDER'; moves: Move[] }
| { type: 'TEXT'; content: string };
interface Move { index: number; type: 'INSERT' | 'REMOVE'; key?: string | number; }
React diff 三策略:1) 同层比较(不跨层)2) 不同类型直接替换 3) 使用 key 优化列表复用。这些策略将 O(n³) 的树 diff 降为 O(n)。
6. 图
核心技巧:BFS、DFS、拓扑排序。前端主要涉及依赖图分析。
拓扑排序(模块依赖检测)
// 检测模块依赖图中是否存在循环依赖
function hasCircularDependency(
graph: Map<string, string[]>
): boolean {
const visited = new Set<string>();
const inStack = new Set<string>();
function dfs(node: string): boolean {
if (inStack.has(node)) return true; // 环检测
if (visited.has(node)) return false;
visited.add(node);
inStack.add(node);
for (const neighbor of graph.get(node) ?? []) {
if (dfs(neighbor)) return true;
}
inStack.delete(node);
return false;
}
for (const node of graph.keys()) {
if (dfs(node)) return true;
}
return false;
}
// 拓扑排序(构建顺序)
function topologicalSort(graph: Map<string, string[]>): string[] {
const inDegree = new Map<string, number>();
for (const node of graph.keys()) {
inDegree.set(node, 0);
}
for (const [, deps] of graph) {
for (const dep of deps) {
inDegree.set(dep, (inDegree.get(dep) ?? 0) + 1);
}
}
const queue: string[] = [];
for (const [node, degree] of inDegree) {
if (degree === 0) queue.push(node);
}
const result: string[] = [];
while (queue.length) {
const node = queue.shift()!;
result.push(node);
for (const dep of graph.get(node) ?? []) {
inDegree.set(dep, inDegree.get(dep)! - 1);
if (inDegree.get(dep) === 0) queue.push(dep);
}
}
return result.length === graph.size ? result : []; // 空数组表示有环
}
与模块依赖图的联系
Webpack/Vite 在构建时需要分析模块间的 import 依赖关系,构建有向图后通过拓扑排序确定打包顺序。循环依赖(环)会导致运行时问题,因此检测环是关键步骤。
7. 排序
前端关注点:理解 Array.prototype.sort() 的底层实现及其稳定性。
快速排序
function quickSort(arr: number[]): number[] {
if (arr.length <= 1) return arr;
const pivot = arr[Math.floor(arr.length / 2)];
const left = arr.filter((x) => x < pivot);
const middle = arr.filter((x) => x === pivot);
const right = arr.filter((x) => x > pivot);
return [...quickSort(left), ...middle, ...quickSort(right)];
}
归并排序(稳定排序)
function mergeSort(arr: number[]): number[] {
if (arr.length <= 1) return arr;
const mid = Math.floor(arr.length / 2);
const left = mergeSort(arr.slice(0, mid));
const right = mergeSort(arr.slice(mid));
return merge(left, right);
}
function merge(left: number[], right: number[]): number[] {
const result: number[] = [];
let i = 0;
let j = 0;
while (i < left.length && j < right.length) {
if (left[i] <= right[j]) {
result.push(left[i++]);
} else {
result.push(right[j++]);
}
}
return [...result, ...left.slice(i), ...right.slice(j)];
}
Array.sort() 的实现
| 引擎 | 算法 | 稳定性 |
|---|---|---|
| V8 (Chrome) | TimSort(归并+插入) | 稳定 |
| SpiderMonkey (Firefox) | TimSort | 稳定 |
| JavaScriptCore (Safari) | TimSort | 稳定 |
ES2019 规范要求
Array.prototype.sort()必须是稳定排序。在此之前各引擎行为不一致,是一个常见面试陷阱。
8. 二分查找
核心技巧:在有序数据中 O(log n) 查找。
// 标准二分查找
function binarySearch(arr: number[], target: number): number {
let left = 0;
let right = arr.length - 1;
while (left <= right) {
const mid = left + ((right - left) >> 1); // 防溢出写法
if (arr[mid] === target) return mid;
if (arr[mid] < target) left = mid + 1;
else right = mid - 1;
}
return -1;
}
// 查找插入位置(左边界)
function searchInsert(arr: number[], target: number): number {
let left = 0;
let right = arr.length;
while (left < right) {
const mid = left + ((right - left) >> 1);
if (arr[mid] < target) left = mid + 1;
else right = mid;
}
return left;
}
与虚拟列表的联系
虚拟列表根据 scrollTop 在已排序的位置数组中二分查找起始索引,实现 O(log n) 定位可视区域。
9. 动态规划
前端面试中 DP 难度一般不高,掌握经典入门题即可。
斐波那契(记忆化 / 滚动变量)
// 滚动变量优化空间至 O(1)
function fib(n: number): number {
if (n <= 1) return n;
let prev2 = 0;
let prev1 = 1;
for (let i = 2; i <= n; i++) {
const curr = prev1 + prev2;
prev2 = prev1;
prev1 = curr;
}
return prev1;
}
爬楼梯
function climbStairs(n: number): number {
if (n <= 2) return n;
let prev2 = 1;
let prev1 = 2;
for (let i = 3; i <= n; i++) {
const curr = prev1 + prev2;
prev2 = prev1;
prev1 = curr;
}
return prev1;
}
最长递增子序列
// O(n log n) 解法(贪心 + 二分)
function lengthOfLIS(nums: number[]): number {
const tails: number[] = [];
for (const num of nums) {
let left = 0;
let right = tails.length;
while (left < right) {
const mid = left + ((right - left) >> 1);
if (tails[mid] < num) left = mid + 1;
else right = mid;
}
if (left === tails.length) tails.push(num);
else tails[left] = num;
}
return tails.length;
}
Vue 3 的 DOM diff 中使用了最长递增子序列来最小化 DOM 移动操作。
10. 经典前端算法实现
LRU Cache(完整版)
见上文第 4 节。
Trie 前缀树(路由匹配)
class TrieNode {
children: Map<string, TrieNode> = new Map();
isEnd: boolean = false;
handler?: () => void;
}
class RouterTrie {
private root = new TrieNode();
insert(path: string, handler: () => void): void {
let node = this.root;
for (const ch of path) {
if (!node.children.has(ch)) {
node.children.set(ch, new TrieNode());
}
node = node.children.get(ch)!;
}
node.isEnd = true;
node.handler = handler;
}
search(path: string): (() => void) | null {
let node = this.root;
for (const ch of path) {
if (!node.children.has(ch)) return null;
node = node.children.get(ch)!;
}
return node.isEnd ? node.handler ?? null : null;
}
startsWith(prefix: string): boolean {
let node = this.root;
for (const ch of prefix) {
if (!node.children.has(ch)) return false;
node = node.children.get(ch)!;
}
return true;
}
}
防抖与节流
// 防抖:延迟执行,重复调用重置计时器
function debounce<T extends (...args: unknown[]) => void>(
fn: T,
delay: number,
options: { leading?: boolean; trailing?: boolean } = {}
): T & { cancel: () => void } {
const { leading = false, trailing = true } = options;
let timer: ReturnType<typeof setTimeout> | null = null;
let lastCallTime = 0;
function debounced(this: unknown, ...args: unknown[]) {
const now = Date.now();
// leading: 第一次调用立即执行
if (leading && now - lastCallTime > delay) {
fn.apply(this, args);
lastCallTime = now;
}
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
if (trailing) fn.apply(this, args);
timer = null;
}, delay);
}
debounced.cancel = () => {
if (timer) clearTimeout(timer);
timer = null;
};
return debounced as T & { cancel: () => void };
}
// 节流:固定间隔执行,高频调用只按间隔触发
function throttle<T extends (...args: unknown[]) => void>(
fn: T,
interval: number,
options: { leading?: boolean; trailing?: boolean } = {}
): T & { cancel: () => void } {
const { leading = true, trailing = true } = options;
let timer: ReturnType<typeof setTimeout> | null = null;
let lastCallTime = 0;
let lastArgs: unknown[] | null = null;
function throttled(this: unknown, ...args: unknown[]) {
const now = Date.now();
if (!lastCallTime && !leading) {
lastCallTime = now;
}
const remaining = interval - (now - lastCallTime);
if (remaining <= 0) {
if (timer) {
clearTimeout(timer);
timer = null;
}
fn.apply(this, args);
lastCallTime = now;
lastArgs = null;
} else if (trailing && !timer) {
lastArgs = args;
timer = setTimeout(() => {
if (lastArgs) fn.apply(this, lastArgs);
timer = null;
lastCallTime = leading ? Date.now() : 0;
lastArgs = null;
}, remaining);
}
}
throttled.cancel = () => {
if (timer) clearTimeout(timer);
timer = null;
lastCallTime = 0;
lastArgs = null;
};
return throttled as T & { cancel: () => void };
}
深拷贝(含循环检测)
function deepClone<T>(obj: T, visited = new WeakMap<object, unknown>()): T {
// 基本类型
if (obj === null || typeof obj !== 'object') return obj;
// 循环引用检测
if (visited.has(obj as object)) return visited.get(obj as object) as T;
// Date
if (obj instanceof Date) return new Date(obj) as T;
// RegExp
if (obj instanceof RegExp) return new RegExp(obj.source, obj.flags) as T;
// Map
if (obj instanceof Map) {
const clone = new Map();
visited.set(obj as object, clone);
obj.forEach((val, key) => clone.set(key, deepClone(val, visited)));
return clone as T;
}
// Set
if (obj instanceof Set) {
const clone = new Set();
visited.set(obj as object, clone);
obj.forEach((val) => clone.add(deepClone(val, visited)));
return clone as T;
}
// Array
if (Array.isArray(obj)) {
const clone: unknown[] = [];
visited.set(obj as object, clone);
for (let i = 0; i < obj.length; i++) {
clone[i] = deepClone(obj[i], visited);
}
return clone as T;
}
// 普通对象
const clone = Object.create(Object.getPrototypeOf(obj));
visited.set(obj as object, clone);
for (const key of Reflect.ownKeys(obj as object)) {
(clone as Record<PropertyKey, unknown>)[key] = deepClone(
(obj as Record<PropertyKey, unknown>)[key],
visited
);
}
return clone;
}
EventEmitter(发布订阅)
type Listener = (...args: unknown[]) => void;
class EventEmitter {
private events = new Map<string, Set<Listener>>();
on(event: string, listener: Listener): () => void {
if (!this.events.has(event)) {
this.events.set(event, new Set());
}
this.events.get(event)!.add(listener);
// 返回取消订阅函数
return () => this.off(event, listener);
}
off(event: string, listener: Listener): void {
this.events.get(event)?.delete(listener);
}
once(event: string, listener: Listener): () => void {
const wrapper: Listener = (...args) => {
listener(...args);
this.off(event, wrapper);
};
return this.on(event, wrapper);
}
emit(event: string, ...args: unknown[]): void {
const listeners = this.events.get(event);
if (listeners) {
// 复制一份防止遍历中修改
for (const listener of [...listeners]) {
listener(...args);
}
}
}
}
Promise.all / Promise.race 实现
function promiseAll<T>(promises: Promise<T>[]): Promise<T[]> {
return new Promise((resolve, reject) => {
const results: T[] = new Array(promises.length);
let count = 0;
if (promises.length === 0) {
resolve(results);
return;
}
promises.forEach((promise, index) => {
Promise.resolve(promise).then(
(value) => {
results[index] = value;
count++;
if (count === promises.length) resolve(results);
},
(reason) => reject(reason)
);
});
});
}
function promiseRace<T>(promises: Promise<T>[]): Promise<T> {
return new Promise((resolve, reject) => {
if (promises.length === 0) return; // 永远 pending
promises.forEach((promise) => {
Promise.resolve(promise).then(resolve, reject);
});
});
}
常见陷阱
| # | 陷阱 | 说明 | 正确做法 |
|---|---|---|---|
| 1 | 防抖节流混淆 | 防抖是”等最后一次”,节流是”固定间隔执行” | 明确需求:搜索联想用防抖,滚动加载用节流 |
| 2 | WeakMap 遍历 | WeakMap 不可遍历,无 forEach/keys() | 需遍历时用 Map,仅 GC 关联用 WeakMap |
| 3 | sort 缺少比较函数 | [10, 2, 1].sort() 结果为 [1, 10, 2](按字符串排序) | 始终传入比较函数:.sort((a, b) => a - b) |
| 4 | 深拷贝遗漏循环引用 | 递归深拷贝对象互相引用时栈溢出 | 使用 WeakMap 记录已访问对象 |
| 5 | 二分查找边界错误 | left <= right vs left < right 混用 | 明确搜索区间:闭区间用 <=,左闭右开用 < |
| 6 | 栈/队列用 shift 性能差 | Array.shift() 是 O(n) 操作 | 队列高频操作可用双指针或循环数组实现 |
| 7 | 递归爆栈 | 深层 DOM 树递归遍历可能栈溢出 | 改用迭代(栈模拟递归),或使用尾递归优化 |
| 8 | Fiber 误解为树 | Fiber 本质是链表结构,不是树 | 理解 child/sibling/return 的链表遍历模型 |
| 9 | 忽略 sort 稳定性 | 旧引擎 sort 不稳定,可能导致排序结果不一致 | ES2019+ 已保证稳定;兼容场景需手动稳定排序 |
| 10 | LRU 只用 Map 不够 | JS Map 的迭代顺序虽是插入序,但无法 O(1) 删除中间节点 | 面试需实现 HashMap + 双向链表 双结构 |
算法面试备考最佳实践
- 按专题刷题:先按数据结构分类(链表→栈→树→图),比随机刷效率高 3 倍
- 前端优先:先刷高频前端实现题(深拷贝、LRU、EventEmitter、Promise),再刷 LeetCode 经典题
- 限时训练:简单题 15 分钟,中等题 25 分钟,困难题可跳过
- 先思路后代码:面试中先口头描述思路,确认方向正确再编码
- 复杂度必答:写完代码主动分析时间/空间复杂度
- 边界用例:空输入、单元素、重复元素、极大/极小值——提交前先检查
- 手写能力:脱离 IDE 和自动补全,用白板/纸笔练习
- 复盘总结:每题做后记录套路模板,同类型题一通百通
推荐刷题顺序:数组/字符串 → 哈希表 → 链表 → 栈/队列 → 树 → 二分查找 → 排序 → 动态规划 → 图
面试题
1. 实现一个带过期时间的 LRU 缓存
考察点:哈希表 + 双向链表 + 过期策略
interface CacheNode {
key: number;
value: number;
expireAt: number;
prev: CacheNode | null;
next: CacheNode | null;
}
class LRUCacheWithExpiry {
private capacity: number;
private map = new Map<number, CacheNode>();
private head: CacheNode;
private tail: CacheNode;
constructor(capacity: number) {
this.capacity = capacity;
this.head = { key: 0, value: 0, expireAt: 0, prev: null, next: null };
this.tail = { key: 0, value: 0, expireAt: 0, prev: null, next: null };
this.head.next = this.tail;
this.tail.prev = this.head;
}
get(key: number): number {
if (!this.map.has(key)) return -1;
const node = this.map.get(key)!;
if (Date.now() > node.expireAt) {
this.removeNode(node);
this.map.delete(key);
return -1;
}
this.removeNode(node);
this.addToHead(node);
return node.value;
}
put(key: number, value: number, ttl: number = 60000): void {
const expireAt = Date.now() + ttl;
if (this.map.has(key)) {
const node = this.map.get(key)!;
node.value = value;
node.expireAt = expireAt;
this.removeNode(node);
this.addToHead(node);
} else {
const node: CacheNode = {
key, value, expireAt, prev: null, next: null,
};
this.map.set(key, node);
this.addToHead(node);
if (this.map.size > this.capacity) {
const removed = this.tail.prev!;
this.removeNode(removed);
this.map.delete(removed.key);
}
}
}
private removeNode(node: CacheNode): void {
node.prev!.next = node.next;
node.next!.prev = node.prev;
}
private addToHead(node: CacheNode): void {
node.next = this.head.next;
node.prev = this.head;
this.head.next!.prev = node;
this.head.next = node;
}
}
2. 实现并发请求限制
考察点:队列、Promise 控制、异步调度
async function concurrentLimit(
urls: string[],
maxConcurrency: number
): Promise<unknown[]> {
const results: unknown[] = new Array(urls.length);
let currentIndex = 0;
async function worker(): Promise<void> {
while (currentIndex < urls.length) {
const index = currentIndex++;
try {
const response = await fetch(urls[index]);
results[index] = await response.json();
} catch (error) {
results[index] = error;
}
}
}
const workers = Array.from(
{ length: Math.min(maxConcurrency, urls.length) },
() => worker()
);
await Promise.all(workers);
return results;
}
3. 实现带并发数限制的 Promise 调度器
考察点:任务队列、微任务调度、并发控制
class Scheduler {
private maxConcurrency: number;
private running = 0;
private queue: Array<() => Promise<unknown>> = [];
constructor(maxConcurrency: number) {
this.maxConcurrency = maxConcurrency;
}
add<T>(task: () => Promise<T>): Promise<T> {
return new Promise((resolve, reject) => {
const wrapped = async () => {
this.running++;
try {
const result = await task();
resolve(result);
} catch (error) {
reject(error);
} finally {
this.running--;
this.next();
}
};
if (this.running < this.maxConcurrency) {
wrapped();
} else {
this.queue.push(wrapped);
}
});
}
private next(): void {
if (this.queue.length > 0 && this.running < this.maxConcurrency) {
const task = this.queue.shift()!;
task();
}
}
}
// 使用示例
// const scheduler = new Scheduler(2);
// scheduler.add(() => fetch('/api/1'));
// scheduler.add(() => fetch('/api/2'));
// scheduler.add(() => fetch('/api/3')); // 等待前两个完成一个后再执行
4. 实现字符串模板引擎
考察点:正则、栈、字符串解析
function render(
template: string,
data: Record<string, unknown>
): string {
return template.replace(/\{\{(\w+(?:\.\w+)*)\}\}/g, (_, path: string) => {
const keys = path.split('.');
let result: unknown = data;
for (const key of keys) {
result = (result as Record<string, unknown>)?.[key];
if (result === undefined) return '';
}
return String(result);
});
}
// render('Hello, {{user.name}}!', { user: { name: 'World' } })
// => 'Hello, World!'
5. 实现虚拟列表核心算法
考察点:二分查找、DOM 性能优化
class VirtualList {
private positions: { top: number; height: number }[] = [];
private visibleCount: number;
private containerHeight: number;
constructor(
itemCount: number,
itemHeight: number,
containerHeight: number
) {
this.containerHeight = containerHeight;
this.visibleCount = Math.ceil(containerHeight / itemHeight) + 1;
// 预计算每项位置(等高场景可 O(1) 计算,不等高需存储)
for (let i = 0; i < itemCount; i++) {
this.positions.push({ top: i * itemHeight, height: itemHeight });
}
}
// 根据 scrollTop 二分查找起始索引
getVisibleRange(scrollTop: number): {
startIndex: number;
endIndex: number;
offsetY: number;
} {
let left = 0;
let right = this.positions.length - 1;
while (left <= right) {
const mid = (left + right) >> 1;
const { top, height } = this.positions[mid];
if (scrollTop >= top && scrollTop < top + height) {
const startIndex = mid;
const endIndex = Math.min(
startIndex + this.visibleCount,
this.positions.length - 1
);
return {
startIndex,
endIndex,
offsetY: this.positions[startIndex].top,
};
}
if (scrollTop < top) right = mid - 1;
else left = mid + 1;
}
return { startIndex: 0, endIndex: this.visibleCount - 1, offsetY: 0 };
}
}
6. 判断两个对象深相等
考察点:递归、类型判断、边界处理
function isDeepEqual(a: unknown, b: unknown): boolean {
// 同一引用
if (a === b) return true;
// null 或非对象
if (a === null || b === null || typeof a !== 'object' || typeof b !== 'object') {
return false;
}
// 类型不同
if (Object.prototype.toString.call(a) !== Object.prototype.toString.call(b)) {
return false;
}
// 数组
if (Array.isArray(a)) {
if (a.length !== (b as unknown[]).length) return false;
return a.every((val, i) => isDeepEqual(val, (b as unknown[])[i]));
}
// 普通对象
const keysA = Object.keys(a);
const keysB = Object.keys(b);
if (keysA.length !== keysB.length) return false;
return keysA.every(
(key) => Object.prototype.hasOwnProperty.call(b, key) &&
isDeepEqual(
(a as Record<string, unknown>)[key],
(b as Record<string, unknown>)[key]
)
);
}
7. 实现一个 JSON Path 查询器
考察点:递归、路径解析、树遍历
function queryByPath(obj: unknown, path: string): unknown {
const tokens = path
.replace(/\[(\d+)]/g, '.$1') // arr[0] → arr.0
.replace(/\['([^']+)']/g, '.$1') // obj['key'] → obj.key
.split('.')
.filter(Boolean);
let result: unknown = obj;
for (const token of tokens) {
if (result === null || result === undefined) return undefined;
result = (result as Record<string | number, unknown>)[token];
}
return result;
}
// queryByPath({ a: { b: [{ c: 42 }] } }, "a.b[0].c") => 42
8. 实现数组扁平化并去重排序
考察点:递归/迭代扁平化、Set 去重、排序
function flattenUniqueSort(arr: unknown[]): number[] {
// 扁平化
function flat(input: unknown[]): number[] {
const result: number[] = [];
for (const item of input) {
if (Array.isArray(item)) {
result.push(...flat(item));
} else if (typeof item === 'number') {
result.push(item);
}
}
return result;
}
// 去重 + 排序
return [...new Set(flat(arr))].sort((a, b) => a - b);
}
// flattenUniqueSort([1, [2, [3, 2, 1], 4], [5, 3]])
// => [1, 2, 3, 4, 5]
相关链接
- LeetCode 热题 100 — 高频算法题精选
- 代码随想录 — 按专题系统刷题指南
- React Fiber 架构 — Fiber 链表遍历原理
- Vue3 Diff 算法源码 — 最长递增子序列应用
- V8 Array.sort 实现 — TimSort 原理
- ES2019 Stable sort — 规范要求