文章 NEW

Developer notes / Archive

前端与 AI 应用开发高频手撕题:30 题可背诵答案

前端 20 题与 AI 应用开发 10 题,覆盖异步、原型、缓存、RAG、Tool Calling、Agent 和 SSE,并提供可运行参考实现。

2026/08/08 35 min read 0 浏览
#前端面试#TypeScript#Python#AI 应用开发#RAG
猫耳看板娘在白板前冲刺复习前端与 AI 高频题
目录 / On this page

这份清单按“能在白板或编辑器里写出来”为标准:先说清输入、输出和边界,再写核心循环。不要背 API 名称,重点是解释为什么这样处理异步、原型链和失败场景。

第一部分:前端高频手撕题(TypeScript)

1. 防抖 debounce

题意:连续调用结束后只执行最后一次,常用于搜索框。追问:如何支持立即执行与取消?

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

复杂度为 O(1)。背诵:每次调用清旧定时器,只让最后一次落地。

2. 节流 throttle

function throttle<T extends (...args: any[]) => void>(fn: T, wait: number) {
  let last = 0;
  return function (this: ThisParameterType<T>, ...args: Parameters<T>) {
    const now = Date.now();
    if (now - last >= wait) {
      last = now;
      fn.apply(this, args);
    }
  };
}

滚动事件常用。追问尾调用时,用定时器保存最后一组参数。背诵:限制的是执行频率,不是触发频率。

3. 深拷贝 deepClone

function deepClone<T>(value: T, seen = new WeakMap<object, unknown>()): T {
  if (value === null || typeof value !== "object") return value;
  if (value instanceof Date) return new Date(value) as T;
  if (value instanceof RegExp) return new RegExp(value) as T;
  if (seen.has(value as object)) return seen.get(value as object) as T;
  const result: any = Array.isArray(value)
    ? []
    : Object.create(Object.getPrototypeOf(value));
  seen.set(value as object, result);
  for (const key of Reflect.ownKeys(value as object))
    result[key] = deepClone((value as any)[key], seen);
  return result;
}

WeakMap 解决循环引用;函数、DOM 与带私有槽的内建对象需要按业务约定处理。

4. 数组扁平化 flat

function flat<T>(items: unknown[], depth = Infinity): T[] {
  const result: T[] = [];
  for (const item of items)
    Array.isArray(item) && depth > 0
      ? result.push(...flat<T>(item, depth - 1))
      : result.push(item as T);
  return result;
}

时间 O(n),递归深度很大时可改为显式栈。

5. 数组去重

const unique = <T>(items: T[]) => [...new Set(items)];
const uniqueBy = <T>(items: T[], key: (item: T) => unknown) => [
  ...new Map(items.map((item) => [key(item), item])).values(),
];

追问对象去重必须定义“相同”的 key;Set 不能按对象内容比较。

6. 手写 Promise(核心状态机)

class TinyPromise<T> {
  private state: "pending" | "fulfilled" | "rejected" = "pending";
  private value!: T;
  private reason: unknown;
  private ok: Array<(v: T) => void> = [];
  private bad: Array<(e: unknown) => void> = [];
  constructor(
    executor: (resolve: (v: T) => void, reject: (e: unknown) => void) => void,
  ) {
    const resolve = (v: T) => {
      if (this.state !== "pending") return;
      this.state = "fulfilled";
      this.value = v;
      this.ok.forEach((f) => f(v));
    };
    const reject = (e: unknown) => {
      if (this.state !== "pending") return;
      this.state = "rejected";
      this.reason = e;
      this.bad.forEach((f) => f(e));
    };
    try {
      executor(resolve, reject);
    } catch (e) {
      reject(e);
    }
  }
  then(onOk: (v: T) => void, onBad?: (e: unknown) => void) {
    this.state === "fulfilled"
      ? onOk(this.value)
      : this.state === "rejected"
        ? onBad?.(this.reason)
        : (this.ok.push(onOk), onBad && this.bad.push(onBad));
  }
}

完整 Promise/A+ 还要处理微任务、thenable 和链式返回值。背诵:状态只能从 pending 迁移一次。

7. Promise.all

function promiseAll<T>(items: Iterable<T | PromiseLike<T>>) {
  const list = [...items];
  return new Promise<T[]>((resolve, reject) => {
    if (!list.length) return resolve([]);
    const out: T[] = [];
    let done = 0;
    list.forEach((item, i) =>
      Promise.resolve(item).then((v) => {
        out[i] = v;
        if (++done === list.length) resolve(out);
      }, reject),
    );
  });
}

