前端开发面试题:算法编程题(27 题)

技术 · 2025-06-26
前端开发面试题:算法编程题(27 题)

编程题

P1:使用 Promise 实现红绿灯交替重复

function trafficLight(times: number, order: string[] = ['red', 'yellow', 'green']) {
  const delay = (ms: number) => new Promise(res => setTimeout(res, ms));
  const lights: Record<string, number> = { red: 3000, yellow: 1000, green: 2000 };

  return new Promise<void>(async resolve => {
    for (let i = 0; i < times; i++) {
      for (const color of order) {
        console.log(color);
        await delay(lights[color]);
      }
    }
    resolve();
  });
}

// 递归写法(更直观)
function loop(times: number) {
  const tick = (count: number): Promise<void> => {
    if (count >= times) return Promise.resolve();
    return new Promise(res => setTimeout(res, 1000))
      .then(() => { console.log('🔴'); return new Promise(r => setTimeout(r, 1000)); })
      .then(() => { console.log('🟡'); return new Promise(r => setTimeout(r, 1000)); })
      .then(() => { console.log('🟢'); })
      .then(() => tick(count + 1));
  };
  return tick(0);
}

关键点setTimeout 包成 Promise,用 async/await 或 Promise 链按顺序触发。


P2:bind、call、apply 有什么区别?如何手写 bind?

区别

方法参数形式调用时机返回值
call(obj, arg1, arg2, ...)逐个参数立即调用函数返回值
apply(obj, [args])数组立即调用函数返回值
bind(obj, arg1, ...)逐个参数返回新函数绑定 this 的新函数

手写 call

Function.prototype.myCall = function(thisArg: any, ...args: any[]) {
  const fn = Symbol('fn'); // 唯一 key 避免覆盖
  thisArg = thisArg ?? globalThis;
  thisArg[fn] = this;
  const result = thisArg[fn](...args);
  delete thisArg[fn];
  return result;
};

手写 apply

Function.prototype.myApply = function(thisArg: any, args: any[] = []) {
  const fn = Symbol('fn');
  thisArg = thisArg ?? globalThis;
  thisArg[fn] = this;
  const result = thisArg[fn](...args);
  delete thisArg[fn];
  return result;
};

手写 bind(考点,需考虑参数合并 + new 调用):

Function.prototype.myBind = function(thisArg: any, ...presetArgs: any[]) {
  const self = this;
  return function Fn(...laterArgs: any[]) {
    // 用 new 调用时 this 应指向新对象
    const isNew = this instanceof Fn;
    return self.apply(isNew ? this : thisArg, [...presetArgs, ...laterArgs]);
  };
};

P3:字符串压缩(利用字符重复次数)

function compress(str: string): string {
  if (!str) return '';
  let result = '';
  let count = 1;
  for (let i = 0; i < str.length; i++) {
    if (str[i] === str[i + 1]) {
      count++;
    } else {
      result += str[i] + count;
      count = 1;
    }
  }
  return result.length < str.length ? result : str;
}

示例'aabcccccaaa''a2b1c5a3'


P4:new 操作符具体干了什么?

四步执行

  1. 创建空对象 obj = {}
  2. 设置原型:obj.__proto__ = Constructor.prototype
  3. 执行构造函数(this 指向 obj),为 obj 添加属性;
  4. 判断返回值

    • 显式返回对象 → 返回该对象;
    • 其他情况 → 返回 obj

手写实现

function myNew(Constructor: Function, ...args: any[]) {
  const obj = Object.create(Constructor.prototype);
  const result = Constructor.apply(obj, args);
  return result instanceof Object ? result : obj;
}

P5:如何实现上拉加载、下拉刷新?

核心原理

// 上拉加载:监听 scroll,判断距离底部
function onScroll() {
  const { scrollTop, clientHeight, scrollHeight } = document.documentElement;
  if (scrollTop + clientHeight >= scrollHeight - 50) {
    loadMore(); // 触发加载
  }
}

