webDevInterviewQuestions/algorithm/04-practical-scenarios.md
2026-01-19 10:21:18 +08:00

8.0 KiB
Raw Permalink Blame History

实际场景与逻辑题

1. 并发控制(限制并发请求数)

场景批量上传文件如100张图片每次最多5个并发、网络爬虫控制请求频率、避免压垂服务器。 解决:大量异步任务同时发起导致浏览器卡顿或服务端拒绝服务。

async function limitConcurrency(tasks, limit) {
  const results = [];      // 存储所有任务结果
  const executing = [];    // 当前正在执行的任务
  
  for (const [index, task] of tasks.entries()) {
    // 包装任务为 Promise
    const p = Promise.resolve().then(() => task()).then(res => {
      results[index] = res;  // 存储结果,保证顺序
      executing.splice(executing.indexOf(p), 1);  // 完成后从执行列表移除
    });
    executing.push(p);
    
    // 如果达到并发上限,等待任一任务完成
    if (executing.length >= limit) {
      await Promise.race(executing);
    }
  }
  
  await Promise.all(executing);  // 等待剩余任务完成
  return results;
}

// 使用示例:批量请求,最多 3 个并发
const tasks = urls.map(url => () => fetch(url));
await limitConcurrency(tasks, 3);

异步任务调度器

class Scheduler {
  constructor(limit) {
    this.limit = limit;    // 最大并发数
    this.queue = [];       // 等待队列
    this.running = 0;      // 当前运行中的任务数
  }
  
  // 添加任务,返回 Promise
  add(promiseCreator) {
    return new Promise((resolve, reject) => {
      // 将任务和对应的 resolve/reject 存入队列
      this.queue.push({ promiseCreator, resolve, reject });
      this.run();  // 尝试执行
    });
  }
  
  // 执行队列中的任务
  run() {
    // 未达上限且队列有任务时,取出执行
    while (this.running < this.limit && this.queue.length) {
      const { promiseCreator, resolve, reject } = this.queue.shift();
      this.running++;
      promiseCreator()
        .then(resolve, reject)
        .finally(() => { this.running--; this.run(); });  // 完成后继续执行下一个
    }
  }
}

2. 缓存函数Memoization

场景计算密集型函数结果缓存如斐波那契、API 请求缓存、React useMemo 原理。 解决:相同输入重复计算浪费性能,用空间换时间。

// 基础版:永久缓存
function memoize(fn) {
  const cache = new Map();  // 缓存存储
  return function(...args) {
    const key = JSON.stringify(args);  // 参数序列化作为 key
    if (cache.has(key)) return cache.get(key);  // 命中缓存
    const result = fn.apply(this, args);  // 执行原函数
    cache.set(key, result);  // 存入缓存
    return result;
  };
}

// 进阶版:支持过期时间 TTL
function memoizeWithTTL(fn, ttl = 60000) {
  const cache = new Map();
  return function(...args) {
    const key = JSON.stringify(args);
    const cached = cache.get(key);
    // 检查缓存是否存在且未过期
    if (cached && Date.now() - cached.time < ttl) {
      return cached.value;
    }
    const result = fn.apply(this, args);
    cache.set(key, { value: result, time: Date.now() });  // 存储值和时间戳
    return result;
  };
}

3. 解析 URL 参数

场景从链接提取分享参数、跟踪渠道来源utm_source、路由参数解析。 解决:将 URL 查询字符串转换为结构化对象,方便业务使用。

// 方法1字符串分割法
function parseQuery(url) {
  const query = url.split('?')[1] || '';  // 取 ? 后的部分
  return query.split('&').reduce((acc, pair) => {
    const [key, value] = pair.split('=').map(decodeURIComponent);  // 解码
    if (key) {
      // 处理重复 key转为数组
      acc[key] = acc[key] 
        ? [].concat(acc[key], value) 
        : value;
    }
    return acc;
  }, {});
}

