高频手写精选
实现 Promise.all
Promise.all:将可迭代对象转成数组,空数组立即兑现[];逐个Promise.resolve,按原下标保存结果,计数归零后兑现;任一拒绝立即拒绝,其他任务不会自动取消。JSON2DOM = react的render函数
JSON2DOM:递归创建元素;文本节点用
createTextNode,元素用createElement,属性需区分className、事件和普通属性,再递归追加 children。React 的render还包含协调、状态与更新,不能等同于简单 JSON2DOM。树形结构转成列表
树转列表:深度优先遍历,把节点的
children拆掉,输出{...node, parentId};记录访问过的节点以防循环引用,保留顺序。列表转成树形结构
列表转树:先用
Map(id → 节点副本)建索引,再按parentId挂到父节点children;根节点单独收集。时间 O(n),需约定孤儿节点和重复 id 的处理。Array.prototype.flat
flat(depth):递归或显式栈遍历;遇数组且剩余深度大于 0 则展开,否则加入结果。原生flat会跳过稀疏数组空位且不修改原数组。instanceof
instanceof:先检查右侧对象的Symbol.hasInstance,若存在可调用方法就由它决定结果;否则右侧必须是函数,沿左侧值的原型链查找该函数的prototype。自定义方法也可能让基本值返回 true。call apply bind
call/apply/bind:call逐项传参,apply传数组状参数,bind返回固定this与前置参数的新函数。手写版还须处理严格模式、构造调用、原型和length,临时属性方案并不完全等价。Array.prototype.map
map:遍历0..length-1,只在索引存在时调用回调callback.call(thisArg, value, index, array),输出等长数组并保留空位;先检查回调可调用。用正则实现模板字符串
模板字符串替换:如
/\{\{\s*([\w.]+)\s*\}\}/g匹配{{user.name}},回调用安全的路径读取函数取值;若插入 HTML,需做上下文转义,不能eval。lodash.get
lodash.get:把点号、方括号或数组路径解析为键序列后逐层读取;最终解析值为undefined时返回默认值,路径不存在也同样返回默认值,不必区分两者。此题只读数据,不应额外执行路径表达式。深拷贝
深拷贝:普通结构优先
structuredClone;手写时用WeakMap处理循环引用,并针对Date、RegExp、Map、Set、数组等逐类复制。函数、DOM 节点和原型行为须单独约定。寄生组合式继承
寄生组合继承:
Child.prototype = Object.create(Parent.prototype, {constructor:{value:Child,writable:true,configurable:true}});在Child中Parent.call(this, ...)初始化实例属性,避免重复调用父构造函数。发布订阅者模式
发布订阅:维护
Map<事件, Set<回调>>;on加入并返回退订函数,emit用快照遍历,off删除。约定回调抛错是否中断后续订阅者。
实现 Promise (hot)
完整实现Promise A+
Promise/A+:实现
pending/fulfilled/rejected三态、只能首次落定、异步执行then回调和then返回新 Promise;核心resolvePromise要处理 thenable、递归吸收、自解析拒绝与防止多次调用。A+ 不规定finally/all,完整原生兼容还需遵循 ECMAScript 微任务语义。实现Promise.prototype.finally
finally:若onFinally不是函数,直接透传原结果;否则通过then在兑现/拒绝两条路径调用它,等待其返回值落定后继续传原值或原错误,清理失败则以新错误覆盖。简化版可用Promise.resolve(onFinally()),完整兼容还须考虑 Promise 子类的Symbol.species。实现Promise.allSettled
allSettled:对每项Promise.resolve,按输入顺序存{status:'fulfilled',value}或{status:'rejected',reason},全部完成才兑现;空输入兑现[]。实现 Promise.race
race:对每项Promise.resolve(item).then(resolve,reject);第一个落定结果决定输出。空输入保持 pending。实现 Promise.prototype.catch
catch:等价于this.then(undefined, onRejected);会返回一个新的 Promise。Promise.resolve
resolve:Promise.resolve(value)吸收 thenable;同构造器的 Promise 可直接返回。手写时防止 thenable 的多次落定和自引用。Promise.reject
reject:Promise.reject(reason)返回已拒绝的新 Promise,不吸收传入的 Promise。
Promise 周边场景题(hot)
交通灯
交通灯:把每个灯封装成
await sleep(duration)后切换状态,外层while (!signal.aborted)顺序运行;离开页面时取消计时器。封装异步的fetch,使用async await方式来使用
异步 fetch:
async function request(url, options){ const r=await fetch(url,options); if(!r.ok) throw new Error(String(r.status)); return r.json(); };调用方用try/catch,并支持signal。repeat(console.log, 5, 1000)
repeat(fn,5,1000):循环 5 次,每次await sleep(1000)后调用fn;明确是否应立即执行第一次,并返回可取消句柄。封装一个工具函数输入一个promiseA返回一个promiseB如果超过1s没返回则抛出异常如果正常则输出正确的值
一秒超时:
Promise.race([promiseA, timeoutPromise])可让等待方超时,但不能中断原任务;若能控制请求,应传AbortSignal.timeout(1000)。请求5s未完成就终止
五秒未完成终止:创建
AbortController,setTimeout(() => controller.abort(), 5000),传signal给fetch,finally清除定时器;不要只做Promise.race。实现一个sleep函数
sleep(ms):new Promise(resolve => setTimeout(resolve, ms));如需取消,再监听AbortSignal并清除定时器。js每隔一秒打印1,2,3,4,5
每秒打印 1 到 5:
for(let i=1;i<=5;i++){ await sleep(1000); console.log(i); },要立即输出 1 则把打印放到等待之前。使用 setTimeout 实现 setInterval
setTimeout模拟setInterval:在回调结束后再次setTimeout(tick, interval);返回取消函数。此方式不会重叠,但间隔包含回调耗时。promise实现图片异步加载
图片异步加载:
new Promise((resolve,reject)=>{const img=new Image(); img.onload=()=>resolve(img); img.onerror=reject; img.src=url;});先绑事件再设src。使用Promise封装AJAX请求
Promise 封装 AJAX:创建
XMLHttpRequest,注册load/error/timeout/abort,open/send;只把允许的 HTTP 状态当成功,错误时拒绝,支持取消与超时。我们能反过来使用 setinterval 模拟实现 settimeout 吗?
setInterval模拟setTimeout:首次触发时立即clearInterval(id)再执行回调;可实现近似效果,但计时精度和页面节流仍受环境影响。异步任务:依次发送3次网络请求,拿到服务器数据
三次串行请求:
for (const input of inputs) { const response = await fetch(...); results.push(await response.json()); };后一请求需要前一结果时用其结果构造参数。实现网络请求超时判断,超过三秒视为超时
三秒请求超时:
fetch(url,{signal:AbortSignal.timeout(3000)});区分超时、用户取消和 HTTP 非 2xx 响应。promise中断请求
Promise 无通用“中断”;Promise 只是结果容器。请求取消要用
AbortController并把signal传给可取消的底层 API。给定一系列的api,测量上传速度(实现的时候用的GET请求)并选择一个加载时间最短的api
测量 API:对每个候选发独立请求,以
performance.now()差值计时,处理超时/错误,再按成功请求耗时选最小;GET 测的是延迟与下载,不等于上传速度。上传速度须用实际 POST/PUT 字节数除以耗时。settimeout系统补偿时间
补偿计时:记录计划执行时间
next += interval,每次使用setTimeout(tick, Math.max(0,next-performance.now()));后台节流无法完全补偿。setTimeout准时
setTimeout无法保证“准时”;可按绝对时间重排定时器并在回调中计算漂移,主线程阻塞或浏览器节流时仍会迟到。并发多个请求,返回先得到response的。函数输入为url数组,输出为第一个返回的response的结果
并发请求取最先成功/最先完成需区分:
Promise.race取最先落定,Promise.any取最先成功;请求完成后可取消其他fetch。JS异步数据流,实现并发异步请求,结果顺序输出
并发发起、顺序输出:先
const promises=urls.map(request),再按顺序for (const p of promises) console.log(await p);提前为每项附拒绝处理,避免未处理拒绝。Promise串行
Promise 串行:
tasks.reduce((p,task)=>p.then(()=>task()), Promise.resolve());tasks必须是函数,不能是已启动的 Promise。处理高并发, 100 条数据,带宽为 10, 跑满带宽
100 项、并发 10:维护最多 10 个 worker,各 worker 从共享下标领取下一任务,完成后继续领取;结果按输入下标存储,最后
await Promise.all(workers)。设计一个简单的任务队列, 要求分别在 1,3,4 秒后打印出 "1", "2", "3";
1、3、4 秒后打印:如果是相对起点,分别设置三个
setTimeout;若指相邻间隔,则顺序await sleep(1000/3000/4000),需先确认题意。实现有并行限制的 Promise 调度器
并行限制调度器:队列保存任务函数,
active < limit时启动,任务在finally中递减active并启动下一项;add返回该任务结果 Promise。实现 Scheduler
Scheduler:与上一题相同,核心是不超过并发上限、每项结果和错误独立传回,并处理空队列与同步抛错。
有并发限制的Promise.all(ts类型)
TypeScript 版限流
all:签名可用function limitAll<T>(tasks: Array<() => Promise<T>>, limit:number): Promise<T[]>;校验limit > 0,worker 并发领取索引,按原顺序写结果,任一失败拒绝。实现 如果上一次的没请求完,之后的就无响应
忽略后续请求:用
inFlight锁;若已在请求则直接返回既定结果(如undefined),否则try { return await request() } finally { inFlight=false }。需明确“无响应”是忽略还是复用当前 Promise。使用 Promise 实现每隔三秒输出时间
每隔三秒输出时间:循环
await sleep(3000); console.log(new Date());提供停止标志或AbortSignal以释放定时器。使用 Promise 改写回调地狱
回调地狱:先把每个 Node 风格回调接口包装为 Promise,再用
async/await依赖顺序执行,并在try/catch统一处理失败。设计一个函数,该函数的参数为可同时发送请求的大小,返回一个函数,该函数的参数为要请求的url。 实现的效果为,同时发送n个请求,当有请求返回后往请求队列里push新的请求,并输出刚刚结束的请求的返回值
返回限流请求函数:内部维护
queue和active;调用时把{url,resolve,reject}入队,空位立即发请求,完成后输出结果、落定调用方 Promise 并继续取队列。Promise.retry 超时重新请求,并在重试一定次数依然失败时输出缓存内容
重试与缓存:每次用独立超时信号请求,失败后最多重试 N 次,可指数退避;全部失败时返回事先存在的缓存,否则抛最后错误。非幂等写操作不能盲目重试。
写一个 mySetInterVal(fn, a, b),每次间隔 a,a+b,a+2b 的时间,然后写一个 myClear,停止上面的 mySetInterVal
递增间隔:保存次数
n,每次执行后安排下一次a+n*b毫秒;myClear设置停止标志并clearTimeout(id)。首轮是等待a后执行。
JavaScript 常考手写题
产生一个不重复的随机数组
假设从
[min,max]取n个不重复整数:验证n <= max-min+1,可先建候选数组再 Fisher–Yates 洗牌并取前 n 项;避免无限随机重抽。await async 如何实现
async/await可理解为把生成器的每一步包装成 Promise:next(value)得到{value,done},未结束时Promise.resolve(value).then(step,throwStep),结束时兑现返回值。使用递归完成 1 到 100 的累加
sum(n)=n<=0?0:n+sum(n-1),所以sum(100)=5050;实际大范围求和可用n*(n+1)/2避免栈溢出。打印出 1~10000 以内的对称数
遍历 1 到 10000,把数字转字符串并与反转字符串比较;“对称数”按十进制回文数解释,若 10000 包含与否按题目约定。
实现一个字符串匹配算法indexOf
indexOf(text,pattern):外层枚举起点0..text.length-pattern.length,内层逐字符比较,首次全匹配返回起点;空模式返回 0,无匹配返回 -1。请实现一个模块 math,支持链式调用math.add(2,4).minus(3).times(2);
链式 math:对象持有当前值,
add/minus/times修改值并返回this,用value()取结果;示例需约定add(2,4)为初始加法,结果(2+4-3)*2=6。手写用 ES6proxy 如何实现 arr[-1] 的访问(滴滴2020)
new Proxy(arr,{get(target,key,receiver){if(typeof key==='string' && /^-[1-9]\d*$/.test(key)){const i=target.length+Number(key);return i>=0?Reflect.get(target,String(i),receiver):undefined} return Reflect.get(target,key,receiver)}});必须先排除 Symbol 键,数组迭代等操作才不会因Number(Symbol)抛错。有一堆整数,请把他们分成三份,确保每一份和尽量相等(11,42,23,4,5,6 4 5 6 11 23 42 56 78 90)
三组尽量等和属于三路划分,通常是组合优化题。小数组可回溯搜索并剪枝,最小化三组和的最大差;大数组可按数值降序放入当前最小和组作近似,需说明不保证最优。
之字形打印矩阵
假设按行“之”字打印:偶数行从左到右、奇数行从右到左;若题目指对角线之字遍历,需另定遍历方向。
数组中的最大值
非空数组用
Math.max(...arr)或reduce((max,x)=>Math.max(max,x));大数组避免展开参数过多,空数组需约定返回值。用尾递归实现斐波那契数列
尾递归
fib(n,a=0,b=1){return n===0?a:fib(n-1,b,a+b)};JS 引擎不普遍保证尾调用优化,大 n 建议改循环或BigInt。实现简单路由
简单路由:监听
popstate,调用history.pushState后主动渲染当前location.pathname对应视图;直达路径需服务器回退到index.html。封装一个localstorage的setItem和getItem方法
setItem用JSON.stringify,getItem用JSON.parse;包装try/catch处理禁用存储、配额与坏 JSON,并约定缺失值为null。1-1000回文数
同第 4 题,限定 1 到 1000:
String(n) === [...String(n)].reverse().join('')。随机生成字符串
若用于口令/令牌,用
crypto.getRandomValues取随机字节,并通过拒绝采样映射到字符表以避免取模偏差;Math.random仅适合非安全用途。判断一个字符串是否为驼峰字符串, judge('ByteDance','BD') -> true judge('Bytedance','BD') -> false
按大写字母提取缩写:
[...s].filter(ch=>/[A-Z]/.test(ch)).join('')===abbr;ByteDance得BD,Bytedance得B。此定义仅适用于 ASCII 驼峰。压缩字符串
例如游程编码:连续相同字符写成“字符+次数”,
aaabb→a3b2;需约定数字或分隔符出现在原文时如何消歧。Map 场景题:按 categories 将商品归类,输出每个分类下的商品名称
原讨论给出商品
{name,categories}数组,目标是按分类反向归组。用Map<分类, 商品名称数组>,遍历每个商品的每个分类并追加名称,最后转为[{name:'热门',categories:['可乐','苹果']}, ...];分类首次出现的顺序可由Map保留。输入50a6we8y20x 输出50个a,6个we,8个y,20个x
需要给多字符片段定义分界,否则
50a6we8y20x有歧义。若约定“数字后直到下一段数字前全是重复片段”,用/\d+\D+/g分段,分别取数量和片段后repeat。手写defineProperty
原讨论实际在探讨用
Object.defineProperty实现属性监听:遍历对象自有键,为每个键定义get收集依赖、set在值变化时通知订阅者,并递归监听新赋入的对象。需说明新增/删除属性和部分数组操作无法仅靠现有属性的 getter/setter 自动捕获;若题意是“复刻原生 defineProperty”,纯 JS 无法完全实现。String string 值一样返回true Object Object 返回true function function 都是声明的一个新的变量 返回false
String原始值按值相等;对象、函数按引用相等。两个不同对象即使结构相同也不相等;同一引用的对象或函数相等,故原题“Object Object 返回 true”要明确是否同一引用。对输入的字符串:去除其中的字符'b';去除相邻的'a'和'c'
先删除所有
b,再反复删除相邻ac,直到稳定;若删除会产生新邻接,可用栈在线处理,每见c且栈顶为a则弹出。用一行代码,将数组中的字符串和字符串对象(new String(123))直接判定出来
arr.filter(x => typeof x === 'string' || x instanceof String);跨 realm 字符串对象时instanceof不可靠,可用Object.prototype.toString.call(x)==='[object String]'。实现 before(n, fn):fn 最多执行 n 次,此后返回最后一次结果
按原讨论中的示例,闭包记录次数和最近结果;前 n 次调用执行
fn并保存结果,之后直接返回最近结果。注意这与 lodash 的before(n,fn)只执行前 n−1 次不同,必须按本题示例实现。实现一下 console.log
原讨论中的实现是包装调用:
function myLog(...args){ console.log(...args) };若要求从零实现浏览器 DevTools 控制台,纯 JS 无法做到,需区分这两种题意。实现(5).add(3).minus(2)功能
可给
Number.prototype定义add/minus返回普通数,但污染全局原型且与装箱有关;更稳妥是num(5).add(3).minus(2).value(),结果 6。将十进制数字转为二进制数字字符串
非负安全整数可用
n.toString(2);手写时不断取n%2并整除 2,逆序拼接,0 返回'0'。负数的补码位宽需另外指定。封装remove child.remove()销毁自身
child.remove()已是标准 DOM API;兼容写法child.parentNode?.removeChild(child)。删除节点前应解除应用自行注册的外部监听或资源。字符串中字母的出现次数
遍历字符串,用
Map或对象计数;“字母”是否含 Unicode、是否区分大小写需约定。输出一个等腰三角形
高度 n 的第 i 行输出
n-i个空格加2*i-1个星号;用repeat拼每行。实现一个函数a,使其奇数次调用时返回1,偶数次调用时返回2(不能使用全局变量)
闭包保存调用次数:
const a=(()=>{let n=0;return ()=>++n%2?1:2})();无需全局变量。从数组中找出最接近目标数的值
给定非空数组
values与目标数target,遍历并保留Math.abs(x-target)最小的元素;原讨论的例子目标为 37,[3,56,56,23,7,76,-2,345,45,76,3]的结果是 45。距离相同保留先出现者,空数组需另定返回或抛错。不用循环求和
若求 1 到 n,直接
n*(n+1)/2;若是任意数组且禁用循环语句,可用递归或reduce,但底层仍会遍历。连续赋值操作
a=b=c=1从右向左赋值;a.x = a = {}会先确定左侧引用,再计算右侧赋值,可能导致旧对象的x指向新对象。需给出具体表达式才能唯一推导。输入一串字符串,根据字符串求出每个字母的数量并返回结果对象。(数字为1时可省略
遍历字符串计频并返回对象,例
aab→{a:2,b:1};“数字为 1 可省略”暗示输出可能是编码字符串,原题格式不完整。创建包含10个1的数组 多种方法
Array(10).fill(1)、Array.from({length:10},()=>1)、[...Array(10)].map(()=>1);注意Array(10).map(()=>1)因空位不会执行回调。将 ['zm', 'za', 'b', 'lm', 'ln', 'k'] 按首字母分组,并将组内字符串排序
遍历字符串,以首字母为键收集到对象或
Map,再对每个分组调用sort();示例输出{b:['b'],k:['k'],l:['lm','ln'],z:['za','zm']}。原讨论已补充这一目标。将升序整数数组 [0,1,2,4,5,7,13,15,16] 压缩为 ["0->2", "4->5", "7", "13", "15->16"]
输入必须是升序整数数组:扫描每段连续的
+1区间,记录起点和终点;单个数输出自身字符串,连续段输出起点->终点。给定输入应得到题目所示五段;空数组返回[],重复数如何处理需另定。['ab', 'c', 'ab', 'd', 'c'] => ['ab1', 'c1' ,'ab2', 'd', 'c2']
先计每个值总出现次数,再遍历维护已出现次数;仅重复的值追加序号,得到题示结果。
移除空属性
过滤对象条目;“空”需定义。常见仅删
null/undefined/'':Object.fromEntries(Object.entries(obj).filter(([,v])=>v!==null&&v!==undefined&&v!==''))。判断两个对象是否相等
先定义浅比较还是深比较。深比较需处理类型、键集合、数组顺序、循环引用、
Date/Map/Set和原型;通常用配对WeakMap避免无限递归。一个数组,找出每个数组元素右侧第一个比当前数大的数的下标,时间复杂度O(N)
单调递减栈保存未找到答案的下标;遍历新元素时反复弹出比它小的下标并填当前下标,剩余填 -1;时间 O(n)。
寻找出现次数最多的三个标签
用
Map计频,再按次数降序、平局按字典序排序取前 3;数据量大可用容量 3 的最小堆。素数
n>1且不存在2..sqrt(n)的整数因子即素数;可跳过偶数,生成区间素数可用埃氏筛。实现日期格式化函数
用
Date取年月日时分秒,padStart(2,'0')补齐,按令牌替换;明确使用本地时区还是 UTC,避免把月份从 0 开始当成 1。实现 jsonp
JSONP 仅能 GET:创建唯一全局回调名,拼入 URL 查询参数,插入
<script>;回调或出错/超时后清理脚本和全局函数。只能向可信源使用,响应会执行脚本。将 www.baidu.taobao.com 按点号分段反转为 com.taobao.baidu.www
原讨论要求反转点号分隔的域名片段,而不是反转字符或 URL 路径:
www.baidu.taobao.com→com.taobao.baidu.www。可用split('.').reverse().join('.');若禁用这些方法,就从右向左扫描片段并拼接。解析 URL Params 为对象
new URL(url).searchParams解析,重复键可用getAll保留数组;URLSearchParams会处理百分号解码。不能把?a=1&a=2默默丢成单值。调用计数器(支持重置)
闭包持有计数,
count()增加并返回值,count.reset()将其设回初值;不暴露可随意修改的全局状态。颜色生成
原题未说明颜色格式。若生成随机
#RRGGBB,从crypto.getRandomValues(new Uint8Array(3))取 3 字节,各自转两位十六进制后拼接;若仅装饰用途也可用Math.random。JavaScript怎么清空数组
arr.length=0会原地清空并影响同一数组的其他引用;arr=[]只是让当前变量指向新数组;splice(0)也会原地清空。判断A、B数组的包含关系(值和数量),A属于B返回1,B属于A返回2,两者相等返回0,其他返回-1
用计数
Map比较每个值出现次数。两边计数完全相同返回 0;A 计数均不大于 B 返回 1;B 属于 A 返回 2;否则 -1。对象元素按引用比较。对象的合并
浅合并用
{...a,...b}或Object.assign({},a,b),后者覆盖同名键;深合并需单独规定数组、空值和循环引用策略,避免原型污染。实现一个可无限延伸的数组
若题意是按需得到无限序列,可用生成器
function* numbers(){let i=0;while(true) yield i++},逐次next()而不一次存入内存;原讨论也有用 Proxy 为越界索引提供默认值的不同理解。JS 数组本身不能真正占用无限内存,仍需明确目标接口。将按空格分列、换行分行的多行字符串转为二维数组
原讨论的输入是以换行分行、连续空白分列的数字文本,可用
s.trim().split(/\r?\n/).map(line=>line.trim().split(/\s+/))得到字符串二维数组;若要数字,再对每项用Number转换。空输入需单独返回[]。请实现一个通用的Array解构赋值
JS 已有
[a,b]=iterable;手写通用解构要遵守迭代协议、默认值、剩余元素和迭代器关闭规则。仅针对数组可按下标取值,但不等价于通用解构。数组合并
浅合并
[...a,...b]或a.concat(b);是否去重、是否深拷贝元素另行约定。数组交集,并集,差集
集合语义先用
const sa=new Set(A), sb=new Set(B)去重;交集new Set([...sa].filter(x=>sb.has(x))),并集new Set([...sa,...sb]),差集new Set([...sa].filter(x=>!sb.has(x)))。若题目要求保留重复次数,则改用计数Map。多维数组全排列
多维数组笛卡尔积:
arrays.reduce((acc,arr)=>acc.flatMap(prefix=>arr.map(x=>[...prefix,x])),[[]]);任一维为空则结果为空。判断对象是否存在循环引用
DFS 用“当前递归路径”
WeakSet检测回边;退出节点时删除。仅用全局 visited 会把共享引用误判为循环。找出数组中最小的 k 个数,并保持它们在原数组中的相对顺序
先取最小 k 个数的多重计数,再原顺序遍历数组,遇到仍需的值就加入并减计数;排序阈值法时间 O(n log n),可用选择算法优化。
逆对象扁平
假设把点路径键还原为嵌套对象:按
.分割路径逐层创建对象并赋值;要处理数组索引、路径冲突并拒绝__proto__/constructor/prototype。对象扁平化
DFS 遍历对象并把路径连接成
a.b键;数组可选择索引路径。循环引用、点号本身出现在键中、空对象都需约定表示法。实现 执行一次的函数
once(fn)用闭包存called与result,第一次fn.apply(this,args),之后返回缓存值;若函数抛错,通常不标记成功,以便下次重试。链式调用
方法返回
this或新包装对象以继续调用,最后用value()取结果;需明确是可变链还是不可变链。偏函数
偏函数先保存部分参数,调用时合并后续参数并以调用时
this调原函数;如partial(fn,...bound){return function(...rest){return fn.apply(this,[...bound,...rest])}}。实现管道函数
管道函数按从左到右执行:
fns.reduce((value,fn)=>fn(value),initial);要支持异步时用 Promise 链。手写事件代理(委托)
在共同父节点监听事件,使用
event.target.closest(selector)找匹配子元素,并确认它仍属于父节点;适用于动态子元素,卸载时移除监听。数据类型判断
typeof识别基本值与函数,但typeof null==='object';对象具体类型可用Object.prototype.toString.call(value),跨 realm 和Symbol.toStringTag需注意。类数组转数组
Array.from(arrayLike)或Array.prototype.slice.call(arrayLike);前者同时支持可迭代对象并可映射。预加载
图片预加载可创建
Image并设src,关键资源可用<link rel="preload">;仅预加载确实马上会用的资源,避免争抢带宽。图片懒加载
优先原生
<img loading="lazy">;需自定义进入视口行为时用IntersectionObserver,加载后取消观察并处理失败占位。数组去重
基本值去重
Array.from(new Set(arr));对象按引用去重,按字段去重用Map(key → item)并明确保留首项还是末项。防抖 节流
防抖在连续触发停止一段时间后执行;节流在窗口内最多执行一次。实现时保存计时器/上次时间,提供
cancel,并处理首次与末次执行需求。函数组合 compose redux-saga koa 洋葱模型
普通
compose(f,g)(x)=f(g(x))从右往左;Koa 洋葱模型是中间件await next()前后包裹下游,需防止next被调用两次。sum(x,y)和sum(x)(y)
可让
sum(x,y)在第二参存在时直接返回和,否则返回y=>x+y;判断参数个数用arguments.length,避免把合法的0当作缺失。curry柯里化
柯里化把多参数函数拆成逐步接收参数的函数,收集到原函数形参个数后执行;默认参数、剩余参数会影响
fn.length,需约定完成条件。
设计模式相关
观察者模式
观察者模式:被观察者持有观察者集合,状态变化时逐个通知;与发布订阅相比,观察者通常直接订阅特定对象,发布订阅常通过事件中心解耦双方。退订和通知中抛错策略要明确。
树-场景题(hot)
DOM2JSON
DOM2JSON:递归读取
nodeType;文本节点输出文本,元素节点输出小写标签、选定属性和childNodes。避免读取事件处理器、私有数据或过大的 DOM 子树。计算目录树的深度
树深度:空树为 0,叶子为 1,
depth(node)=1+max(depth(child));极深树可用显式栈避免调用栈溢出。树形结构获取路径名
路径名:DFS 时传入祖先路径数组,到目标节点返回
[...path,node.name];需要返回所有匹配时不要找到首项就终止。对象树遍历
对象树遍历:DFS 可用栈,BFS 可用队列;对每个节点处理后遍历
children,用WeakSet防循环。获取树对象属性
“获取树对象属性”缺少目标结构。若按指定键收集属性,遍历节点并
result.push(node[key]),未定义值按需求跳过。查找json中的children路径
查找
children路径:递归访问子节点,维护索引或名称路径;命中目标后返回路径,重复目标时需决定返回一个还是全部。对象字符串转化成树形结构
对象字符串转树:若输入是 JSON 字符串,先
JSON.parse,再验证节点结构;若是缩进文本,按缩进层级用栈构树。原题缺少样例,无法确定格式。判断有无符合路径和 -> 打印所有路径
DFS 累加沿途数值,命中目标和时输出当前路径;负数存在时不能仅凭当前和大于目标剪枝。
获取树结构中的name:getName
DFS 遍历每个节点,收集
name;若仅取目标节点名称,应先说明按 id、路径还是其他条件查找。
实现 JS 原生方法
Array.prototype.forEach
forEach:转换接收者为对象,取长度快照,只访问存在的索引,调用callback.call(thisArg,value,index,object);返回undefined。Array.prototype.filter
filter:同样跳过空位,回调为真时将原值追加到新数组;结果数组是密集的。Array.prototype.reduce
reduce:有初始值从索引 0 开始;无初始值时查找首个存在的索引作为累加器,空数组且无初始值抛TypeError。Array.prototype.fill
fill(value,start,end):把负索引换算成从末尾计,范围截在[0,length],逐个写入原数组并返回原数组;会填补空位。Array.prototype.includes
includes:按SameValueZero比较,因此NaN可找到,+0/-0相等;空位按undefined处理,支持负起始索引。Array.prototype.push
push(...items):从当前length起写入各项,更新长度并返回新长度;数组接近最大长度时可能抛错。Array.prototype.unshift
unshift(...items):从后向前移动旧元素,再写入新元素,返回新长度;需保留原有空位。Array.prototype.copy
标准数组原型没有
copy方法;可能指copyWithin:把数组内区间复制到目标位置,重叠时选择正确方向,原地修改并返回数组。Array.prototype.getLevel
标准数组原型没有
getLevel;若指数组嵌套深度,可递归求1+max(子数组深度),空数组/非数组的基准层数先约定。实现es6的set集合
Set 可用
Map模拟增删查与迭代,值比较需用SameValueZero且维持插入顺序;直接使用对象键会把不同对象混为一谈。实现es6的map集合
Map 可用原生
Map作为说明其行为的参考;纯 JS 完整复刻需对象键身份、插入顺序、迭代器与删除再插入语义,不能只用普通对象代替。String.prototype.zpadStart
zpadStart非标准方法;若意为左补零,String(value).padStart(targetLength,'0'),超过长度保持原字符串。Object.assign
Object.assign逐个源对象拷贝自身可枚举的字符串与 Symbol 键到目标,会触发 getter/setter,浅拷贝并返回目标;null/undefined源跳过。Object.is
Object.is类似严格相等,但NaN与自身相等,+0与-0不相等;可用x===y ? x!==0 || 1/x===1/y : x!==x && y!==y。JSON.stringify
JSON.stringify要处理转义、数组和对象、toJSON、replacer、缩进、循环引用及undefined/BigInt等规则;不宜声称简单递归版等价于原生。JSON.parse
JSON.parse需词法分析与递归下降解析字符串、数字、布尔、null、数组和对象,并拒绝非法语法;绝不使用eval解析不可信 JSON。trim
trim:删除字符串两端的 ECMAScript 空白和换行字符,不改变中间内容;/^\s+|\s+$/g是常见近似实现。实现new
new:创建以Constructor.prototype为原型的对象,用该对象调用构造函数;若构造函数返回对象/函数就采用返回值,否则返回新对象。箭头函数不能被new。String.prototype.repeat
repeat(n):验证 n 为非负有限数并转整数,重复字符串 n 次;可用倍增拼接减少次数,过大输出应抛范围错误。String.prototype.includes
字符串
includes(search,position)可用indexOf(search,position)!==-1,但需拒绝正则入参,并处理位置转换与空串。
JS 库函数实现
lodash.chunk
chunk(array,size):要求size为正整数,循环i += size并slice(i,i+size);空数组返回[],不修改输入。
JS 工具函数
判断一个对象是否是isPlainObject
isPlainObject:先排除null和非对象,再检查原型为Object.prototype或null;跨 realm 对象需要更谨慎,不能仅看constructor === Object。
手写 nodejs 模块
promisify
promisify(fn):返回函数,在原参数后追加 Node 风格(err,value)=>err?reject(err):resolve(value)回调;调用原函数时保留this,同步抛错也应拒绝。
正则相关
正则判断手机号,qq,颜色,邮箱
手机号、QQ、颜色、邮箱只能按明确业务规则写正则;例如 CSS 六位十六进制色
^#[0-9a-fA-F]{6}$。手机号号段会变化,邮箱完整规范不适合单个简短正则,优先格式初筛加实际验证。字符串的大小写取反
遍历字符,将小写字母转大写、大写转小写,其他不变;只针对 ASCII 可用
/[a-zA-Z]/g。检验字符串首尾是否含有数字
^\d|\d$测首或尾为数字;若要求首尾都为数字,用^\d.*\d$并单独处理单字符数字。去除字符串空格
只去两端空格用
trim();删除全部空白可用replace(/\s/g,''),仅普通空格用/ /g。去除字符串中的字母
ASCII 字母用
replace(/[A-Za-z]/g,'');Unicode 字母可考虑/\p{L}/gu,需目标环境支持。正则 驼峰转换
驼峰转下划线:先处理连续大写缩写与单词边界,再
/([a-z0-9])([A-Z])/g插_并转小写;仅简单正则会把XMLHttp拆错。对象key的驼峰转下划线
递归转换对象键,同时保留数组结构;只处理自身属性,遇循环引用用
WeakMap,并约定键冲突时如何处理。判断字符串中是否存在连续的三个数
若指三个连续数字字符,用
/\d{3}/;若指数值连续递增如123,需逐位比较相邻数字差是否为 1。((2+3)+(3*4))+2---->['(2 + 3)+(3 * 4)', '2 + 3', '3 * 4']
用栈匹配左右括号,记录每对括号对应的原串范围,从内到外抽取;只用正则难以处理任意嵌套层数,输出的空格格式另定。
排序算法
冒泡排序
冒泡:反复比较相邻元素,逆序则交换;一轮无交换可提前结束。平均/最坏 O(n²),稳定。
选择排序
选择:每轮找到剩余元素最小值并与当前位置交换;O(n²),通常不稳定。
快速排序
快排:选枢轴,把小于与大于枢轴的元素分区,再递归两边;平均 O(n log n),最坏 O(n²),原地版通常不稳定。
插入排序
插入:依次把当前元素插入前面已排序部分;O(n²),近乎有序时接近 O(n),稳定。
归并排序
归并:二分递归排序后合并两个有序序列;O(n log n),额外 O(n) 空间,可稳定。
基数排序
基数:按个位、十位等从低到高做稳定分桶;适用于整数或固定编码键,时间与位数及基数有关,负数需分开处理。
计数排序
计数:统计有限整数范围内各值频次,前缀和可用于稳定输出;时间 O(n+值域),值域大时不适合。
希尔排序
希尔:按递减 gap 对各组做插入排序,最后 gap=1;性能依赖 gap 序列,通常不稳定。
桶排序
桶:把值映射到若干桶,桶内排序后依次拼接;分布均匀时较快,偏斜时可能退化,需明确桶边界。
堆排序
堆:建最大堆后反复把堆顶换到末尾并下沉;O(n log n)、额外 O(1) 空间、不稳定。
自定义 Hooks 手写题
Hooks怎么封装手势逻辑
手势 Hook 在
pointerdown/move/up/cancel记录起点、位移和时间,计算方向/速度,设阈值避免误触;在 effect 清理监听,并考虑触屏滚动冲突与键盘替代操作。Hooks实现移动端的滑动轮播插件
轮播 Hook 管理当前索引、拖动位移和动画状态;手势结束按距离/速度切页,索引循环,自动播放计时器在悬停、失焦及卸载时暂停/清理。
如何用 Hooks 模拟 componentDidMount 与componentWillUnmount
useEffect(()=>{ setup(); return cleanup; },[])对应挂载后的副作用和清理;开发模式 Strict Mode 可能执行“建立→清理→建立”检查,不能假设只运行一次。实现一个useBodyScrollLock ,当出现弹窗时 阻止背景滚动
弹窗打开时在 effect 保存
body.style.overflow原值并设hidden,清理时恢复;多个弹窗并存需共享锁计数,移动端还要处理滚动穿透与焦点管理。ts 实现 hooks: useInterval
useInterval(callback,delay)用 ref 保存最新回调,effect 随delay创建/清除 interval;delay=null暂停,避免闭包读到旧状态。实现 useQuery
useQuery需先定义是读取 URL 参数还是数据查询。URL 场景可订阅路由变化并从URLSearchParams派生值;数据场景需缓存键、请求状态和失效规则。实现useRequest
useRequest至少返回{data,error,loading,run,cancel};每次请求持有AbortController或版本号,卸载/新请求时防止旧结果覆盖,处理错误与重复提交。
组件设计题(Vue/React/JS均可)
全选
全选:源状态是已选 id 集合;全选取当前可选项 id,反选移除它们;“部分选择”状态从数量派生,并设置原生 checkbox 的
indeterminate。轮播图
轮播图:索引、上一张/下一张、自动播放和交互暂停;按钮可键盘操作,有可访问名称;动画尊重减少动态效果偏好。
根据response渲染table
表格:校验响应结构,列配置决定渲染,稳定 id 作 key;提供加载、空、错误、分页状态,不将不可信字段当 HTML 注入。
歌词滚动功能(hot)
歌词滚动:解析时间戳并按时间排序,用二分查找当前播放时间对应行;只在行变化时滚到可视区域,用户手动滚动时暂停自动跟随。
实现一个 百度搜索框 (搜索提示)
搜索提示:输入防抖,给每次请求版本号或取消前次请求,确保只展示最新结果;键盘上下/回车/Escape、空结果与
combobox/listbox语义要完整。实现todos
Todos:任务 id、文本、完成状态为核心状态;支持增删改查与过滤,持久化时处理坏数据;列表项用稳定 id。
计时器或倒计时组件
倒计时:记录结束时刻,用
Date.now()差值计算剩余,而不是每秒减 1;卸载清理 timer,切后台后回来仍能校准。设计一个公会入驻信息提交页
入驻提交页:把字段分组、必填与格式验证、保存草稿、提交中禁重复、失败可恢复;上传材料和隐私告知依业务要求设计。
编码实现宽高不相同图片的自适配排列
不同尺寸图片:保留原始宽高比,可用 CSS Grid/Masonry 或按原尺寸计算布局;图片预留尺寸避免 CLS,懒加载并限制单张最大宽度。
列表子元素顺序反转
反转列表顺序:若只改变显示顺序用
[...items].reverse(),不要原地改 props;交互和屏幕阅读顺序应与视觉顺序一致,谨慎使用flex-direction:row-reverse。遍历树组件
树组件:递归节点组件或扁平可见节点列表,按 id 管理展开/选中;大树用虚拟列表,键盘上下/左右导航和
aria-expanded要支持。选项卡
选项卡:管理 active id,按钮用
role=tab、内容用tabpanel,箭头键切换焦点并支持 Home/End;切换时决定是否保留面板状态。拖拽
拖拽:原生 Drag and Drop 或 Pointer Events;更新列表必须依据稳定 id,提供键盘移动方式、拖拽预览、取消和越界处理。
HTML CSS 手写题
实现圆形环状进度条
环形进度条可用 SVG
<circle>的stroke-dasharray/stroke-dashoffset按比例绘制;提供文本百分比与role="progressbar"、aria-valuenow/min/max,不要只靠颜色表达进度。
JavaScript 与浏览器原理
事件循环中,宏任务、微任务和浏览器绘制的执行顺序是什么?
一轮事件循环取一个任务执行,随后清空微任务队列,再有机会更新渲染;实际调度由宿主环境决定,不能认为每个宏任务之后必定绘制。
持续追加微任务为什么会让页面无法响应输入?如何处理长任务?
微任务会在渲染前持续清空,递归追加可能使输入和绘制饿死;把大工作分片并主动让出主线程,可用任务队列、
scheduler.yield()(有兼容性处理)或 Worker。闭包如何形成?在实际项目中怎样排查闭包导致的内存占用?
函数保留创建处词法环境即形成闭包;内存排查用 Heap Snapshot 比较保留对象与引用链,检查未清理监听、计时器和长生命周期缓存。
原型链、class 和继承之间是什么关系?
class是基于原型的语法;实例沿[[Prototype]]链查找方法,extends建立实例和构造器继承链,方法通常放在原型而非每个实例上。箭头函数与普通函数的 this、arguments 和构造行为有何不同?
箭头函数词法绑定
this,无自己的arguments,不能作构造函数;普通函数的this由调用方式决定,可用new,有自己的arguments。ESM 与 CommonJS 的加载、导出和循环依赖行为有何区别?
ESM 静态声明、实时绑定且支持异步模块;CommonJS 运行时
require、导出对象,循环依赖可能得到尚未初始化的部分导出。两者互操作依运行环境。浏览器从输入 URL 到页面可交互,经历了哪些关键步骤?
URL 解析后检查缓存/解析 DNS,建立连接与 TLS,发请求收响应;浏览器解析 HTML/CSS、加载关键资源、布局绘制并执行影响交互的脚本。真实顺序含并行与缓存分支。
浏览器如何构建 DOM/CSSOM,并完成布局、绘制与合成?
HTML 构建 DOM、CSS 构建 CSSOM,再形成渲染树;计算样式与布局、绘制图层并合成。样式/几何变化可能触发重算或重排,transform 常只需合成。
如何用 DevTools 定位内存泄漏、长任务和频繁重排?
Performance 面板看长任务和 Layout,Memory Heap Snapshot 看保留链,Performance Monitor 看节点/堆增长;用可复现步骤和前后对比确认原因。
localStorage、sessionStorage、IndexedDB 和 Cookie 应如何选择?
Cookie 随请求发送,适合少量会话数据并设安全属性;localStorage 持久同步,sessionStorage 按标签页会话,IndexedDB 异步且适合较大结构化数据。敏感令牌避免暴露给脚本。
TypeScript
any、unknown 和 never 分别适用于什么场景?
any跳过检查,仅边界迁移时谨慎用;unknown是安全的未知值,使用前须收窄;never表示不可能值,可做穷尽检查。如何用类型守卫和判别联合安全地收窄联合类型?
用
typeof、in、自定义谓词或判别字段收窄联合;判别联合给每种分支不同字面量kind,避免直接断言类型。如何用 never 检查联合类型的分支是否穷尽?
switch(value.kind)的default中写const unreachable: never = value;新增联合成员时编译器会指出遗漏分支。泛型约束、条件类型和 infer 如何用于设计可复用 API?
T extends Constraint限制泛型能力;条件类型按类型关系选择分支,infer从匹配形状提取内部类型,例如从函数提取返回值。实现 Partial、Pick、Omit、ReturnType 和 Awaited 中任意两个工具类型。
例如
type MyPartial<T>={[K in keyof T]?:T[K]};type MyPick<T,K extends keyof T>={[P in K]:T[P]}。原生工具类型还涉及一些细节,面试应说明边界。interface 与 type 在扩展和声明合并方面有何区别?
interface可声明合并且方便扩展对象形状;type可表达联合、交叉、元组、条件类型等,不能同名声明合并。二者多数对象类型场景可互用。如何为异步请求建模成功、加载、失败三种状态,避免非法状态?
用判别联合:
{status:'loading'} | {status:'success',data:T} | {status:'error',error:Error};只有成功分支才有 data,避免多个独立布尔量形成非法组合。为什么 Vite 能转换 TypeScript,却仍需要单独执行类型检查?
Vite 开发时用转译器去掉 TS 类型以求速度,不执行完整语义类型检查;CI 仍需
tsc --noEmit或vue-tsc --noEmit。
HTML、CSS 与无障碍
Flexbox 与 Grid 分别适合哪些布局?实现响应式卡片网格。
Flex 适合单轴排布,Grid 适合二维行列;响应式卡片可用
grid-template-columns:repeat(auto-fit,minmax(min(100%,16rem),1fr))。BFC 是什么?它如何影响外边距折叠和浮动?
BFC 是独立的块格式化上下文,内部浮动参与高度、与外部浮动布局隔离,内部外边距与外部不折叠;可用
display:flow-root创建。如何避免字体、图片和异步内容造成布局偏移?
图片声明宽高或
aspect-ratio,字体预加载并考虑替换指标,异步区域预留空间;用真实 CLS 数据找移动元素。如何实现暗色模式,并兼顾系统偏好与用户手动选择?
用
prefers-color-scheme作默认值、用户设置作覆盖并持久化;保证对比度,更新color-scheme让原生控件匹配。如何让弹窗具备正确的焦点管理、键盘操作和语义?
优先原生
<dialog>;打开时聚焦合适元素,限制焦点在模态内容内,Escape 可关闭,关闭后回到触发按钮,并标注标题。何时使用原生 HTML 元素,何时才需要 ARIA 属性?
原生按钮、链接、输入自带键盘和语义,优先使用;只有原生语义无法表达时补 ARIA,ARIA 不会自动添加交互行为。
如何验证表单错误提示对键盘用户和屏幕阅读器可用?
标签关联控件,错误文本用
aria-describedby关联,提交后聚焦错误摘要或首个错误;仅颜色变化不足够,用键盘和屏幕阅读器实测。如何实现宽屏到移动端的自适应布局,而不依赖固定设备宽度?
用内容驱动的 Grid/Flex、相对单位与容器查询,让内容自然换行;窄视口、缩放到 200% 和长文案都需检查溢出。
网络、缓存与安全
HTTP 强缓存与协商缓存如何配合?Cache-Control 和 ETag 分别解决什么问题?
Cache-Control指定可缓存性与新鲜期,命中强缓存可不发请求;过期后带If-None-Match与 ETag 协商,未变则 304。带哈希的静态资源适合长期缓存。CORS 为什么由浏览器执行?预检请求在什么情况下出现?
CORS 是浏览器同源策略的一部分,服务器通过响应头授予跨源读取权限;非简单方法/头或某些内容类型会先发 OPTIONS 预检,服务器间请求不受浏览器 CORS 限制。
从 DNS、TCP/TLS 到 HTTP 响应,HTTPS 请求的关键步骤是什么?
先解析 DNS、建立传输连接(TCP 或 QUIC)、TLS 验证证书并协商密钥,然后发送 HTTP 请求与接收响应;可复用连接并有缓存/CDN 分支。
HTTP/2 与 HTTP/3 对前端资源加载有什么影响?
HTTP/2 多路复用和头压缩减少请求开销,但单 TCP 丢包仍可能影响流;HTTP/3 基于 QUIC,可减少部分队头阻塞及连接迁移成本。仍应控制资源大小和优先级。
SSE、WebSocket 与普通 HTTP 请求应如何选择?
普通 HTTP 适合一次性请求;SSE 适合服务端单向事件流与自动重连;WebSocket 适合持续双向低延迟通信,需处理心跳与断线恢复。
如何实现 Token 刷新时的并发去重和失败请求重放?
用单个
refreshPromise让并发 401 共享刷新;刷新成功后重放可安全重试的请求并防无限循环,失败则清理登录态。请求体不可重放时需特殊处理。如何防止 XSS?为何只设置 CSP 仍不够?
默认对不可信文本做上下文编码,避免
innerHTML,对允许的 HTML 用可信清洗库;CSP 是纵深防御,不能替代正确编码和 DOM API 使用。Cookie 的 HttpOnly、Secure 和 SameSite 分别解决什么问题?
HttpOnly阻止 JS 读取 Cookie,Secure要求 HTTPS 传输,SameSite限制跨站请求携带;仍需考虑 CSRF、XSS 与会话过期。CSRF 的触发条件是什么?前后端如何配合防护?
站外页面诱导浏览器携带受害者凭据发送有副作用请求,即可形成 CSRF;用 SameSite、CSRF token、Origin/Referer 检查及避免 GET 改数据共同防护。
如何设计上传大文件的分片、断点续传、校验和重试?
客户端切块并记录上传 id、分片索引和校验值,服务端幂等接收、查缺补漏与最终合并校验;失败只重传缺失块,限制并发并支持取消。
React 与 React 19
React 的 render 与 commit 阶段分别做什么?状态更新何时触发重新渲染?
render 阶段计算下一棵 UI,可被中断/重试;commit 阶段把变化提交到 DOM 并运行相关 effect。状态更新安排重新渲染,同值更新可能被跳过。
列表 key 为什么必须稳定?错误的 key 会造成什么问题?
key 标识同级列表项身份,重排时帮助保留正确组件状态;用索引作 key 在插入/删除/排序时可能让输入状态错位。
useEffect 的依赖、清理和闭包过期问题如何排查?
effect 应列全读取的响应式依赖,返回清理函数;闭包过期可通过正确依赖、函数式更新或 ref 解决,避免靠禁用 lint 掩盖问题。
useMemo、useCallback 和 React.memo 在什么条件下才有收益?
只有计算昂贵、引用稳定性确实影响子组件,且测量证明收益时才用;记忆化本身也有成本,先用 React Profiler 定位。
useTransition 与 useDeferredValue 分别适合什么交互?
useTransition把特定状态更新标为非紧急并给 pending 状态;useDeferredValue让某个值的消费延后,适合输入即时响应、结果稍后更新。如何在 React 中处理表单提交的 pending、错误和乐观更新?
提交时禁重复或标 pending,显示可恢复错误;乐观更新需在失败时回滚或重新同步,尤其注意并发提交的顺序。
useActionState、useFormStatus 和 useOptimistic 各承担什么职责?
useActionState管理 action 结果与 pending,useFormStatus读取所属表单提交状态,useOptimistic在异步期间提供临时乐观视图。React 中何时使用 useState、Context 或外部状态库?
局部状态用
useState;跨层共享少量稳定值可用 Context;复杂跨页面共享或高频更新再评估外部状态库,避免把所有状态集中。如何定位大型 React 应用的无效渲染并量化优化效果?
用 React Profiler 找提交次数和耗时,确定导致更新的 state/props,改动后对同一交互量化比较;开发模式数据不等于生产模式。
如何设计一个支持取消、竞态处理和错误状态的异步数据 Hook?
Hook 在 effect 中创建
AbortController,请求开始设 loading,完成时只让当前版本写入 data/error,清理时 abort;给调用方retry与取消入口。React Compiler 的自动记忆化与手动 useMemo、useCallback 有什么关系?如何验证它是否生效?
React Compiler 可自动记忆化组件和 Hook 中部分计算;手动
useMemo/useCallback仍可精确控制特定值的稳定性,但不能把记忆化当作程序正确性的前提。检查构建配置与编译诊断,用 Profiler 对比提交次数和耗时。
Vue 3
Vue 3 的 reactive 和 ref 如何跟踪依赖并触发更新?
reactive通过 Proxy 跟踪对象属性读写,ref通过.value跟踪读写;effect 读取时收集依赖,写入触发调度更新。computed、watch 和 watchEffect 的使用场景及执行时机有何不同?
computed缓存派生值;watch监控指定源并可取新旧值;watchEffect自动追踪同步读取的依赖。默认 watcher 回调通常在父更新后、组件 DOM 更新前运行,需 DOM 更新后用flush:'post'。Vue 组件之间如何选择 props、emit、provide/inject 和 Pinia?
父子数据用 props/emit,深层依赖注入用 provide/inject,跨页面共享业务状态可用 Pinia;避免把局部临时状态全局化。
v-model 在原生表单和自定义组件上分别如何工作?
原生表单
v-model映射值属性和输入事件;自定义组件默认用modelValueprop 和update:modelValue事件,也可声明命名模型。Vue 的 nextTick 解决了什么问题?什么时候不该依赖它?
nextTick等待 Vue 批量 DOM 更新完成,适合更新数据后读取新 DOM;不应用它代替正确的数据流或异步业务完成通知。Vue 2 项目迁移 Vue 3 时,如何分阶段处理 API 与依赖兼容?
先升级依赖并运行迁移构建,处理不兼容 API/插件;可逐模块迁移组件,覆盖关键交互测试,再切换应用入口。
为什么解构 reactive 对象可能丢失响应性?如何修复?
直接解构普通变量会失去 Proxy 属性读取链;使用
toRefs/toRef保留关联,或在消费处继续读取state.key。Vue 列表渲染中 key 的作用是什么?如何避免组件状态错位?
key 帮助 Vue 在列表变动时识别节点与组件身份;用稳定 id,不在重排列表中用索引作 key。
Next.js 与服务端渲染
CSR、SSR、SSG 与按需重新生成页面分别适用于什么场景?
CSR 适合强交互且 SEO 要求低的界面;SSR 适合请求时个性化内容;SSG 适合变化少的公开内容;按需重新生成适合可容忍短时间旧内容的公开页面。
Server Component 与 Client Component 的边界如何划分?
Server Component 负责服务端数据读取、保密逻辑和减少客户端 JS;需要 state、事件、浏览器 API 的边界用 Client Component,尽量让客户端边界靠近叶子。
Hydration mismatch 有哪些常见原因?如何定位和修复?
服务端与客户端首屏输出不一致会报错,常见于时间、随机数、浏览器状态或非法 HTML 嵌套;定位具体节点,保证首次输出一致,浏览器专有值放 effect 后处理。
哪些数据可以在服务端读取,哪些交互必须留在客户端?
密钥、数据库读取只在服务端;交互、DOM、localStorage 在客户端。服务端返回给客户端的 props 必须可序列化且不含秘密。
如何用 Suspense 和流式渲染改善慢数据页面的体验?
用 Suspense 为慢数据区块设 fallback,让外层内容先到达;流式响应逐步发送边界内容,仍需有错误边界和可用的加载状态。
如何设计公开页面与个性化页面的缓存策略,避免数据串用?
公共数据可按明确标签/时效缓存;含用户身份的数据按请求隔离或不缓存,缓存键必须包含影响输出的身份/权限维度,防止串用。
如何让服务端渲染页面同时具备良好的 SEO 和可交互性?
服务端输出可抓取的标题、正文与元数据,客户端对需要的区域进行 hydration;减少阻塞脚本并确保交互在加载前有可理解的状态。
Next.js 的 Cache Components 与 use cache 如何划分静态、缓存和请求时数据?
Cache Components 让可缓存区域显式使用
use cache并设置生命周期/失效策略;依请求变化的区域保持动态,使用 Suspense 边界。具体行为随 Next.js 版本和配置变化,应以项目版本验证。
性能、工程化与测试
LCP、INP、CLS 各衡量什么?如何用真实用户数据定位问题?
LCP 衡量最大内容渲染,INP 衡量交互响应,CLS 衡量意外布局偏移;按设备/页面分组看真实用户分位数,再用实验室工具找根因。
首屏图片、字体与脚本分别如何优化,并如何验证收益?
图片用合适尺寸/格式并优先加载 LCP 图,字体减少字重并控制交换,脚本按需拆分/延后;用同口径真实用户指标和网络瀑布图验证。
长列表和聊天记录应如何虚拟化?动态高度会带来什么问题?
只渲染视口及缓冲区项目;动态高度需测量并维护位置索引,插入历史消息时保持滚动锚点,避免跳动。
Vite 开发服务器与生产构建的工作方式有何区别?
Vite 开发时按需通过原生 ESM 提供模块并快速 HMR;生产构建打包、优化、拆分和压缩资源。TypeScript 类型检查需另跑。
Tree shaking 失效的常见原因是什么?如何检查打包体积?
副作用标记错误、CommonJS、不支持静态分析的动态访问或整个库导入可影响 tree shaking;用构建分析报告和产物确认实际体积。
如何为组件设计单元测试、交互测试和端到端测试的边界?
纯逻辑用单元测试,组件行为用用户交互测试,跨页面关键流程用 E2E;避免三层重复测试同一实现细节。
如何测试异步请求的加载、失败、取消和重复提交?
用可控的请求 mock 验证 loading→success/error、取消后不更新、重复提交行为;断言用户可见状态与请求次数。
Monorepo 中如何划分共享包、版本与依赖边界?
共享包只放跨应用稳定能力,明确包之间依赖方向与发布版本;避免应用互相引用内部源码,CI 按变更范围构建测试。
如何制定前端错误监控、Source Map 和发布回滚方案?
采集错误与发布版本、匿名会话线索;Source Map 私有上传并限权限;发布采用可回退产物/开关,白屏率异常时快速回滚。
如何量化一次性能优化,避免只看本地 Lighthouse 分数?
明确目标指标与样本,固定页面、设备和网络条件比较实验室数据,同时看线上同口径 p75/p95 与错误率,避免只凭一次 Lighthouse 分数。
AI 应用前端与流式交互
如何用 fetch、ReadableStream 和 TextDecoder 逐块解析模型流式响应?
fetch后从response.body.getReader()循环读块,用TextDecoder.decode(value,{stream:true})保留跨块字符;SSE 还需缓冲到完整事件边界再解析,不能假定一块就是一条消息。SSE 流断开、用户取消和网络重连时,界面状态如何恢复?
维护连接状态与最后事件 id;断线按可重放协议重连并去重,用户取消用
AbortController且不自动重连;服务端不支持重放时不能假称续传。模型输出 Markdown 时,如何处理不完整片段和不可信内容?
对不完整 Markdown 可先按文本或增量解析展示,完成后再稳定渲染;禁止直接注入模型生成 HTML,链接与代码展示也要做安全处理。
聊天消息持续增长时,如何控制渲染成本和自动滚动行为?
长列表虚拟化或分段,流式文本按帧/批次更新;仅当用户停留底部时自动跟随,用户向上阅读则显示“跳到底部”按钮。
如何避免旧请求的流式内容覆盖用户最新发起的会话?
每次会话/请求分配版本 id 并取消旧流;收到片段时先核对当前 id,再写入对应消息,切换会话时清理 reader。
工具调用或 Agent 运行较久时,如何展示进度、错误和可取消状态?
明确“排队/运行/等待工具/完成/失败/已取消”状态,显示可理解的进度与重试入口;取消要传到底层并处理服务端仍在执行的情况。
如何审查 AI 生成代码的正确性、安全性与可维护性?
先核对需求与数据边界,再审查输入校验、依赖、授权、错误处理与复杂度;运行测试和真实交互,对关键逻辑保留人工复核。
你如何在实际项目中使用 AI 工具,并验证它产出的代码?
可以描述具体任务、给 AI 的上下文范围、所采用与拒绝的建议,以及测试/审查证据;面试重点是你如何验证,而非仅说“用 AI 提效”。
系统设计与项目深挖
如何设计一个高并发搜索建议框,并处理防抖、取消和结果乱序?
输入防抖并设最小长度,取消旧请求或用版本号丢弃旧结果;缓存短期热门查询,限制服务端速率,完整处理键盘操作、加载和空态。
如何设计后台系统的路由、按钮和数据权限?前后端职责如何划分?
前端路由/按钮权限仅控制体验,后端对每个资源和操作强制鉴权;权限模型可用角色加资源动作,并避免把敏感数据先发给无权用户。
如何设计大型前端应用的组件边界和状态管理方案?
按业务领域划分组件与状态,局部状态留局部,跨域状态经明确接口共享;区分服务端缓存数据与 UI 临时状态,防止全局 store 成为耦合中心。
如何解释一次真实项目的性能瓶颈、改动、指标和取舍?
用“场景与基线→证据→改动→前后指标→取舍”叙述真实案例,例如 p75 LCP 从多少降到多少;没有实测数据就如实说明。
如何设计稳定的文件上传体验,包括进度、失败恢复和重复提交?
分片上传、进度计算、断点查询和幂等完成接口;网络失败可重试单片,重复提交用上传 id/幂等键防止重复文件,最终校验完整性。
如何设计实时聊天页面的消息顺序、断线重连和历史分页?
消息用服务端序号或时间加稳定 id 排序,发送中临时 id 与回执对账;断线按游标补消息,历史分页向上插入并保持滚动位置。
WebView 与原生页面通信时,如何设计 JS Bridge 的权限和错误处理?
JS Bridge 采用白名单方法、版本化请求/响应 id、来源校验与权限控制;每次调用有超时和结构化错误,敏感能力必须由原生端复核授权。
当线上页面出现白屏时,你会如何定位、止损和复盘?
先确认影响范围并回滚/切流止损,再查发布版本、静态资源加载、JS 错误、接口和 CDN;复现根因、补监控与回归测试,记录恢复时间和预防措施。