// 下拉刷新:touch 事件计算下拉距离
let startY = 0, pulling = false;
el.addEventListener('touchstart', e => {
  if (window.scrollY === 0) {
    startY = e.touches[0].clientY;
    pulling = true;
  }
});
el.addEventListener('touchmove', e => {
  if (!pulling) return;
  const dy = e.touches[0].clientY - startY;
  if (dy > 0) {
    e.preventDefault(); // 阻止默认滚动
    el.style.transform = `translateY(${dy}px)`;
  }
});
el.addEventListener('touchend', e => {
  if (pulling) {
    pulling = false;
    el.style.transform = '';
    // 触发刷新
    refresh().finally(() => {});
  }
});

推荐方案:直接用第三方库(如 better-scrollvue-virtual-scroller、React 的 react-pull-to-refresh)。


P6:大文件怎么实现断点续传?

核心流程

  1. 文件分片:使用 Blob.slice() 将文件切成固定大小(5MB)的 chunks;
  2. 计算唯一标识spark-md5 计算文件 hash(避免全量上传);
  3. 秒传校验:上传前查询服务端是否已有该 hash 的文件,有则秒传;
  4. 分片上传:每个 chunk 携带 indexhash、文件 hash
  5. 断点续传:上传前查询服务端已有分片,跳过已上传部分;
  6. 合并请求:所有分片上传完成后通知服务端合并。

关键代码骨架

async function upload(file: File) {
  const CHUNK_SIZE = 5 * 1024 * 1024;
  const chunks = [];
  for (let i = 0; i < file.size; i += CHUNK_SIZE) {
    chunks.push(file.slice(i, i + CHUNK_SIZE));
  }
  const fileHash = await calcHash(chunks);
  const { uploaded = [] } = await api.checkFile(fileHash);

  const tasks = chunks
    .map((chunk, idx) => ({ idx, chunk }))
    .filter(({ idx }) => !uploaded.includes(idx))
    .map(({ idx, chunk }) =>
      api.uploadChunk(chunk, fileHash, idx).retry(3)
    );

  // 限制并发 6
  await pLimit(6, tasks);
  await api.mergeChunks(fileHash);
}

P7:防抖与节流的编码实现

防抖(debounce):触发后等待 wait ms,若再次触发则重置。

function debounce<T extends (...args: any[]) => any>(fn: T, wait: number) {
  let timer: any = null;
  return function (this: any, ...args: Parameters<T>) {
    clearTimeout(timer);
    timer = setTimeout(() => fn.apply(this, args), wait);
  };
}

节流(throttle):固定时间内只执行一次(首触发或尾触发)。

// 时间戳版(首触发立即执行)
function throttle<T extends (...args: any[]) => any>(fn: T, wait: number) {
  let last = 0;
  return function (this: any, ...args: Parameters<T>) {
    const now = Date.now();
    if (now - last >= wait) {
      last = now;
      fn.apply(this, args);
    }
  };
}

// 定时器版(尾触发)
function throttle2(fn: Function, wait: number) {
  let timer: any = null;
  return function (this: any, ...args: any[]) {
    if (!timer) {
      timer = setTimeout(() => {
        timer = null;
        fn.apply(this, args);
      }, wait);
    }
  };
}

使用场景:防抖 → 搜索输入、resize;节流 → 滚动、mousemove、点击。


P8:AJAX 的原理与实现

原理:通过 XMLHttpRequestFetch 异步与服务器交换数据,局部刷新页面。

手写 XHR

function ajax({ url, method = 'GET', data, headers = {} }: any) {
  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest();
    xhr.open(method, url, true);
    Object.entries(headers).forEach(([k, v]) => xhr.setRequestHeader(k, v as string));
    xhr.onload = () => {
      if (xhr.status >= 200 && xhr.status < 300) {
        resolve(JSON.parse(xhr.responseText));
      } else {
        reject(new Error(xhr.statusText));
      }
    };
    xhr.onerror = () => reject(new Error('Network Error'));
    xhr.send(data ? JSON.stringify(data) : null);
  });
}