// 方法2正则表达式
function parseQueryRegex(url) {
  const result = {};
  // 匹配 ?key=value 或 &key=value
  url.replace(/[?&]([^=&#]+)=([^&#]*)/g, (_, key, value) => {
    result[decodeURIComponent(key)] = decodeURIComponent(value);
  });
  return result;
}

// 示例: "https://example.com?a=1&b=2&a=3"
// => { a: ['1', '3'], b: '2' }  // a 有两个值

4. DOM 查找最近公共祖先

场景:事件委托的目标元素判断、富文本编辑器选区处理、拖拽边界计算。 解决:在 DOM 树中找到两个节点的最近共同父级。

function findCommonAncestor(node1, node2) {
  const ancestors = new Set();  // 存储 node1 的所有祖先
  
  // 第一步:收集 node1 的所有祖先节点
  let current = node1;
  while (current) {
    ancestors.add(current);
    current = current.parentNode;
  }
  
  // 第二步:遍历 node2 的祖先,找第一个在 ancestors 中的
  current = node2;
  while (current) {
    if (ancestors.has(current)) return current;  // 找到公共祖先
    current = current.parentNode;
  }
  
  return null;  // 无公共祖先
}

// 原生方法(现代浏览器)
// node1.compareDocumentPosition(node2)

5. 懒加载/无限滚动

场景:电商商品列表、社交信息流、图片画廊、新闻列表。 解决:首屏加载慢、一次性加载大量数据卡顿的问题。

// 使用 IntersectionObserver 实现懒加载
function lazyLoad(selector) {
  const observer = new IntersectionObserver((entries) => {
    entries.forEach(entry => {
      if (entry.isIntersecting) {  // 元素进入可视区域
        const img = entry.target;
        img.src = img.dataset.src;  // 将 data-src 赋值给 src
        observer.unobserve(img);    // 停止观察该元素
      }
    });
  });
  
  // 观察所有带指定选择器的图片
  document.querySelectorAll(selector).forEach(img => observer.observe(img));
}

// 无限滚动:滚动到底部时加载更多
function infiniteScroll(container, loadMore) {
  const observer = new IntersectionObserver(([entry]) => {
    if (entry.isIntersecting) loadMore();  // 触底则加载更多
  });
  
  // 创建底部哨兵元素
  const sentinel = document.createElement('div');
  container.appendChild(sentinel);
  observer.observe(sentinel);  // 观察哨兵元素
}

6. LRU 缓存

场景浏览器缓存淘汰、图片缓存池、Redis 内存管理、keep-alive 组件缓存。 解决:内存有限时如何淘汰最久未使用的数据,保留热点数据。

// LRU (Least Recently Used) 最近最少使用缓存
// 利用 Map 的有序性:插入顺序即为访问顺序
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);  // 已存在则先删除
    this.cache.set(key, value);  // 重新插入到最后
    if (this.cache.size > this.capacity) {
      // 超出容量删除最早插入的Map 的第一个 key
      this.cache.delete(this.cache.keys().next().value);
    }
  }
}

7. 大数相加

场景:订单号/交易号处理、金融精确计算、超出 JS Number 范围的计算。 解决JS 数字最大安全整数 2^53-1 限制,大数运算精度丢失问题。

// 模拟竖式加法,从个位开始相加
function addBigNumbers(a, b) {
  const maxLen = Math.max(a.length, b.length);
  // 对齐长度,前面补 0
  a = a.padStart(maxLen, '0');
  b = b.padStart(maxLen, '0');
  
  let carry = 0, result = '';  // carry 进位
  for (let i = maxLen - 1; i >= 0; i--) {
    const sum = +a[i] + +b[i] + carry;  // 当前位相加
    result = (sum % 10) + result;       // 取个位数
    carry = Math.floor(sum / 10);       // 计算进位
  }
  
  return carry ? carry + result : result;  // 最高位有进位则补上
}

// 示例: addBigNumbers("12345678901234567890", "98765432109876543210")