保持输入顺序;任一失败立即 reject。

8. Promise.race

function promiseRace<T>(items: Iterable<T | PromiseLike<T>>) {
  return new Promise<T>((resolve, reject) => {
    for (const item of items) Promise.resolve(item).then(resolve, reject);
  });
}

空数组永不 settle;常和超时 Promise 组合。

9. Promise.allSettled

function allSettled<T>(items: Iterable<T | PromiseLike<T>>) {
  return Promise.all(
    [...items].map((item) =>
      Promise.resolve(item).then(
        (value) => ({ status: "fulfilled" as const, value }),
        (reason) => ({ status: "rejected" as const, reason }),
      ),
    ),
  );
}

适合批处理:记录全部结果而不是短路失败。

10. instanceof

function myInstanceof(value: unknown, Ctor: Function) {
  if (
    value == null ||
    (typeof value !== "object" && typeof value !== "function")
  )
    return false;
  let proto = Object.getPrototypeOf(value);
  while (proto) {
    if (proto === Ctor.prototype) return true;
    proto = Object.getPrototypeOf(proto);
  }
  return false;
}

跨 iframe 可能失效,因为构造函数原型不是同一个对象。

11. new

function myNew<T>(Ctor: new (...args: any[]) => T, ...args: any[]): T {
  const instance = Object.create(Ctor.prototype);
  const returned = (Ctor as any).apply(instance, args);
  return returned &&
    (typeof returned === "object" || typeof returned === "function")
    ? returned
    : instance;
}

顺序:建原型对象、绑定 this、执行构造器、处理显式对象返回。

12–14. call / apply / bind

function myCall(fn: Function, context: object, ...args: unknown[]) {
  const key = Symbol();
  (context as any)[key] = fn;
  const result = (context as any)[key](...args);
  delete (context as any)[key];
  return result;
}
function myApply(fn: Function, context: object, args: unknown[] = []) {
  return myCall(fn, context, ...args);
}
function myBind(fn: Function, context: object, ...first: unknown[]) {
  return (...rest: unknown[]) => myCall(fn, context, ...first, ...rest);
}

bind 用作构造器时要让 this 指向新实例,这是高频进阶追问。

15. Object.create

function myCreate(proto: object | null) {
  function F() {}
  F.prototype = proto;
  return new (F as any)();
}

本质是创建一个原型指向 proto 的新对象。

16. EventEmitter / 发布订阅

class EventEmitter {
  private events = new Map<string, Set<(...args: any[]) => void>>();
  on(name: string, fn: (...args: any[]) => void) {
    (this.events.get(name) ?? this.events.set(name, new Set()).get(name)!).add(
      fn,
    );
    return () => this.off(name, fn);
  }
  off(name: string, fn: Function) {
    this.events.get(name)?.delete(fn as any);
  }
  emit(name: string, ...args: any[]) {
    this.events.get(name)?.forEach((fn) => fn(...args));
  }
  once(name: string, fn: (...args: any[]) => void) {
    const off = this.on(name, (...args) => {
      off();
      fn(...args);
    });
  }
}

追问:监听器遍历前复制集合,避免 emit 中移除监听器带来的迭代问题。

17. sleep

const sleep = (ms: number, signal?: AbortSignal) =>
  new Promise<void>((resolve, reject) => {
    const id = setTimeout(resolve, ms);
    signal?.addEventListener(
      "abort",
      () => {
        clearTimeout(id);
        reject(signal.reason);
      },
      { once: true },
    );
  });

可取消的 sleep 才能安全用于重试和轮询。

18. 并发请求控制

async function pool<T>(tasks: Array<() => Promise<T>>, limit = 3) {
  const out: T[] = [];
  let next = 0;
  async function worker() {
    while (next < tasks.length) {
      const i = next++;
      out[i] = await tasks[i]();
    }
  }
  await Promise.all(
    Array.from({ length: Math.min(limit, tasks.length) }, worker),
  );
  return out;
}

用索引预留位置来保持结果顺序;失败策略应明确为 fail-fast 或 all-settled。

19. LRU Cache(TypeScript / Python)

// 手撕版:Map 负责 O(1) 定位,双向链表负责 O(1) 调整访问顺序。
class Node<K, V> {
  prev: Node<K, V> | null = null;
  next: Node<K, V> | null = null;

  constructor(
    public key: K,
    public value: V,
  ) {}
}

