Developer notes / Archive
前端与 AI 应用开发高频手撕题:30 题可背诵答案
前端 20 题与 AI 应用开发 10 题,覆盖异步、原型、缓存、RAG、Tool Calling、Agent 和 SSE,并提供可运行参考实现。
目录 / 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;
}
}# 手撕版:dict 负责 O(1) 定位,双向链表负责 O(1) 调整访问顺序。
class Node:
def __init__(self, key=None, value=None):
self.key = key
self.value = value
self.prev = None
self.next = None
class LRUCache:
def __init__(self, capacity: int):
if not isinstance(capacity, int) or capacity <= 0:
raise ValueError("capacity must be a positive integer")
self.capacity = capacity
self.cache = {}
# 哨兵节点让头尾插入和删除不需要判空。
self.head = Node()
self.tail = Node()
self.head.next = self.tail
self.tail.prev = self.head
def get(self, key):
node = self.cache.get(key)
if node is None:
return None
self._move_to_front(node) # 命中后成为最新使用。
return node.value
def put(self, key, value):
node = self.cache.get(key)
if node is not None:
node.value = value
self._move_to_front(node)
return
node = Node(key, value)
self.cache[key] = node
self._add_to_front(node)
if len(self.cache) > self.capacity:
least_recent = self._remove_least_recent()
del self.cache[least_recent.key]
def _add_to_front(self, node):
node.prev = self.head
node.next = self.head.next
self.head.next.prev = node
self.head.next = node
def _remove(self, node):
node.prev.next = node.next
node.next.prev = node.prev
def _move_to_front(self, node):
self._remove(node)
self._add_to_front(node)
def _remove_least_recent(self):
node = self.tail.prev
self._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:接口以 embed、llm 和 decide 等函数注入,方便把同一逻辑接到任意模型服务。
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);
}# step 必须为正数,否则切块不会前进。
def chunk_text(text: str, size: int = 500, overlap: int = 80) -> list[str]:
if size <= overlap: raise ValueError("size must exceed overlap")
step = size - overlap
return [text[i:i + size] for i in range(0, len(text), step) if text[i:i + size]]复杂度 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;
}import math
# zip 同步遍历两个同维向量。
def cosine(a: list[float], b: list[float]) -> float:
if len(a) != len(b) or not a: raise ValueError("invalid vectors")
dot = sum(x * y for x, y in zip(a, b))
norm = math.sqrt(sum(x*x for x in a) * sum(y*y for y in b))
return dot / norm if norm else 0.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);
}# 返回分数最高的 k 条,而不是只返回原始文档。
def top_k(query, items, k=3):
ranked = [{"item": item, "score": cosine(query, item["vector"])} for item in items]
return sorted(ranked, key=lambda row: row["score"], reverse=True)[: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);
}# 先检索再组织上下文,限制模型只基于资料回答。
async def answer_with_rag(question, docs, embed, llm):
hits = top_k(await embed(question), docs, 3)
context = "\n---\n".join(row["item"]["text"] for row in hits)
prompt = f"只根据资料回答;资料不足时说明不知道。\n资料:\n{context}\n问题:{question}"
return await llm(prompt)关键是将召回文本标为不可信资料,并要求资料不足时拒绝编造;不要把用户问题直接拼成可执行指令。
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);
}TOOLS = {}
# 仅暴露显式登记过的工具。
def register(name, description, fn): TOOLS[name] = {"description": description, "run": fn}
async def call_tool(name, input):
if name not in TOOLS: raise ValueError("tool not allowed")
return await 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");
}# 将工具结果写回状态,供下一轮决策参考。
async def run_agent(message, decide):
state = message
for _ in range(6):
next_step = await decide(state)
if next_step["type"] == "final": return next_step["text"]
result = await call_tool(next_step["name"], next_step["input"])
state += "\n工具结果:" + repr(result)
raise RuntimeError("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;
}import asyncio
# Semaphore 在 await 期间限制正在执行的任务数。
async def with_limit(jobs, limit=3):
semaphore = asyncio.Semaphore(limit)
async def run(job):
async with semaphore: return await job()
return await asyncio.gather(*(run(job) for job in jobs))并发限制保护模型配额和本地资源;队列满时还应定义超时或背压策略。
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;
}import asyncio
# 只有临时错误才应该使用该重试器。
async def retry(fn, retries=3, base_ms=300):
for i in range(retries + 1):
try: return await fn()
except Exception:
if i == retries: raise
await asyncio.sleep(base_ms * 2**i / 1000)只重试临时错误(如 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。
def trim_context(messages, max_chars=8000):
system = [m for m in messages if m["role"] == "system"]
used, keep = sum(len(m["content"]) for m in system), []
for message in reversed([m for m in messages if m["role"] != "system"]):
if used + len(message["content"]) > max_chars: break
keep.insert(0, message); used += len(message["content"])
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); }
}
}import json
# 只有遇到空行才得到一个完整 SSE 事件。
async def read_sse(response):
buffer = ""
async for chunk in response.aiter_text():
buffer += chunk
*events, buffer = buffer.split("\n\n")
for event in events:
line = next(
(value for value in event.splitlines()
if value.startswith("data:")),
"",
)
data = line[5:].strip()
if data and data != "[DONE]":
yield json.loads(data)网络 chunk 不等于 SSE 事件,必须先维护 buffer 再按空行切分。断连、非法 JSON 和 [DONE] 都是必答边界。
面试时的答题顺序
- 先复述输入、输出与边界;
- 讲数据结构或状态机,再写最小可运行版本;
- 主动说明时间/空间复杂度;
- 最后补一个工程化追问:取消、错误处理、限流、可观测性或安全边界。
高频题不是比谁代码写得长。面试官更在意:你是否能让异步结果有序、让状态可终止、让外部输入不越权。
♪
0:00 / 0:00