Fetch 版本

async function fetchJSON(url: string, options: RequestInit = {}) {
  const res = await fetch(url, options);
  if (!res.ok) throw new Error(`${res.status} ${res.statusText}`);
  return res.json();
}

P9:深拷贝浅拷贝区别与实现

区别

  • 浅拷贝:只复制对象的第一层属性,引用类型仍共享同一地址(Object.assign{...obj}slice)。
  • 深拷贝:递归复制所有层级,新对象与原对象完全隔离。

深拷贝实现

function deepClone<T>(obj: T, map = new WeakMap()): T {
  if (obj === null || typeof obj !== 'object') return obj;
  if (obj instanceof Date) return new Date(obj) as any;
  if (obj instanceof RegExp) return new RegExp(obj.source, obj.flags) as any;
  if (map.has(obj as any)) return map.get(obj as any);

  const cloneObj: any = Array.isArray(obj) ? [] : {};
  map.set(obj as any, cloneObj);
  for (const key in obj) {
    if (Object.prototype.hasOwnProperty.call(obj, key)) {
      cloneObj[key] = deepClone((obj as any)[key], map);
    }
  }
  return cloneObj;
}

生产推荐structuredClone(obj)(原生)、lodash.cloneDeep(处理更多类型)。

注意:函数、Symbol、原型链、循环引用、特殊对象(Map/Set)需特别处理。


P10:JS 实现二叉树与基本操作

class TreeNode {
  val: number;
  left: TreeNode | null = null;
  right: TreeNode | null = null;
  constructor(val: number) { this.val = val; }
}

// 遍历
const preorder = (root: TreeNode | null): number[] => {
  if (!root) return [];
  return [root.val, ...preorder(root.left), ...preorder(root.right)];
};
const inorder = (root: TreeNode | null): number[] =>
  !root ? [] : [...inorder(root.left), root.val, ...inorder(root.right)];
const postorder = (root: TreeNode | null): number[] =>
  !root ? [] : [...postorder(root.left), ...postorder(root.right)];

// 层序(BFS)
const 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;
};

P11:实现轮播图组件

核心要点

<!-- Vue 版 -->
<template>
  <div class="carousel" @mouseenter="pause" @mouseleave="play">
    <div class="track" :style="{ transform: `translateX(-${index * 100}%)` }">
      <div v-for="(img, i) in images" :key="i" class="slide">
        <img :src="img" />
      </div>
    </div>
    <button @click="prev">‹</button>
    <button @click="next">›</button>
    <div class="dots">
      <span v-for="(_, i) in images" :key="i"
        :class="{ active: i === index }" @click="index = i" />
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount } from 'vue';
const props = defineProps<{ images: string[]; interval?: number }>();
const index = ref(0);
let timer: any = null;

const next = () => index.value = (index.value + 1) % props.images.length;
const prev = () => index.value = (index.value - 1 + props.images.length) % props.images.length;
const play = () => { timer = setInterval(next, props.interval ?? 3000); };
const pause = () => clearInterval(timer);

onMounted(play);
onBeforeUnmount(pause);
</script>

关键点

  • transform + transition 实现平滑滚动;
  • 自动播放 + 鼠标悬停暂停;
  • 无限循环(首尾克隆 + 索引修正);
  • 触摸滑动支持(移动端)。

P12:将数字转换为汉语输出