class LRUCache<K, V> {
  private readonly cache = new Map<K, Node<K, V>>();
  // 哨兵节点避免在头尾插入、删除时写大量空指针判断。
  private readonly head = new Node<K, V>(undefined as K, undefined as V);
  private readonly tail = new Node<K, V>(undefined as K, undefined as V);

  constructor(private readonly capacity: number) {
    if (!Number.isInteger(capacity) || capacity <= 0) {
      throw new Error("capacity must be a positive integer");
    }
    this.head.next = this.tail;
    this.tail.prev = this.head;
  }

  get(key: K): V | undefined {
    const node = this.cache.get(key);
    if (!node) return undefined;
    this.moveToFront(node); // 命中后成为最新使用。
    return node.value;
  }

  put(key: K, value: V): void {
    const existing = this.cache.get(key);
    if (existing) {
      existing.value = value;
      this.moveToFront(existing);
      return;
    }

    const node = new Node(key, value);
    this.cache.set(key, node);
    this.addToFront(node);

    if (this.cache.size > this.capacity) {
      const leastRecent = this.removeLeastRecent();
      this.cache.delete(leastRecent.key);
    }
  }

  private addToFront(node: Node<K, V>): void {
    node.next = this.head.next;
    node.prev = this.head;
    this.head.next!.prev = node;
    this.head.next = node;
  }

  private remove(node: Node<K, V>): void {
    node.prev!.next = node.next;
    node.next!.prev = node.prev;
  }

  private moveToFront(node: Node<K, V>): void {
    this.remove(node);
    this.addToFront(node);
  }

  private removeLeastRecent(): Node<K, V> {
    const node = this.tail.prev!;
    this.remove(node);
    return node;
  }
}

这是完整手撕版:哈希表负责按 key 定位节点,双向链表从头到尾表示“最近使用 → 最久未使用”。get、更新、插入和淘汰均为 O(1)。背诵:读取也算访问,所以 get 后要移到头部;容量超限则删除尾部节点。

20. 函数柯里化 curry

function curry(fn: Function, ...saved: unknown[]): any {
  return (...args: unknown[]) => {
    const all = [...saved, ...args];
    return all.length >= fn.length ? fn(...all) : curry(fn, ...all);
  };
}

依赖 fn.length,默认参数或剩余参数会使“参数数量”不可靠。

第二部分:AI 应用开发手撕题

这部分刻意不调用具体 SDK:接口以 embedllmdecide 等函数注入,方便把同一逻辑接到任意模型服务。

1. 文本切块 Chunk + Overlap

// 下一块从 size - overlap 处开始,保留上下文。
export function chunkText(text: string, size = 500, overlap = 80) {
  if (size <= overlap) throw new Error("size must exceed overlap");
  const result: string[] = [];
  for (let start = 0; start < text.length; start += size - overlap) {
    result.push(text.slice(start, start + size));
  }
  return result.filter(Boolean);
}

复杂度 O(n)。size 必须大于 overlap,否则步长为零或负数。追问:生产环境应优先按段落/句子切分,并保留文档来源和 chunk 索引。

2. 余弦相似度 Cosine Similarity

// 点积除以两个向量的模长,结果范围通常为 [-1, 1]。
export function cosine(a: number[], b: number[]) {
  if (a.length !== b.length || !a.length) throw new Error("invalid vectors");
  let dot = 0, aa = 0, bb = 0;
  for (let i = 0; i < a.length; i++) { dot += a[i] * b[i]; aa += a[i] ** 2; bb += b[i] ** 2; }
  return aa && bb ? dot / Math.sqrt(aa * bb) : 0;
}

复杂度 O(d)。零向量不能归一化,返回 0 或显式报错都可以,但必须统一约定。

3. Top-K 向量检索

type Item = { id: string; vector: number[]; text: string };
// 先打分再排序;大规模数据应交给向量索引。
export function topK(query: number[], items: Item[], k = 3) {
  return items.map(item => ({ item, score: cosine(query, item.vector) }))
    .sort((a, b) => b.score - a.score).slice(0, k);
}

朴素实现是 O(n log n);规模大时用最小堆降为 O(n log k),再由向量索引库承担近似检索。

4. 最简 RAG

// embed 与 llm 由调用方注入,避免绑定具体模型 SDK。
export async function answerWithRag(question: string, docs: Item[], embed: (s: string) => Promise<number[]>, llm: (p: string) => Promise<string>) {
  const hits = topK(await embed(question), docs, 3);
  const context = hits.map(({ item }) => item.text).join("\n---\n");
  return llm("只根据资料回答;资料不足时说明不知道。\n资料:\n" + context + "\n问题:" + question);
}