function trans(num: number): string {
  if (num === 0) return '零';
  const digits = '零一二三四五六七八九';
  const units = ['', '十', '百', '千'];
  const bigUnits = ['', '万', '亿', '万亿'];

  const sectionToCN = (n: number): string => {
    let result = '';
    let zeroFlag = false;
    let i = 0;
    while (n > 0) {
      const d = n % 10;
      if (d === 0) {
        if (!zeroFlag && result) zeroFlag = true;
      } else {
        if (zeroFlag) { result = '零' + result; zeroFlag = false; }
        result = digits[d] + units[i] + result;
      }
      n = Math.floor(n / 10);
      i++;
    }
    return result;
  };

  let result = '';
  let bigIdx = 0;
  let needZero = false;
  while (num > 0) {
    const section = num % 10000;
    if (section === 0) {
      if (result && !result.startsWith('零')) needZero = true;
    } else {
      const cn = sectionToCN(section);
      if (needZero) result = '零' + result;
      result = cn + bigUnits[bigIdx] + result;
      needZero = false;
    }
    num = Math.floor(num / 10000);
    bigIdx++;
  }
  return result;
}

P13:编写 Vue 组件,使用插槽接收外部内容

<!-- Card.vue -->
<template>
  <div class="card">
    <header v-if="$slots.header">
      <slot name="header" :title="title" />
    </header>
    <main>
      <slot>默认内容</slot>
    </main>
    <footer v-if="$slots.footer">
      <slot name="footer" />
    </footer>
  </div>
</template>

<script setup lang="ts">
defineProps<{ title?: string }>();
</script>

<!-- 使用 -->
<Card title="卡片">
  <template #header="{ title }">
    <h2>{{ title }}</h2>
  </template>
  <p>主体内容</p>
  <template #footer>
    <button>确定</button>
  </template>
</Card>

P14:去除字符串中出现次数最少的字符,不改变原顺序

function removeMinChars(str: string): string {
  const counts = new Map<string, number>();
  for (const ch of str) counts.set(ch, (counts.get(ch) || 0) + 1);
  const minCount = Math.min(...counts.values());
  return [...str].filter(ch => counts.get(ch)! > minCount).join('');
}

P18:树转数组(扁平化)

function treeToArray(tree: any[]): any[] {
  const result: any[] = [];
  const dfs = (nodes: any[]) => {
    nodes.forEach(node => {
      result.push({ id: node.id, name: node.name, pid: node.pid });
      if (node.children?.length) dfs(node.children);
    });
  };
  dfs(tree);
  return result;
}

P19:数组转树

function arrayToTree(arr: Array<{ id: number; pid: number; name: string }>): any[] {
  const map = new Map<number, any>();
  const roots: any[] = [];
  arr.forEach(item => map.set(item.id, { ...item, children: [] }));
  arr.forEach(item => {
    const node = map.get(item.id)!;
    if (item.pid === 0) {
      roots.push(node);
    } else {
      map.get(item.pid)?.children.push(node);
    }
  });
  return roots;
}

P20:删除链表的一个节点

已知要删除节点的指针(无头节点引用)

function deleteNode(node: ListNode): void {
  // 把下一个节点的值复制到当前节点,然后删除下一个节点
  node.val = node.next!.val;
  node.next = node.next!.next;
}

已知链表头 + 值

function deleteNodeByVal(head: ListNode | null, val: number): ListNode | null {
  if (!head) return null;
  if (head.val === val) return head.next;
  let cur = head;
  while (cur.next && cur.next.val !== val) cur = cur.next;
  if (cur.next) cur.next = cur.next.next;
  return head;
}

P21:实现请求并发控制(最多 N 个并发)

async function pLimit<T>(max: number, tasks: (() => Promise<T>)[]): Promise<T[]> {
  const results: T[] = [];
  let idx = 0;
  const workers = Array.from({ length: max }, async () => {
    while (idx < tasks.length) {
      const cur = idx++;
      results[cur] = await tasks[cur]();
    }
  });
  await Promise.all(workers);
  return results;
}

进阶:失败重试 + 优先级 → 推荐使用 p-limit + p-retry npm 包。


P22:实现 fetchWithRetry(带重试机制)

async function fetchWithRetry(
  url: string,
  options: RequestInit = {},
  retries = 3,
  delay = 1000
): Promise<Response> {
  try {
    const res = await fetch(url, options);
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    return res;
  } catch (err) {
    if (retries <= 0) throw err;
    await new Promise(r => setTimeout(r, delay));
    // 指数退避
    return fetchWithRetry(url, options, retries - 1, delay * 2);
  }
}

P23:链表环的入口节点

快慢指针法

function detectCycle(head: ListNode | null): ListNode | null {
  if (!head || !head.next) return null;
  let slow = head, fast = head;
  while (fast && fast.next) {
    slow = slow.next!;
    fast = fast.next.next!;
    if (slow === fast) break;
  }
  if (slow !== fast) return null;
  // 相遇后,让 slow 从头出发,再次相遇即入口
  slow = head;
  while (slow !== fast) {
    slow = slow.next!;
    fast = fast.next!;
  }
  return slow;
}

原理:快指针走 2k 步,慢指针走 k 步,相遇时让慢指针回到头,再次相遇即为环入口。


P24:多叉树指定层节点的个数

function countAtLevel(root: any, targetLevel: number, curLevel = 1): number {
  if (!root) return 0;
  if (curLevel === targetLevel) return 1;
  let count = 0;
  for (const child of (root.children || [])) {
    count += countAtLevel(child, targetLevel, curLevel + 1);
  }
  return count;
}

// BFS 版
function countAtLevelBFS(root: any, targetLevel: number): number {
  if (!root || targetLevel < 1) return 0;
  let level = 1, queue = [root];
  while (queue.length) {
    if (level === targetLevel) return queue.length;
    const next: any[] = [];
    for (const node of queue) next.push(...(node.children || []));
    queue = next;
    level++;
  }
  return 0;
}

P25:手写快速排序

function quickSort(arr: number[]): number[] {
  if (arr.length <= 1) return arr;
  const pivot = arr[0];
  const left = arr.slice(1).filter(x => x <= pivot);
  const right = arr.slice(1).filter(x => x > pivot);
  return [...quickSort(left), pivot, ...quickSort(right)];
}

// 原地版(性能更好)
function quickSortInPlace(arr: number[], lo = 0, hi = arr.length - 1): number[] {
  if (lo < hi) {
    const pivotIdx = partition(arr, lo, hi);
    quickSortInPlace(arr, lo, pivotIdx - 1);
    quickSortInPlace(arr, pivotIdx + 1, hi);
  }
  return arr;
}
function partition(arr: number[], lo: number, hi: number): number {
  const pivot = arr[hi];
  let i = lo - 1;
  for (let j = lo; j < hi; j++) {
    if (arr[j] <= pivot) {
      i++;
      [arr[i], arr[j]] = [arr[j], arr[i]];
    }
  }
  [arr[i + 1], arr[hi]] = [arr[hi], arr[i + 1]];
  return i + 1;
}

复杂度:平均 O(n log n),最坏 O(n²)。


P26:有序数组原地去重

function uniqueSorted(arr: number[]): number[] {
  if (arr.length <= 1) return arr;
  let slow = 0;
  for (let fast = 1; fast < arr.length; fast++) {
    if (arr[fast] !== arr[slow]) {
      slow++;
      arr[slow] = arr[fast];
    }
  }
  return arr.slice(0, slow + 1);
}

复杂度:O(n) 时间,O(1) 空间。


P27:计算数组中的平均时间

题目:给定形如 [{ start: '10:00', end: '12:00' }, ...] 的数组,计算所有时间段的平均时长(小时)。

function averageTime(intervals: Array<{ start: string; end: string }>): number {
  const toMinutes = (t: string) => {
    const [h, m] = t.split(':').map(Number);
    return h * 60 + m;
  };
  const total = intervals.reduce((sum, { start, end }) => {
    return sum + (toMinutes(end) - toMinutes(start));
  }, 0);
  return total / intervals.length / 60; // 转小时
}

本文作者:小码哥

本文链接:https://wesee.club/archives/1188/

版权声明:自由转载-非商用-非衍生-保持署名(cc 创意共享 3.0 许可证

Theme Jasmine by Kent Liao

粤ICP备2023052298号-1