关键是将召回文本标为不可信资料,并要求资料不足时拒绝编造;不要把用户问题直接拼成可执行指令。

5. Tool Registry + Tool Calling

type Tool = { description: string; run: (input: unknown) => Promise<unknown> };
// 白名单注册,阻止模型调用未授权函数。
const tools: Record<string, Tool> = {};
export function register(name: string, tool: Tool) { tools[name] = tool; }
export async function callTool(name: string, input: unknown) {
  if (!tools[name]) throw new Error("tool not allowed");
  return tools[name].run(input);
}

生产代码还必须校验输入 schema、限制可调用工具,并对有副作用动作增加用户确认。

6. Agent Loop

// 最大轮数避免模型反复调用工具造成死循环。
export async function runAgent(message: string, decide: (m: string) => Promise<{ type: "final"; text: string } | { type: "tool"; name: string; input: unknown }>) {
  let state = message;
  for (let turn = 0; turn < 6; turn++) { const next = await decide(state); if (next.type === "final") return next.text; state += "\n工具结果:" + JSON.stringify(await callTool(next.name, next.input)); }
  throw new Error("agent exceeded max turns");
}

最大轮次是安全边界;工具结果要回灌状态,但不能让它覆盖系统规则。

7. LLM 并发控制

// 多个 worker 共享 cursor,因此同时最多执行 limit 个任务。
export async function withLimit<T>(jobs: Array<() => Promise<T>>, limit = 3) {
  const result: T[] = []; let cursor = 0;
  async function worker() { while (cursor < jobs.length) { const index = cursor++; result[index] = await jobs[index](); } }
  await Promise.all(Array.from({ length: Math.min(limit, jobs.length) }, worker)); return result;
}

并发限制保护模型配额和本地资源;队列满时还应定义超时或背压策略。

8. Retry + 指数退避

// 等待时间随失败次数翻倍;生产中还应加入随机抖动。
export async function retry<T>(fn: () => Promise<T>, retries = 3, baseMs = 300): Promise<T> {
  let error: unknown;
  for (let i = 0; i <= retries; i++) try { return await fn(); } catch (e) { error = e; if (i < retries) await new Promise(r => setTimeout(r, baseMs * 2 ** i)); }
  throw error;
}

只重试临时错误(如 429、网络超时),并在生产中加入随机抖动,避免客户端同时重试。

9. 对话上下文截断

type Message = { role: string; content: string };
// 永远先保留 system,再从最新消息向前填充预算。
export function trimContext(messages: Message[], maxChars = 8000) {
  const system = messages.filter(m => m.role === "system");
  const rest = messages.filter(m => m.role !== "system"); let used = system.reduce((n, m) => n + m.content.length, 0); const keep: Message[] = [];
  for (let i = rest.length - 1; i >= 0; i--) { if (used + rest[i].content.length > maxChars) break; keep.unshift(rest[i]); used += rest[i].content.length; }
  return [...system, ...keep];
}

系统提示应优先保留,随后从最新消息向前填充预算。真实系统应使用 tokenizer 而不是字符数,并将被截断历史压缩为摘要。

10. SSE 流式输出处理

// 网络 chunk 可能截断事件,buffer 必须跨读取保留。
export async function* readSse(response: Response) {
  const reader = response.body!.pipeThrough(new TextDecoderStream()).getReader(); let buffer = "";
  while (true) { const { value, done } = await reader.read(); if (done) break; buffer += value; const events = buffer.split("\n\n"); buffer = events.pop()!;
    for (const event of events) { const data = event.split("\n").find(line => line.startsWith("data:"))?.slice(5).trim(); if (data && data !== "[DONE]") yield JSON.parse(data); }
  }
}

网络 chunk 不等于 SSE 事件,必须先维护 buffer 再按空行切分。断连、非法 JSON 和 [DONE] 都是必答边界。

面试时的答题顺序

  1. 先复述输入、输出与边界;
  2. 讲数据结构或状态机,再写最小可运行版本;
  3. 主动说明时间/空间复杂度;
  4. 最后补一个工程化追问:取消、错误处理、限流、可观测性或安全边界。

高频题不是比谁代码写得长。面试官更在意:你是否能让异步结果有序、让状态可终止、让外部输入不越权。

Now Playing

Every Day Is NightGaroad

0:00 / 0:00

播放列表 · 4 首