高频手写精选
实现 Promise.all
Promise.all:将可迭代对象转成数组,空数组立即兑现[];逐个Promise.resolve,按原下标保存结果,计数归零后兑现;任一拒绝立即拒绝,其他任务不会自动取消。参考代码 · jsfunction all(items) { return new Promise((resolve, reject) => { const values = Array.from(items), out = Array(values.length); if (!values.length) return resolve([]); let left = values.length; values.forEach((value, i) => Promise.resolve(value).then( result => { out[i] = result; if (--left === 0) resolve(out); }, reject)); }); } all([Promise.resolve(1), 2]).then(console.log);结果:输出 `[1, 2]`;拒绝时返回首个拒绝原因。
JSON2DOM = react的render函数
JSON2DOM:递归创建元素;文本节点用
createTextNode,元素用createElement,属性需区分className、事件和普通属性,再递归追加 children。React 的render还包含协调、状态与更新,不能等同于简单 JSON2DOM。参考代码 · jsfunction json2dom(node) { if (typeof node === 'string' || typeof node === 'number') return document.createTextNode(String(node)); const el = document.createElement(node.type); for (const [key, value] of Object.entries(node.props ?? {})) { if (key === 'className') el.className = value; else if (key.startsWith('on') && typeof value === 'function') el.addEventListener(key.slice(2).toLowerCase(), value); else el.setAttribute(key, String(value)); } for (const child of node.children ?? []) el.append(json2dom(child)); return el; } document.body.append(json2dom({type:'p', children:['Hello']}));结果:页面新增 `<p>Hello</p>`;这里只实现首次创建 DOM。
树形结构转成列表
树转列表:深度优先遍历,把节点的
children拆掉,输出{...node, parentId};记录访问过的节点以防循环引用,保留顺序。参考代码 · jsfunction flattenTree(nodes, parentId = null, seen = new WeakSet()) { return nodes.flatMap(node => { if (seen.has(node)) throw Error('cycle'); seen.add(node); const {children = [], ...rest} = node; const out = [{...rest, parentId}, ...flattenTree(children, node.id, seen)]; seen.delete(node); return out; }); } console.log(flattenTree([{id:1, children:[{id:2}]}]));结果:`[{id:1,parentId:null},{id:2,parentId:1}]`;循环引用会抛错。
列表转成树形结构
列表转树:先用
Map(id → 节点副本)建索引,再按parentId挂到父节点children;根节点单独收集。时间 O(n),需约定孤儿节点和重复 id 的处理。参考代码 · jsfunction toTree(rows) { const nodes = new Map(rows.map(({id, ...rest}) => [id, {id, ...rest, children:[]}])); const roots = []; for (const row of rows) { const node = nodes.get(row.id); if (row.parentId == null) roots.push(node); else if (nodes.has(row.parentId)) nodes.get(row.parentId).children.push(node); else throw Error(`missing parent ${row.parentId}`); } return roots; } console.log(toTree([{id:1,parentId:null},{id:2,parentId:1}]));结果:根节点 1 的 `children` 包含节点 2;重复 id、环应另行拒绝。
Array.prototype.flat
flat(depth):递归或显式栈遍历;遇数组且剩余深度大于 0 则展开,否则加入结果。原生flat会跳过稀疏数组空位且不修改原数组。参考代码 · jsfunction flat(input, depth = 1) { const out = []; function visit(arr, left) { for (let i = 0; i < arr.length; i++) if (i in arr) { if (Array.isArray(arr[i]) && left > 0) visit(arr[i], left - 1); else out.push(arr[i]); } } visit(input, depth); return out; } console.log(flat([1,[2,[3]]], 1));结果:`[1, 2, [3]]`。
instanceof
instanceof:先检查右侧对象的Symbol.hasInstance,若存在可调用方法就由它决定结果;否则右侧必须是函数,沿左侧值的原型链查找该函数的prototype。自定义方法也可能让基本值返回 true。参考代码 · jsfunction myInstanceof(value, rhs) { if (rhs == null) throw TypeError('invalid right operand'); const method = rhs[Symbol.hasInstance]; if (method != null) { if (typeof method !== 'function') throw TypeError('@@hasInstance must be callable'); return !!method.call(rhs, value); } if (typeof rhs !== 'function') throw TypeError('right operand is not callable'); const prototype = rhs.prototype; if (Object(prototype) !== prototype) throw TypeError('invalid prototype'); if (value === null || (typeof value !== 'object' && typeof value !== 'function')) return false; for (let p = Object.getPrototypeOf(value); p; p = Object.getPrototypeOf(p)) if (p === prototype) return true; return false; } console.log(myInstanceof([], Array), myInstanceof(3, {[Symbol.hasInstance]: x => x === 3}));结果:`true true`;无自定义 `Symbol.hasInstance` 时,基本值返回 `false`。
call apply bind
call/apply/bind:call逐项传参,apply传数组状参数,bind返回固定this与前置参数的新函数。手写版还须处理严格模式、构造调用、原型和length,临时属性方案并不完全等价。参考代码 · jsfunction callWith(fn, thisArg, ...args) { return Reflect.apply(fn, thisArg, args); } function applyWith(fn, thisArg, args) { return Reflect.apply(fn, thisArg, args ?? []); } function bindWith(fn, thisArg, ...bound) { return function boundFn(...rest) { return new.target ? Reflect.construct(fn, [...bound,...rest], fn) : Reflect.apply(fn, thisArg, [...bound,...rest]); }; } const add=function(x){return this.n+x;}; console.log(callWith(add,{n:2},3),applyWith(add,{n:2},[3]),bindWith(add,{n:2})(3));结果:`5 5 5`;以 `Reflect.apply/construct` 为底层原语,未覆盖原生 bound 函数的全部元数据。
Array.prototype.map
map:遍历0..length-1,只在索引存在时调用回调callback.call(thisArg, value, index, array),输出等长数组并保留空位;先检查回调可调用。参考代码 · jsfunction mapLike(array, callback, thisArg) { if (typeof callback !== 'function') throw TypeError('callback'); const source = Object(array), out = new Array(source.length >>> 0); for (let i = 0; i < out.length; i++) if (i in source) out[i] = callback.call(thisArg, source[i], i, source); return out; } console.log(mapLike([1,,3], x => x * 2));结果:`[2, <空位>, 6]`。
用正则实现模板字符串
模板字符串替换:如
/\{\{\s*([\w.]+)\s*\}\}/g匹配{{user.name}},回调用安全的路径读取函数取值;若插入 HTML,需做上下文转义,不能eval。参考代码 · jsfunction template(text, data) { return text.replace(/\{\{\s*([\w.]+)\s*\}\}/g, (_, path) => String(path.split('.').reduce((obj, key) => obj?.[key], data) ?? '')); } console.log(template('Hi {{user.name}}', {user:{name:'Ada'}}));结果:`Hi Ada`;若写入 HTML,必须按 HTML 上下文转义。
lodash.get
lodash.get:把点号、方括号或数组路径解析为键序列后逐层读取;最终解析值为undefined时返回默认值,路径不存在也同样返回默认值,不必区分两者。此题只读数据,不应额外执行路径表达式。参考代码 · jsfunction get(object, path, fallback) { const keys = Array.isArray(path) ? path : String(path).replace(/\[(\d+)\]/g, '.$1').split('.'); const value = keys.reduce((current, key) => current?.[key], object); return value === undefined ? fallback : value; } console.log(get({a:[{b:0}]}, 'a[0].b', 9));结果:`0`;复杂 Lodash 路径语法不在这个简化版中。
深拷贝
深拷贝:普通结构优先
structuredClone;手写时用WeakMap处理循环引用,并针对Date、RegExp、Map、Set、数组等逐类复制。函数、DOM 节点和原型行为须单独约定。参考代码 · jsfunction clone(value, seen = new WeakMap()) { if (value === null || typeof value !== 'object') return value; if (seen.has(value)) return seen.get(value); if (value instanceof Date) return new Date(value); if (value instanceof RegExp) return new RegExp(value.source, value.flags); const out = Array.isArray(value) ? [] : Object.create(Object.getPrototypeOf(value)); seen.set(value, out); for (const key of Reflect.ownKeys(value)) out[key] = clone(value[key], seen); return out; } const a = {}; a.self = a; const b = clone(a); console.log(b !== a, b.self === b);结果:`true true`;Map/Set 等需专门处理。
寄生组合式继承
寄生组合继承:
Child.prototype = Object.create(Parent.prototype, {constructor:{value:Child,writable:true,configurable:true}});在Child中Parent.call(this, ...)初始化实例属性,避免重复调用父构造函数。参考代码 · jsfunction Parent(name) { this.name = name; } Parent.prototype.greet = function() { return `Hi ${this.name}`; }; function Child(name) { Parent.call(this, name); } Child.prototype = Object.create(Parent.prototype, {constructor:{value:Child, writable:true, configurable:true}}); console.log(new Child('Ada').greet());结果:`Hi Ada`;父构造函数每个实例只执行一次。
发布订阅者模式
发布订阅:维护
Map<事件, Set<回调>>;on加入并返回退订函数,emit用快照遍历,off删除。约定回调抛错是否中断后续订阅者。参考代码 · jsfunction emitter() { const events = new Map(); return { on(name, fn) { const set = events.get(name) ?? new Set(); set.add(fn); events.set(name,set); return () => set.delete(fn); }, emit(name, ...args) { for (const fn of [...(events.get(name) ?? [])]) fn(...args); } }; } const bus = emitter(); const off = bus.on('x', console.log); bus.emit('x', 1); off();结果:输出 `1` 一次;取消订阅后不再输出。
实现 Promise (hot)
完整实现Promise A+
Promise/A+:实现
pending/fulfilled/rejected三态、只能首次落定、异步执行then回调和then返回新 Promise;核心resolvePromise要处理 thenable、递归吸收、自解析拒绝与防止多次调用。A+ 不规定finally/all,完整原生兼容还需遵循 ECMAScript 微任务语义。参考代码 · jsclass APromise { #state='pending'; #value; #listeners=[]; constructor(executor) { let sealed=false; const settle=(state,value)=>{ if(this.#state!=='pending')return; this.#state=state;this.#value=value; for(const listener of this.#listeners)queueMicrotask(listener); this.#listeners=[]; }; const resolve=value=>{if(sealed)return;sealed=true;resolvePromise(this,value,x=>settle('fulfilled',x),e=>settle('rejected',e));}; const reject=error=>{if(sealed)return;sealed=true;settle('rejected',error);}; try{executor(resolve,reject);}catch(error){reject(error);} } then(onFulfilled,onRejected){ let next; next=new APromise((resolve,reject)=>{ const run=()=>{ try{ const fulfilled=this.#state==='fulfilled'; const handler=fulfilled?onFulfilled:onRejected; if(typeof handler!=='function')return fulfilled?resolve(this.#value):reject(this.#value); resolvePromise(next,handler(this.#value),resolve,reject); }catch(error){reject(error);} }; this.#state==='pending'?this.#listeners.push(run):queueMicrotask(run); }); return next; } } function resolvePromise(promise,value,resolve,reject){ if(value===promise)return reject(new TypeError('self resolution')); if(value!==null && (typeof value==='object'||typeof value==='function')){ let then;try{then=value.then;}catch(error){return reject(error);} if(typeof then==='function'){ let called=false; try{then.call(value,y=>{if(!called){called=true;resolvePromise(promise,y,resolve,reject);}},e=>{if(!called){called=true;reject(e);}});} catch(error){if(!called)reject(error);} return; } } resolve(value); } new APromise(resolve=>resolve({then(ok){ok(3);ok(4);}})).then(console.log);结果:只输出 `3`;包含状态、异步 `then`、链式返回、thenable 吸收和自解析拒绝,尚未运行官方 Promises/A+ 测试套件。
实现Promise.prototype.finally
finally:若onFinally不是函数,直接透传原结果;否则通过then在兑现/拒绝两条路径调用它,等待其返回值落定后继续传原值或原错误,清理失败则以新错误覆盖。简化版可用Promise.resolve(onFinally()),完整兼容还须考虑 Promise 子类的Symbol.species。参考代码 · jsfunction finallyLike(promise, onFinally) { if (typeof onFinally !== 'function') return promise.then(onFinally, onFinally); return promise.then( value => Promise.resolve(onFinally()).then(() => value), error => Promise.resolve(onFinally()).then(() => { throw error; }) ); } finallyLike(Promise.resolve(3), () => console.log('cleanup')).then(console.log);结果:先输出 `cleanup`,再输出 `3`;子类 species 语义未覆盖。
实现Promise.allSettled
allSettled:对每项Promise.resolve,按输入顺序存{status:'fulfilled',value}或{status:'rejected',reason},全部完成才兑现;空输入兑现[]。参考代码 · jsfunction allSettled(items) { return Promise.all(Array.from(items, item => Promise.resolve(item).then( value => ({status:'fulfilled', value}), reason => ({status:'rejected', reason})))); } allSettled([1, Promise.reject('x')]).then(console.log);结果:`[{status:'fulfilled',value:1},{status:'rejected',reason:'x'}]`。
实现 Promise.race
race:对每项Promise.resolve(item).then(resolve,reject);第一个落定结果决定输出。空输入保持 pending。参考代码 · jsfunction race(items) { return new Promise((resolve, reject) => { for (const item of items) Promise.resolve(item).then(resolve, reject); }); } race([Promise.resolve('first'), new Promise(r => setTimeout(r, 10, 'later'))]).then(console.log);结果:`first`;空输入永不落定。
实现 Promise.prototype.catch
catch:等价于this.then(undefined, onRejected);会返回一个新的 Promise。参考代码 · jsfunction catchLike(promise, onRejected) { return promise.then(undefined, onRejected); } catchLike(Promise.reject('error'), e => `handled ${e}`).then(console.log);结果:`handled error`。
Promise.resolve
resolve:Promise.resolve(value)吸收 thenable;同构造器的 Promise 可直接返回。手写时防止 thenable 的多次落定和自引用。参考代码 · jsfunction resolveLike(value) { return new Promise(resolve => resolve(value)); } resolveLike({then(resolve){resolve(3)}}).then(console.log);结果:`3`;原生 Promise 的 resolve 过程会吸收 thenable。
Promise.reject
reject:Promise.reject(reason)返回已拒绝的新 Promise,不吸收传入的 Promise。参考代码 · jsfunction rejectLike(reason) { return new Promise((_, reject) => reject(reason)); } rejectLike('bad').catch(console.log);结果:`bad`;传入 Promise 时不会吸收其状态。
Promise 周边场景题(hot)
交通灯
交通灯:把每个灯封装成
await sleep(duration)后切换状态,外层while (!signal.aborted)顺序运行;离开页面时取消计时器。参考代码 · jsconst sleep = ms => new Promise(r => setTimeout(r, ms)); async function traffic(signal) { for (const [color, ms] of [['red',3000],['green',2000],['yellow',1000]]) { if (signal.aborted) return; console.log(color); await sleep(ms); } } traffic(new AbortController().signal);结果:依次打印 `red`、`green`、`yellow`;示例只跑一轮,取消需同时清理正在等待的定时器。
封装异步的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。参考代码 · jsasync function request(url, options = {}) { const response = await fetch(url, options); if (!response.ok) throw new Error(`HTTP ${response.status}`); return response.json(); } request('/api/user').then(console.log).catch(console.error);结果:2xx JSON 响应解析为对象;HTTP 错误进入 `catch`。
repeat(console.log, 5, 1000)
repeat(fn,5,1000):循环 5 次,每次await sleep(1000)后调用fn;明确是否应立即执行第一次,并返回可取消句柄。参考代码 · jsasync function repeat(fn, count, interval) { for (let i = 0; i < count; i++) { await new Promise(r => setTimeout(r, interval)); fn(i + 1); } } repeat(console.log, 5, 1000);结果:约每秒输出 `1` 到 `5`,首次输出在约 1 秒后。
封装一个工具函数输入一个promiseA返回一个promiseB如果超过1s没返回则抛出异常如果正常则输出正确的值
一秒超时:
Promise.race([promiseA, timeoutPromise])可让等待方超时,但不能中断原任务;若能控制请求,应传AbortSignal.timeout(1000)。参考代码 · jsfunction withTimeout(promise, ms = 1000) { let timer; return Promise.race([promise, new Promise((_, reject) => { timer = setTimeout(() => reject(new Error('timeout')), ms); })]).finally(() => clearTimeout(timer)); } withTimeout(Promise.resolve(3)).then(console.log);结果:输出 `3`;超时只终止等待,不取消原异步工作。
请求5s未完成就终止
五秒未完成终止:创建
AbortController,setTimeout(() => controller.abort(), 5000),传signal给fetch,finally清除定时器;不要只做Promise.race。参考代码 · jsasync function fetchWithin5s(url) { const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), 5000); try { return await fetch(url, {signal:controller.signal}); } finally { clearTimeout(timer); } }结果:5 秒未完成会取消 `fetch` 并以 AbortError 拒绝。
实现一个sleep函数
sleep(ms):new Promise(resolve => setTimeout(resolve, ms));如需取消,再监听AbortSignal并清除定时器。参考代码 · jsconst sleep = ms => new Promise(resolve => setTimeout(resolve, ms)); sleep(10).then(() => console.log('done'));结果:约 10 毫秒后输出 `done`。
js每隔一秒打印1,2,3,4,5
每秒打印 1 到 5:
for(let i=1;i<=5;i++){ await sleep(1000); console.log(i); },要立即输出 1 则把打印放到等待之前。参考代码 · jsasync function printOneToFive() { for (let i = 1; i <= 5; i++) { await new Promise(r => setTimeout(r, 1000)); console.log(i); } } printOneToFive();结果:每隔约 1 秒依次输出 `1,2,3,4,5`。
使用 setTimeout 实现 setInterval
setTimeout模拟setInterval:在回调结束后再次setTimeout(tick, interval);返回取消函数。此方式不会重叠,但间隔包含回调耗时。参考代码 · jsfunction intervalByTimeout(fn, ms) { let stopped = false, id; function tick() { if (stopped) return; fn(); id = setTimeout(tick, ms); } id = setTimeout(tick, ms); return () => { stopped = true; clearTimeout(id); }; } const stop = intervalByTimeout(() => console.log('tick'), 1000); setTimeout(stop, 3100);结果:约输出三次 `tick` 后停止。
promise实现图片异步加载
图片异步加载:
new Promise((resolve,reject)=>{const img=new Image(); img.onload=()=>resolve(img); img.onerror=reject; img.src=url;});先绑事件再设src。参考代码 · jsfunction loadImage(url) { return new Promise((resolve, reject) => { const image = new Image(); image.onload = () => resolve(image); image.onerror = () => reject(new Error('image load failed')); image.src = url; }); } loadImage('/logo.png').then(img => console.log(img.naturalWidth));结果:成功时输出图片自然宽度;失败时 Promise 拒绝。
使用Promise封装AJAX请求
Promise 封装 AJAX:创建
XMLHttpRequest,注册load/error/timeout/abort,open/send;只把允许的 HTTP 状态当成功,错误时拒绝,支持取消与超时。参考代码 · jsfunction ajax(url) { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.open('GET', url); xhr.onload = () => xhr.status >= 200 && xhr.status < 300 ? resolve(xhr.responseText) : reject(new Error(`HTTP ${xhr.status}`)); xhr.onerror = () => reject(new Error('network error')); xhr.send(); }); } ajax('/api').then(console.log).catch(console.error);结果:成功返回响应文本,网络错误或非 2xx 拒绝。
我们能反过来使用 setinterval 模拟实现 settimeout 吗?
setInterval模拟setTimeout:首次触发时立即clearInterval(id)再执行回调;可实现近似效果,但计时精度和页面节流仍受环境影响。参考代码 · jsfunction timeoutByInterval(fn, ms) { const id = setInterval(() => { clearInterval(id); fn(); }, ms); return () => clearInterval(id); } timeoutByInterval(() => console.log('once'), 1000);结果:约 1 秒后仅输出一次 `once`。
异步任务:依次发送3次网络请求,拿到服务器数据
三次串行请求:
for (const input of inputs) { const response = await fetch(...); results.push(await response.json()); };后一请求需要前一结果时用其结果构造参数。参考代码 · jsasync function serial(urls) { const out = []; for (const url of urls) { const response = await fetch(url); if (!response.ok) throw Error(`HTTP ${response.status}`); out.push(await response.json()); } return out; }结果:三个 URL 时顺序发三次请求,按请求顺序返回三项结果。
实现网络请求超时判断,超过三秒视为超时
三秒请求超时:
fetch(url,{signal:AbortSignal.timeout(3000)});区分超时、用户取消和 HTTP 非 2xx 响应。参考代码 · jsasync function timedRequest(url) { const response = await fetch(url, {signal:AbortSignal.timeout(3000)}); if (!response.ok) throw Error(`HTTP ${response.status}`); return response.json(); }结果:超过 3 秒会超时拒绝;2xx JSON 返回解析值。
promise中断请求
Promise 无通用“中断”;Promise 只是结果容器。请求取消要用
AbortController并把signal传给可取消的底层 API。参考代码 · jsconst controller = new AbortController(); const request = fetch('/api/slow', {signal:controller.signal}); controller.abort(); request.catch(error => console.log(error.name));结果:输出 `AbortError`;取消的是支持 AbortSignal 的请求。
给定一系列的api,测量上传速度(实现的时候用的GET请求)并选择一个加载时间最短的api
测量 API:对每个候选发独立请求,以
performance.now()差值计时,处理超时/错误,再按成功请求耗时选最小;GET 测的是延迟与下载,不等于上传速度。上传速度须用实际 POST/PUT 字节数除以耗时。参考代码 · jsasync function fastest(urls) { const times = await Promise.all(urls.map(async url => { const start = performance.now(); try { const response = await fetch(url, {cache:'no-store'}); if (!response.ok) throw Error(); await response.arrayBuffer(); return {url, ms:performance.now()-start}; } catch { return {url, ms:Infinity}; } })); return times.reduce((best, item) => item.ms < best.ms ? item : best); }结果:返回 GET 下载耗时最短的地址;这不等于上传速度。
settimeout系统补偿时间
补偿计时:记录计划执行时间
next += interval,每次使用setTimeout(tick, Math.max(0,next-performance.now()));后台节流无法完全补偿。参考代码 · jsfunction accurateInterval(fn, interval) { let next = performance.now() + interval, id, stopped = false; function tick() { if (stopped) return; fn(); next += interval; id = setTimeout(tick, Math.max(0,next-performance.now())); } id = setTimeout(tick, interval); return () => { stopped = true; clearTimeout(id); }; }结果:以绝对计划时间补偿单次回调耗时;后台节流仍可能延迟。
setTimeout准时
setTimeout无法保证“准时”;可按绝对时间重排定时器并在回调中计算漂移,主线程阻塞或浏览器节流时仍会迟到。参考代码 · jsconst target = performance.now() + 1000; setTimeout(() => console.log(Math.round(performance.now() - target)), 1000);结果:输出实际漂移毫秒数,通常非严格的 `0`。
并发多个请求,返回先得到response的。函数输入为url数组,输出为第一个返回的response的结果
并发请求取最先成功/最先完成需区分:
Promise.race取最先落定,Promise.any取最先成功;请求完成后可取消其他fetch。参考代码 · jsasync function firstResponse(urls) { const controllers = urls.map(() => new AbortController()); const stopOthers = i => controllers.forEach((c,j) => { if (j !== i) c.abort(); }); return Promise.race(urls.map((url, i) => fetch(url, {signal:controllers[i].signal}).then( response => { stopOthers(i); return response; }, error => { stopOthers(i); throw error; } ))); }结果:返回首个完成的响应;首个失败也会直接拒绝。
JS异步数据流,实现并发异步请求,结果顺序输出
并发发起、顺序输出:先
const promises=urls.map(request),再按顺序for (const p of promises) console.log(await p);提前为每项附拒绝处理,避免未处理拒绝。参考代码 · jsasync function ordered(urls) { const pending = urls.map(url => fetch(url).then(r => r.text()).then(value => ({value}), error => ({error}))); for (const task of pending) { const item = await task; if (item.error) throw item.error; console.log(item.value); } }结果:请求并发开始,打印顺序与 `urls` 一致。
Promise串行
Promise 串行:
tasks.reduce((p,task)=>p.then(()=>task()), Promise.resolve());tasks必须是函数,不能是已启动的 Promise。参考代码 · jsasync function runSerial(tasks) { const out = []; for (const task of tasks) out.push(await task()); return out; } runSerial([() => Promise.resolve(1), () => Promise.resolve(2)]).then(console.log);结果:输出 `[1,2]`;任务函数在轮到时才启动。
处理高并发, 100 条数据,带宽为 10, 跑满带宽
100 项、并发 10:维护最多 10 个 worker,各 worker 从共享下标领取下一任务,完成后继续领取;结果按输入下标存储,最后
await Promise.all(workers)。参考代码 · jsasync function limitAll(tasks, limit) { if (!Number.isInteger(limit) || limit < 1) throw RangeError('limit'); const out = Array(tasks.length); 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; } limitAll([() => Promise.resolve(1), () => Promise.resolve(2)], 10).then(console.log);结果:`[1,2]`;同时运行数不超过 `limit`。
设计一个简单的任务队列, 要求分别在 1,3,4 秒后打印出 "1", "2", "3";
1、3、4 秒后打印:如果是相对起点,分别设置三个
setTimeout;若指相邻间隔,则顺序await sleep(1000/3000/4000),需先确认题意。参考代码 · jsfor (const [ms, value] of [[1000,'1'],[3000,'2'],[4000,'3']]) { setTimeout(() => console.log(value), ms); }结果:相对开始时刻约 1、3、4 秒分别输出 `1`、`2`、`3`。
实现有并行限制的 Promise 调度器
并行限制调度器:队列保存任务函数,
active < limit时启动,任务在finally中递减active并启动下一项;add返回该任务结果 Promise。参考代码 · jsfunction scheduler(limit) { if (!Number.isInteger(limit) || limit < 1) throw RangeError('limit'); let active = 0; const queue = []; function drain() { while (active < limit && queue.length) { const {task,resolve,reject} = queue.shift(); active++; Promise.resolve().then(task).then(resolve,reject).finally(() => { active--; drain(); }); } } return task => new Promise((resolve,reject) => { queue.push({task,resolve,reject}); drain(); }); } const add = scheduler(2); add(() => Promise.resolve(3)).then(console.log);结果:输出 `3`;同时运行的任务最多 2 个。
实现 Scheduler
Scheduler:与上一题相同,核心是不超过并发上限、每项结果和错误独立传回,并处理空队列与同步抛错。
参考代码 · jsclass Scheduler { #queue=[]; #active=0; constructor(limit) { if(!Number.isInteger(limit)||limit<1)throw RangeError('limit');this.limit=limit; } add(task){return new Promise((resolve,reject)=>{this.#queue.push({task,resolve,reject});this.#drain();});} #drain(){while(this.#active<this.limit&&this.#queue.length){const {task,resolve,reject}=this.#queue.shift();this.#active++;Promise.resolve().then(task).then(resolve,reject).finally(()=>{this.#active--;this.#drain();});}} } new Scheduler(2).add(() => Promise.resolve('ok')).then(console.log);结果:`ok`;同时运行的任务最多 2 个。
有并发限制的Promise.all(ts类型)
TypeScript 版限流
all:签名可用function limitAll<T>(tasks: Array<() => Promise<T>>, limit:number): Promise<T[]>;校验limit > 0,worker 并发领取索引,按原顺序写结果,任一失败拒绝。参考代码 · tsasync function limitAll<T>(tasks: Array<() => Promise<T>>, limit: number): Promise<T[]> { if (!Number.isInteger(limit) || limit < 1) throw new RangeError('limit'); const out = new Array<T>(tasks.length); 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; }结果:最多 `limit` 项并发,返回值按输入顺序组成 `T[]`。
实现 如果上一次的没请求完,之后的就无响应
忽略后续请求:用
inFlight锁;若已在请求则直接返回既定结果(如undefined),否则try { return await request() } finally { inFlight=false }。需明确“无响应”是忽略还是复用当前 Promise。参考代码 · jsfunction ignoreWhilePending(request) { let busy = false; return async (...args) => { if (busy) return undefined; busy = true; try { return await request(...args); } finally { busy = false; } }; }结果:前次未完成时后续调用直接得到 `undefined`;前次结束后可重新请求。
使用 Promise 实现每隔三秒输出时间
每隔三秒输出时间:循环
await sleep(3000); console.log(new Date());提供停止标志或AbortSignal以释放定时器。参考代码 · jsasync function printTime(signal) { while (!signal.aborted) { await new Promise(r => setTimeout(r, 3000)); if (!signal.aborted) console.log(new Date().toISOString()); } }结果:约每 3 秒输出一次当前时间;示例停止后仍要等当前计时器结束。
使用 Promise 改写回调地狱
回调地狱:先把每个 Node 风格回调接口包装为 Promise,再用
async/await依赖顺序执行,并在try/catch统一处理失败。参考代码 · jsconst read = file => new Promise((resolve,reject) => require('node:fs').readFile(file,'utf8',(e,data) => e ? reject(e) : resolve(data))); async function load() { try { return await read('a.txt'); } catch (error) { console.error(error); throw error; } }结果:文件存在时返回文本,失败时记录并抛出错误。
设计一个函数,该函数的参数为可同时发送请求的大小,返回一个函数,该函数的参数为要请求的url。 实现的效果为,同时发送n个请求,当有请求返回后往请求队列里push新的请求,并输出刚刚结束的请求的返回值
返回限流请求函数:内部维护
queue和active;调用时把{url,resolve,reject}入队,空位立即发请求,完成后输出结果、落定调用方 Promise 并继续取队列。参考代码 · jsfunction requestQueue(limit) { if(!Number.isInteger(limit)||limit<1)throw RangeError('limit'); let active=0;const queue=[]; function drain(){while(active<limit&&queue.length){const {url,resolve,reject}=queue.shift();active++;fetch(url).then(r=>{if(!r.ok)throw Error(`HTTP ${r.status}`);return r.json();}).then(value=>{console.log(value);resolve(value);},reject).finally(()=>{active--;drain();});}} return url=>new Promise((resolve,reject)=>{queue.push({url,resolve,reject});drain();}); }结果:同时最多发 `limit` 个请求;每个完成时打印并返回该请求结果。
Promise.retry 超时重新请求,并在重试一定次数依然失败时输出缓存内容
重试与缓存:每次用独立超时信号请求,失败后最多重试 N 次,可指数退避;全部失败时返回事先存在的缓存,否则抛最后错误。非幂等写操作不能盲目重试。
参考代码 · jsasync function retry(task, attempts, fallback) { if (!Number.isInteger(attempts) || attempts < 1) throw RangeError('attempts'); let last; for (let i = 0; i < attempts; i++) { try { return await task(); } catch (error) { last = error; } } if (fallback !== undefined) return fallback; throw last; } retry(() => Promise.reject('offline'), 2, 'cached').then(console.log);结果:尝试两次后输出 `cached`;每次请求超时需在 `task` 内实现。
写一个 mySetInterVal(fn, a, b),每次间隔 a,a+b,a+2b 的时间,然后写一个 myClear,停止上面的 mySetInterVal
递增间隔:保存次数
n,每次执行后安排下一次a+n*b毫秒;myClear设置停止标志并clearTimeout(id)。首轮是等待a后执行。参考代码 · jsfunction mySetInterVal(fn, a, b) { let count = 0, stopped = false, id; function tick() { if (stopped) return; fn(); count++; id = setTimeout(tick, a + count*b); } id = setTimeout(tick, a); return () => { stopped = true; clearTimeout(id); }; } const myClear = mySetInterVal(() => console.log('tick'), 1000, 500);结果:约在 1、2.5、4.5 秒输出,调用 `myClear()` 停止。
JavaScript 常考手写题
产生一个不重复的随机数组
假设从
[min,max]取n个不重复整数:验证n <= max-min+1,可先建候选数组再 Fisher–Yates 洗牌并取前 n 项;避免无限随机重抽。参考代码 · jsfunction uniqueRandom(min, max, count) { const size = max - min + 1; if (!Number.isInteger(size) || !Number.isInteger(count) || count < 0 || count > size) throw RangeError('range/count'); const pool = Array.from({length:size}, (_,i) => min + i); for (let i = 0; i < count; i++) { const j = i + Math.floor(Math.random()*(size-i)); [pool[i],pool[j]] = [pool[j],pool[i]]; } return pool.slice(0,count); } console.log(uniqueRandom(1,3,3).sort());结果:一定包含 `1,2,3` 各一次;抽取顺序随机。
await async 如何实现
async/await可理解为把生成器的每一步包装成 Promise:next(value)得到{value,done},未结束时Promise.resolve(value).then(step,throwStep),结束时兑现返回值。参考代码 · jsfunction run(generator) { return new Promise((resolve,reject) => { const iterator = generator(); function step(method, value) { let state; try { state = iterator[method](value); } catch (error) { reject(error); return; } if (state.done) { resolve(state.value); return; } Promise.resolve(state.value).then(x => step('next',x), e => step('throw',e)); } step('next'); }); } run(function*(){ return (yield Promise.resolve(2)) + 1; }).then(console.log);结果:`3`;展示 async/await 的生成器驱动核心,不等于引擎内部实现。
使用递归完成 1 到 100 的累加
sum(n)=n<=0?0:n+sum(n-1),所以sum(100)=5050;实际大范围求和可用n*(n+1)/2避免栈溢出。参考代码 · jsfunction sum(n) { return n <= 0 ? 0 : n + sum(n-1); } console.log(sum(100));结果:`5050`。
打印出 1~10000 以内的对称数
遍历 1 到 10000,把数字转字符串并与反转字符串比较;“对称数”按十进制回文数解释,若 10000 包含与否按题目约定。
参考代码 · jsconst palindromes = Array.from({length:10000}, (_,i) => i+1) .filter(n => String(n) === [...String(n)].reverse().join('')); console.log(palindromes.slice(0,12));结果:前 12 项为 `1,2,3,4,5,6,7,8,9,11,22,33`。
实现一个字符串匹配算法indexOf
indexOf(text,pattern):外层枚举起点0..text.length-pattern.length,内层逐字符比较,首次全匹配返回起点;空模式返回 0,无匹配返回 -1。参考代码 · jsfunction indexOfText(text, pattern) { for (let i = 0; i <= text.length - pattern.length; i++) { let j = 0; while (j < pattern.length && text[i+j] === pattern[j]) j++; if (j === pattern.length) return i; } return -1; } console.log(indexOfText('banana','ana'));结果:`1`;空模式返回 `0`。
请实现一个模块 math,支持链式调用math.add(2,4).minus(3).times(2);
链式 math:对象持有当前值,
add/minus/times修改值并返回this,用value()取结果;示例需约定add(2,4)为初始加法,结果(2+4-3)*2=6。参考代码 · jsconst math = { current: 0, add(...values) { this.current += values.reduce((a,b)=>a+b,0); return this; }, minus(value) { this.current -= value; return this; }, times(value) { this.current *= value; return this; }, value() { return this.current; } }; console.log(math.add(2,4).minus(3).times(2).value());结果:`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)抛错。参考代码 · jsfunction negativeIndex(array) { return new Proxy(array, {get(target,key,receiver) { if (typeof key === 'string' && /^-[1-9]\d*$/.test(key)) { const i = target.length + Number(key); return i < 0 ? undefined : Reflect.get(target,String(i),receiver); } return Reflect.get(target,key,receiver); }}); } console.log(negativeIndex([10,20])[-1]);结果:`20`;Symbol 键正常传给 `Reflect.get`。
有一堆整数,请把他们分成三份,确保每一份和尽量相等(11,42,23,4,5,6 4 5 6 11 23 42 56 78 90)
三组尽量等和属于三路划分,通常是组合优化题。小数组可回溯搜索并剪枝,最小化三组和的最大差;大数组可按数值降序放入当前最小和组作近似,需说明不保证最优。
参考代码 · jsfunction threeGroups(numbers) { const groups = [[],[],[]], totals = [0,0,0]; for (const value of [...numbers].sort((a,b)=>b-a)) { const i = totals.indexOf(Math.min(...totals)); groups[i].push(value); totals[i] += value; } return {groups, totals}; } console.log(threeGroups([11,42,23,4,5,6]).totals);结果:示例为 `[42,23,26]`;这是贪心近似,不能保证最优。
之字形打印矩阵
假设按行“之”字打印:偶数行从左到右、奇数行从右到左;若题目指对角线之字遍历,需另定遍历方向。
参考代码 · jsfunction zigzagRows(matrix) { return matrix.flatMap((row,i) => i % 2 ? [...row].reverse() : row); } console.log(zigzagRows([[1,2],[3,4]]));结果:按行蛇形输出 `[1,2,4,3]`;对角线之字形需另一算法。
数组中的最大值
非空数组用
Math.max(...arr)或reduce((max,x)=>Math.max(max,x));大数组避免展开参数过多,空数组需约定返回值。参考代码 · jsfunction maximum(items) { if (!items.length) throw RangeError('empty'); return items.reduce((a,b) => a > b ? a : b); } console.log(maximum([3,8,2]));结果:`8`。
用尾递归实现斐波那契数列
尾递归
fib(n,a=0,b=1){return n===0?a:fib(n-1,b,a+b)};JS 引擎不普遍保证尾调用优化,大 n 建议改循环或BigInt。参考代码 · jsfunction fib(n, a = 0, b = 1) { return n === 0 ? a : fib(n-1,b,a+b); } console.log(fib(7));结果:`13`;大输入改循环,JS 不保证尾调用优化。
实现简单路由
简单路由:监听
popstate,调用history.pushState后主动渲染当前location.pathname对应视图;直达路径需服务器回退到index.html。参考代码 · jsconst routes = {'/': () => 'Home', '/about': () => 'About'}; function renderRoute() { document.querySelector('#app').textContent = (routes[location.pathname] ?? (()=>'404'))(); } function go(path) { history.pushState(null,'',path); renderRoute(); } addEventListener('popstate',renderRoute); renderRoute();结果:`go('/about')` 后 `#app` 显示 `About`;服务器需把直达路径回退到页面入口。
封装一个localstorage的setItem和getItem方法
setItem用JSON.stringify,getItem用JSON.parse;包装try/catch处理禁用存储、配额与坏 JSON,并约定缺失值为null。参考代码 · jsconst store = { set(key,value) { localStorage.setItem(key,JSON.stringify(value)); }, get(key) { const raw = localStorage.getItem(key); return raw === null ? null : JSON.parse(raw); } }; store.set('a',{n:1}); console.log(store.get('a'));结果:`{n:1}`;生产代码还要处理存储禁用、配额和损坏 JSON。
1-1000回文数
同第 4 题,限定 1 到 1000:
String(n) === [...String(n)].reverse().join('')。参考代码 · jsconst result = Array.from({length:1000},(_,i)=>i+1) .filter(n => String(n) === [...String(n)].reverse().join('')); console.log(result.at(-1));结果:`999`,是 1–1000 范围最后一个回文数。
随机生成字符串
若用于口令/令牌,用
crypto.getRandomValues取随机字节,并通过拒绝采样映射到字符表以避免取模偏差;Math.random仅适合非安全用途。参考代码 · jsfunction randomHex(bytes = 4) { const values = crypto.getRandomValues(new Uint8Array(bytes)); return [...values].map(x => x.toString(16).padStart(2,'0')).join(''); } console.log(randomHex(4).length);结果:`8`;具体字符每次随机变化。
判断一个字符串是否为驼峰字符串, judge('ByteDance','BD') -> true judge('Bytedance','BD') -> false
按大写字母提取缩写:
[...s].filter(ch=>/[A-Z]/.test(ch)).join('')===abbr;ByteDance得BD,Bytedance得B。此定义仅适用于 ASCII 驼峰。参考代码 · jsfunction judge(text, abbreviation) { return [...text].filter(ch => /[A-Z]/.test(ch)).join('') === abbreviation; } console.log(judge('ByteDance','BD'), judge('Bytedance','BD'));结果:`true false`。
压缩字符串
例如游程编码:连续相同字符写成“字符+次数”,
aaabb→a3b2;需约定数字或分隔符出现在原文时如何消歧。参考代码 · jsfunction runLength(text) { let out = ''; for (let i = 0; i < text.length;) { let j = i + 1; while (j < text.length && text[j] === text[i]) j++; out += text[i] + (j-i); i = j; } return out; } console.log(runLength('aaabb'));结果:`a3b2`;原文含数字时需加分隔符防歧义。
Map 场景题:按 categories 将商品归类,输出每个分类下的商品名称
原讨论给出商品
{name,categories}数组,目标是按分类反向归组。用Map<分类, 商品名称数组>,遍历每个商品的每个分类并追加名称,最后转为[{name:'热门',categories:['可乐','苹果']}, ...];分类首次出现的顺序可由Map保留。参考代码 · jsfunction groupProducts(products) { const groups = new Map(); for (const {name,categories} of products) for (const category of categories) { if (!groups.has(category)) groups.set(category,[]); groups.get(category).push(name); } return [...groups].map(([name,categories]) => ({name,categories})); } console.log(groupProducts([{name:'苹果',categories:['水果','热门']}]));结果:`[{name:'水果',categories:['苹果']},{name:'热门',categories:['苹果']}]`。
输入50a6we8y20x 输出50个a,6个we,8个y,20个x
需要给多字符片段定义分界,否则
50a6we8y20x有歧义。若约定“数字后直到下一段数字前全是重复片段”,用/\d+\D+/g分段,分别取数量和片段后repeat。参考代码 · jsfunction expand(text) { return (text.match(/\d+\D+/g) ?? []).map(part => { const [,count,word] = part.match(/^(\d+)(\D+)$/); return word.repeat(Number(count)); }).join(''); } console.log(expand('3a2we'));结果:`aaawewe`;格式非法时应拒绝,而不是悄悄忽略残余片段。
手写defineProperty
原讨论实际在探讨用
Object.defineProperty实现属性监听:遍历对象自有键,为每个键定义get收集依赖、set在值变化时通知订阅者,并递归监听新赋入的对象。需说明新增/删除属性和部分数组操作无法仅靠现有属性的 getter/setter 自动捕获;若题意是“复刻原生 defineProperty”,纯 JS 无法完全实现。参考代码 · jsfunction observe(object, onChange) { for (const key of Object.keys(object)) { let value = object[key]; Object.defineProperty(object,key,{enumerable:true,configurable:true, get() { return value; }, set(next) { if (!Object.is(value,next)) { value = next; onChange(key,next); } } }); } return object; } const state = observe({n:1}, console.log); state.n = 2;结果:输出 `n 2`;新增属性不会自动被监听。
String string 值一样返回true Object Object 返回true function function 都是声明的一个新的变量 返回false
String原始值按值相等;对象、函数按引用相等。两个不同对象即使结构相同也不相等;同一引用的对象或函数相等,故原题“Object Object 返回 true”要明确是否同一引用。参考代码 · jsconst x = {}, same = x; console.log('a' === 'a', x === same, {} === {}, (()=>{}) === (()=>{}));结果:`true true false false`;对象和函数按引用比较。
对输入的字符串:去除其中的字符'b';去除相邻的'a'和'c'
先删除所有
b,再反复删除相邻ac,直到稳定;若删除会产生新邻接,可用栈在线处理,每见c且栈顶为a则弹出。参考代码 · jsfunction removeBAndAC(text) { const stack = []; for (const ch of text) { if (ch === 'b') continue; if (ch === 'c' && stack.at(-1) === 'a') stack.pop(); else stack.push(ch); } return stack.join(''); } console.log(removeBAndAC('abacbc'));结果:空字符串 `''`;删除后新相邻的 `ac` 也会消除。
用一行代码,将数组中的字符串和字符串对象(new String(123))直接判定出来
arr.filter(x => typeof x === 'string' || x instanceof String);跨 realm 字符串对象时instanceof不可靠,可用Object.prototype.toString.call(x)==='[object String]'。参考代码 · jsconst values = ['x', new String('y'), 3, {}]; console.log(values.filter(x => typeof x === 'string' || Object.prototype.toString.call(x) === '[object String]'));结果:包含 `'x'` 和 `String {'y'}` 两项。
实现 before(n, fn):fn 最多执行 n 次,此后返回最后一次结果
按原讨论中的示例,闭包记录次数和最近结果;前 n 次调用执行
fn并保存结果,之后直接返回最近结果。注意这与 lodash 的before(n,fn)只执行前 n−1 次不同,必须按本题示例实现。参考代码 · jsfunction before(n, fn) { let count = 0, last; return function(...args) { if (count < n) { count++; last = fn.apply(this,args); } return last; }; } const f = before(2, x => x*2); console.log(f(1),f(2),f(3));结果:`2 4 4`;按原讨论示例执行前 n 次。
实现一下 console.log
原讨论中的实现是包装调用:
function myLog(...args){ console.log(...args) };若要求从零实现浏览器 DevTools 控制台,纯 JS 无法做到,需区分这两种题意。参考代码 · jsfunction myLog(...args) { console.log(...args); } myLog('hello', 2);结果:控制台输出 `hello 2`;只是调用包装器。
实现(5).add(3).minus(2)功能
可给
Number.prototype定义add/minus返回普通数,但污染全局原型且与装箱有关;更稳妥是num(5).add(3).minus(2).value(),结果 6。参考代码 · jsfunction num(value) { return {add(x){value+=x; return this;}, minus(x){value-=x; return this;}, value(){return value;}}; } console.log(num(5).add(3).minus(2).value());结果:`6`;避免污染 `Number.prototype`。
将十进制数字转为二进制数字字符串
非负安全整数可用
n.toString(2);手写时不断取n%2并整除 2,逆序拼接,0 返回'0'。负数的补码位宽需另外指定。参考代码 · jsfunction binary(n) { if (!Number.isSafeInteger(n) || n < 0) throw RangeError('n'); if (n === 0) return '0'; let out = ''; while (n) { out = (n % 2) + out; n = Math.floor(n/2); } return out; } console.log(binary(13));结果:`1101`。
封装remove child.remove()销毁自身
child.remove()已是标准 DOM API;兼容写法child.parentNode?.removeChild(child)。删除节点前应解除应用自行注册的外部监听或资源。参考代码 · jsfunction removeSelf(node) { node.parentNode?.removeChild(node); } const item = document.createElement('p'); document.body.append(item); removeSelf(item); console.log(item.isConnected);结果:`false`;现代浏览器也可直接 `item.remove()`。
字符串中字母的出现次数
遍历字符串,用
Map或对象计数;“字母”是否含 Unicode、是否区分大小写需约定。参考代码 · jsfunction countLetters(text) { const counts = new Map(); for (const ch of text) if (/[A-Za-z]/.test(ch)) counts.set(ch,(counts.get(ch) ?? 0)+1); return Object.fromEntries(counts); } console.log(countLetters('aAb!a'));结果:`{a:2,A:1,b:1}`;示例区分大小写,仅计 ASCII 字母。
输出一个等腰三角形
高度 n 的第 i 行输出
n-i个空格加2*i-1个星号;用repeat拼每行。参考代码 · jsfunction triangle(height) { return Array.from({length:height},(_,i) => ' '.repeat(height-i-1)+'*'.repeat(2*i+1)).join('\n'); } console.log(triangle(3));结果:输出三行:` *`、` ***`、`*****`。
实现一个函数a,使其奇数次调用时返回1,偶数次调用时返回2(不能使用全局变量)
闭包保存调用次数:
const a=(()=>{let n=0;return ()=>++n%2?1:2})();无需全局变量。参考代码 · jsconst a = (() => { let count = 0; return () => ++count % 2 ? 1 : 2; })(); console.log(a(),a(),a());结果:`1 2 1`。
从数组中找出最接近目标数的值
给定非空数组
values与目标数target,遍历并保留Math.abs(x-target)最小的元素;原讨论的例子目标为 37,[3,56,56,23,7,76,-2,345,45,76,3]的结果是 45。距离相同保留先出现者,空数组需另定返回或抛错。参考代码 · jsfunction nearest(values,target) { if (!values.length) throw RangeError('empty'); return values.reduce((best,x) => Math.abs(x-target) < Math.abs(best-target) ? x : best); } console.log(nearest([3,56,56,23,7,76,-2,345,45,76,3],37));结果:`45`;并列时保留先出现的值。
不用循环求和
若求 1 到 n,直接
n*(n+1)/2;若是任意数组且禁用循环语句,可用递归或reduce,但底层仍会遍历。参考代码 · jsfunction sumTo(n) { return n * (n+1) / 2; } console.log(sumTo(100));结果:`5050`;适用于 1 到 n 的整数求和。
连续赋值操作
a=b=c=1从右向左赋值;a.x = a = {}会先确定左侧引用,再计算右侧赋值,可能导致旧对象的x指向新对象。需给出具体表达式才能唯一推导。参考代码 · jslet a = {old:true}, b = a, c = null; a.x = a = {new:true}; console.log(a.new, b.x === a, c);结果:`true true null`;先固定旧对象的 `a.x` 引用,再执行右侧的 `a={...}`。
输入一串字符串,根据字符串求出每个字母的数量并返回结果对象。(数字为1时可省略
遍历字符串计频并返回对象,例
aab→{a:2,b:1};“数字为 1 可省略”暗示输出可能是编码字符串,原题格式不完整。参考代码 · jsfunction frequencies(text) { return [...text].reduce((out,ch) => (out[ch]=(out[ch]??0)+1,out),Object.create(null)); } console.log({...frequencies('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)因空位不会执行回调。参考代码 · jsconsole.log(Array(10).fill(1), Array.from({length:10},()=>1), [...Array(10)].map(()=>1));结果:三种写法都产生 10 个 `1`;`Array(10).map` 本身不会访问空位。
将 ['zm', 'za', 'b', 'lm', 'ln', 'k'] 按首字母分组,并将组内字符串排序
遍历字符串,以首字母为键收集到对象或
Map,再对每个分组调用sort();示例输出{b:['b'],k:['k'],l:['lm','ln'],z:['za','zm']}。原讨论已补充这一目标。参考代码 · jsfunction groupSorted(words) { const out = {}; for (const word of words) (out[word[0]] ??= []).push(word); for (const group of Object.values(out)) group.sort(); return out; } console.log(groupSorted(['zm','za','b','lm','ln','k']));结果:`{z:['za','zm'],b:['b'],l:['lm','ln'],k:['k']}`;对象键顺序按首次出现。
将升序整数数组 [0,1,2,4,5,7,13,15,16] 压缩为 ["0->2", "4->5", "7", "13", "15->16"]
输入必须是升序整数数组:扫描每段连续的
+1区间,记录起点和终点;单个数输出自身字符串,连续段输出起点->终点。给定输入应得到题目所示五段;空数组返回[],重复数如何处理需另定。参考代码 · jsfunction ranges(nums) { const out = []; for (let i = 0; i < nums.length;) { let j = i; while (j+1 < nums.length && nums[j+1] === nums[j]+1) j++; out.push(i === j ? String(nums[i]) : `${nums[i]}->${nums[j]}`); i = j+1; } return out; } console.log(ranges([0,1,2,4,5,7,13,15,16]));结果:`['0->2','4->5','7','13','15->16']`。
['ab', 'c', 'ab', 'd', 'c'] => ['ab1', 'c1' ,'ab2', 'd', 'c2']
先计每个值总出现次数,再遍历维护已出现次数;仅重复的值追加序号,得到题示结果。
参考代码 · jsfunction numberDuplicates(values) { const total = new Map(), seen = new Map(); values.forEach(x => total.set(x,(total.get(x)??0)+1)); return values.map(x => { seen.set(x,(seen.get(x)??0)+1); return total.get(x) > 1 ? `${x}${seen.get(x)}` : x; }); } console.log(numberDuplicates(['ab','c','ab','d','c']));结果:`['ab1','c1','ab2','d','c2']`。
移除空属性
过滤对象条目;“空”需定义。常见仅删
null/undefined/'':Object.fromEntries(Object.entries(obj).filter(([,v])=>v!==null&&v!==undefined&&v!==''))。参考代码 · jsfunction removeEmpty(object) { return Object.fromEntries(Object.entries(object).filter(([,value]) => value !== null && value !== undefined && value !== '')); } console.log(removeEmpty({a:0,b:'',c:null,d:false}));结果:`{a:0,d:false}`;“空”在此限定为 `null/undefined/''`。
判断两个对象是否相等
先定义浅比较还是深比较。深比较需处理类型、键集合、数组顺序、循环引用、
Date/Map/Set和原型;通常用配对WeakMap避免无限递归。参考代码 · jsfunction equal(a,b) { if (Object.is(a,b)) return true; if (!a || !b || typeof a !== 'object' || typeof b !== 'object') return false; const ak = Reflect.ownKeys(a), bk = Reflect.ownKeys(b); return ak.length === bk.length && ak.every(key => bk.includes(key) && equal(a[key],b[key])); } console.log(equal({x:[1,2]},{x:[1,2]}));结果:`true`;仅适用于无循环的普通对象和数组,其他内建类型需特判。
一个数组,找出每个数组元素右侧第一个比当前数大的数的下标,时间复杂度O(N)
单调递减栈保存未找到答案的下标;遍历新元素时反复弹出比它小的下标并填当前下标,剩余填 -1;时间 O(n)。
参考代码 · jsfunction nextGreaterIndexes(nums) { const out = Array(nums.length).fill(-1), stack = []; nums.forEach((value,i) => { while (stack.length && nums[stack.at(-1)] < value) out[stack.pop()] = i; stack.push(i); }); return out; } console.log(nextGreaterIndexes([2,1,3]));结果:`[2,2,-1]`,时间 O(n)。
寻找出现次数最多的三个标签
用
Map计频,再按次数降序、平局按字典序排序取前 3;数据量大可用容量 3 的最小堆。参考代码 · jsfunction topThree(tags) { const counts = new Map(); for (const tag of tags) counts.set(tag,(counts.get(tag)??0)+1); return [...counts].sort((a,b)=>b[1]-a[1] || a[0].localeCompare(b[0])).slice(0,3); } console.log(topThree(['a','b','a','c','c','a']));结果:`[['a',3],['c',2],['b',1]]`。
素数
n>1且不存在2..sqrt(n)的整数因子即素数;可跳过偶数,生成区间素数可用埃氏筛。参考代码 · jsfunction isPrime(n) { if (!Number.isInteger(n) || n < 2) return false; for (let d = 2; d*d <= n; d++) if (n%d === 0) return false; return true; } console.log([1,2,17,18].map(isPrime));结果:`[false,true,true,false]`。
实现日期格式化函数
用
Date取年月日时分秒,padStart(2,'0')补齐,按令牌替换;明确使用本地时区还是 UTC,避免把月份从 0 开始当成 1。参考代码 · jsfunction formatDate(date) { const pad = n => String(n).padStart(2,'0'); return `${date.getFullYear()}-${pad(date.getMonth()+1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`; } console.log(formatDate(new Date(2026,0,2,3,4)));结果:本地时区下输出 `2026-01-02 03:04`。
实现 jsonp
JSONP 仅能 GET:创建唯一全局回调名,拼入 URL 查询参数,插入
<script>;回调或出错/超时后清理脚本和全局函数。只能向可信源使用,响应会执行脚本。参考代码 · jsfunction jsonp(url, timeout = 5000) { return new Promise((resolve,reject) => { const name = `jsonp_${crypto.randomUUID().replaceAll('-','')}`; const script = document.createElement('script'); const timer = setTimeout(() => done(new Error('timeout')),timeout); function done(error,data) { clearTimeout(timer); script.remove(); delete window[name]; error ? reject(error) : resolve(data); } window[name] = data => done(null,data); script.onerror = () => done(new Error('load failed')); const target = new URL(url,location.href); target.searchParams.set('callback',name); script.src = target.href; document.head.append(script); }); }结果:服务端调用指定回调时返回数据;仅对可信脚本源使用。
将 www.baidu.taobao.com 按点号分段反转为 com.taobao.baidu.www
原讨论要求反转点号分隔的域名片段,而不是反转字符或 URL 路径:
www.baidu.taobao.com→com.taobao.baidu.www。可用split('.').reverse().join('.');若禁用这些方法,就从右向左扫描片段并拼接。参考代码 · jsfunction reverseDomain(host) { return host.split('.').reverse().join('.'); } console.log(reverseDomain('www.baidu.taobao.com'));结果:`com.taobao.baidu.www`。
解析 URL Params 为对象
new URL(url).searchParams解析,重复键可用getAll保留数组;URLSearchParams会处理百分号解码。不能把?a=1&a=2默默丢成单值。参考代码 · jsfunction queryObject(url) { const out = Object.create(null); for (const [key,value] of new URL(url).searchParams) { if (key in out) out[key] = Array.isArray(out[key]) ? [...out[key],value] : [out[key],value]; else out[key] = value; } return out; } console.log(queryObject('https://x.test/?a=1&a=2'));结果:`{a:['1','2']}`。
调用计数器(支持重置)
闭包持有计数,
count()增加并返回值,count.reset()将其设回初值;不暴露可随意修改的全局状态。参考代码 · jsfunction counter() { let n = 0; const count = () => ++n; count.reset = () => { n = 0; }; return count; } const c = counter(); console.log(c(),c()); c.reset(); console.log(c());结果:`1 2`,然后 `1`。
颜色生成
原题未说明颜色格式。若生成随机
#RRGGBB,从crypto.getRandomValues(new Uint8Array(3))取 3 字节,各自转两位十六进制后拼接;若仅装饰用途也可用Math.random。参考代码 · jsfunction randomColor() { return '#' + [...crypto.getRandomValues(new Uint8Array(3))] .map(byte => byte.toString(16).padStart(2,'0')).join(''); } console.log(/^#[0-9a-f]{6}$/.test(randomColor()));结果:`true`;具体颜色随机。
JavaScript怎么清空数组
arr.length=0会原地清空并影响同一数组的其他引用;arr=[]只是让当前变量指向新数组;splice(0)也会原地清空。参考代码 · jsconst list = [1,2], alias = list; list.length = 0; console.log(list,alias);结果:两个引用都看到空数组 `[]`;`list=[]` 只会改变一个变量的引用。
判断A、B数组的包含关系(值和数量),A属于B返回1,B属于A返回2,两者相等返回0,其他返回-1
用计数
Map比较每个值出现次数。两边计数完全相同返回 0;A 计数均不大于 B 返回 1;B 属于 A 返回 2;否则 -1。对象元素按引用比较。参考代码 · jsfunction relation(a,b) { const count = xs => xs.reduce((m,x) => m.set(x,(m.get(x)??0)+1),new Map()); const x = count(a), y = count(b); const subset = (p,q) => [...p].every(([key,n]) => n <= (q.get(key)??0)); const ab = subset(x,y), ba = subset(y,x); return ab && ba ? 0 : ab ? 1 : ba ? 2 : -1; } console.log(relation([1],[1,1]), relation([1,2],[2,1]));结果:`1 0`。
对象的合并
浅合并用
{...a,...b}或Object.assign({},a,b),后者覆盖同名键;深合并需单独规定数组、空值和循环引用策略,避免原型污染。参考代码 · jsconst a = {x:1,shared:'a'}, b = {y:2,shared:'b'}; console.log({...a,...b});结果:`{x:1,shared:'b',y:2}`;这是浅合并。
实现一个可无限延伸的数组
若题意是按需得到无限序列,可用生成器
function* numbers(){let i=0;while(true) yield i++},逐次next()而不一次存入内存;原讨论也有用 Proxy 为越界索引提供默认值的不同理解。JS 数组本身不能真正占用无限内存,仍需明确目标接口。参考代码 · jsfunction* integers() { let n = 0; while (true) yield n++; } const it = integers(); console.log(it.next().value,it.next().value,it.next().value);结果:`0 1 2`;按需产生值,不建立真正无限长数组。
将按空格分列、换行分行的多行字符串转为二维数组
原讨论的输入是以换行分行、连续空白分列的数字文本,可用
s.trim().split(/\r?\n/).map(line=>line.trim().split(/\s+/))得到字符串二维数组;若要数字,再对每项用Number转换。空输入需单独返回[]。参考代码 · jsfunction matrix(text) { if (!text.trim()) return []; return text.trim().split(/\r?\n/).map(line => line.trim().split(/\s+/)); } console.log(matrix('1 2\n3 4'));结果:`[['1','2'],['3','4']]`;要数字时再逐项 `Number`。
请实现一个通用的Array解构赋值
JS 已有
[a,b]=iterable;手写通用解构要遵守迭代协议、默认值、剩余元素和迭代器关闭规则。仅针对数组可按下标取值,但不等价于通用解构。参考代码 · jsfunction take(iterable,count) { const iterator=iterable[Symbol.iterator](),out=[]; try { for(let i=0;i<count;i++){const step=iterator.next();if(step.done)break;out.push(step.value);} } finally { iterator.return?.(); } return out; } console.log(take(new Set([10,20]),2));结果:`[10,20]`;这是按迭代协议提取前两项,默认值和剩余元素需按具体模式扩展。
数组合并
浅合并
[...a,...b]或a.concat(b);是否去重、是否深拷贝元素另行约定。参考代码 · jsconst a = [1,2], b = [3,4]; console.log([...a,...b]);结果:`[1,2,3,4]`,元素仍保持原引用。
数组交集,并集,差集
集合语义先用
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。参考代码 · jsfunction sets(a,b) { const A = new Set(a), B = new Set(b); return {intersection:[...A].filter(x=>B.has(x)), union:[...new Set([...A,...B])], difference:[...A].filter(x=>!B.has(x))}; } console.log(sets([1,1,2],[2,3]));结果:交集 `[2]`、并集 `[1,2,3]`、A−B `[1]`。
多维数组全排列
多维数组笛卡尔积:
arrays.reduce((acc,arr)=>acc.flatMap(prefix=>arr.map(x=>[...prefix,x])),[[]]);任一维为空则结果为空。参考代码 · jsfunction product(arrays) { return arrays.reduce((rows,items) => rows.flatMap(row => items.map(item => [...row,item])),[[]]); } console.log(product([[1,2],['a','b']]));结果:`[[1,'a'],[1,'b'],[2,'a'],[2,'b']]`。
判断对象是否存在循环引用
DFS 用“当前递归路径”
WeakSet检测回边;退出节点时删除。仅用全局 visited 会把共享引用误判为循环。参考代码 · jsfunction hasCycle(value, active = new WeakSet()) { if (!value || typeof value !== 'object') return false; if (active.has(value)) return true; active.add(value); const found = Reflect.ownKeys(value).some(key => hasCycle(value[key],active)); active.delete(value); return found; } const a = {}; a.self = a; console.log(hasCycle(a));结果:`true`;共享子对象但无回边时返回 `false`。
找出数组中最小的 k 个数,并保持它们在原数组中的相对顺序
先取最小 k 个数的多重计数,再原顺序遍历数组,遇到仍需的值就加入并减计数;排序阈值法时间 O(n log n),可用选择算法优化。
参考代码 · jsfunction smallestInOriginalOrder(values,k) { if (!Number.isInteger(k) || k < 0 || k > values.length) throw RangeError('k'); const need = new Map(); for (const x of [...values].sort((a,b)=>a-b).slice(0,k)) need.set(x,(need.get(x)??0)+1); return values.filter(x => { const n = need.get(x)??0; if (!n) return false; need.set(x,n-1); return true; }); } console.log(smallestInOriginalOrder([5,1,3,2],2));结果:`[1,2]`,保持原数组相对顺序。
逆对象扁平
假设把点路径键还原为嵌套对象:按
.分割路径逐层创建对象并赋值;要处理数组索引、路径冲突并拒绝__proto__/constructor/prototype。参考代码 · jsfunction unflatten(entries) { const out = Object.create(null); for (const [path,value] of Object.entries(entries)) { const keys = path.split('.'); if (keys.some(k => ['__proto__','prototype','constructor'].includes(k))) throw Error('unsafe key'); let node = out; for (const key of keys.slice(0,-1)) node = node[key] ??= Object.create(null); node[keys.at(-1)] = value; } return out; } console.log(unflatten({'a.b':1}).a.b);结果:`1`;数组索引和路径冲突未实现。
对象扁平化
DFS 遍历对象并把路径连接成
a.b键;数组可选择索引路径。循环引用、点号本身出现在键中、空对象都需约定表示法。参考代码 · jsfunction flattenObject(object, prefix = '', out = {}) { for (const [key,value] of Object.entries(object)) { const path = prefix ? `${prefix}.${key}` : key; if (value && typeof value === 'object' && !Array.isArray(value)) flattenObject(value,path,out); else out[path] = value; } return out; } console.log(flattenObject({a:{b:1}}));结果:`{'a.b':1}`;循环引用与键本身含点号需另定规则。
实现 执行一次的函数
once(fn)用闭包存called与result,第一次fn.apply(this,args),之后返回缓存值;若函数抛错,通常不标记成功,以便下次重试。参考代码 · jsfunction once(fn) { let called = false, result; return function(...args) { if (!called) { result = fn.apply(this,args); called = true; } return result; }; } const f = once(x => x*2); console.log(f(2),f(9));结果:`4 4`;同步抛错时可重试。
链式调用
方法返回
this或新包装对象以继续调用,最后用value()取结果;需明确是可变链还是不可变链。参考代码 · jsfunction chain(value) { return {add(n){value+=n;return this;},times(n){value*=n;return this;},value(){return value;}}; } console.log(chain(2).add(3).times(4).value());结果:`20`。
偏函数
偏函数先保存部分参数,调用时合并后续参数并以调用时
this调原函数;如partial(fn,...bound){return function(...rest){return fn.apply(this,[...bound,...rest])}}。参考代码 · jsfunction partial(fn,...bound) { return function(...rest) { return fn.apply(this,[...bound,...rest]); }; } console.log(partial((a,b,c)=>a+b+c,1,2)(3));结果:`6`。
实现管道函数
管道函数按从左到右执行:
fns.reduce((value,fn)=>fn(value),initial);要支持异步时用 Promise 链。参考代码 · jsfunction pipe(...functions) { return input => functions.reduce((value,fn)=>fn(value),input); } console.log(pipe(x=>x+1,x=>x*2)(3));结果:`8`,从左到右执行。
手写事件代理(委托)
在共同父节点监听事件,使用
event.target.closest(selector)找匹配子元素,并确认它仍属于父节点;适用于动态子元素,卸载时移除监听。参考代码 · jsfunction delegate(root,selector,handler) { const listener = event => { const target = event.target.closest?.(selector); if (target && root.contains(target)) handler(event,target); }; root.addEventListener('click',listener); return () => root.removeEventListener('click',listener); } const off = delegate(document.body,'button',(_,button)=>console.log(button.textContent));结果:点击 `body` 内的按钮会输出按钮文本;调用 `off()` 后停止。
数据类型判断
typeof识别基本值与函数,但typeof null==='object';对象具体类型可用Object.prototype.toString.call(value),跨 realm 和Symbol.toStringTag需注意。参考代码 · jsfunction typeOf(value) { return Object.prototype.toString.call(value).slice(8,-1).toLowerCase(); } console.log(typeOf(null),typeOf([]),typeOf(new Date()));结果:`null array date`;自定义 `Symbol.toStringTag` 可影响结果。
类数组转数组
Array.from(arrayLike)或Array.prototype.slice.call(arrayLike);前者同时支持可迭代对象并可映射。参考代码 · jsfunction toArray(arrayLike) { return Array.from(arrayLike); } console.log(toArray({0:'a',1:'b',length:2}));结果:`['a','b']`。
预加载
图片预加载可创建
Image并设src,关键资源可用<link rel="preload">;仅预加载确实马上会用的资源,避免争抢带宽。参考代码 · jsfunction preloadImage(url) { const image = new Image(); image.src = url; return image; } const image = preloadImage('/banner.webp');结果:浏览器开始加载图片;`image.complete` 可检查加载完成状态。
图片懒加载
优先原生
<img loading="lazy">;需自定义进入视口行为时用IntersectionObserver,加载后取消观察并处理失败占位。参考代码 · jsconst observer = new IntersectionObserver(entries => { for (const entry of entries) if (entry.isIntersecting) { const image = entry.target; image.src = image.dataset.src; observer.unobserve(image); } }); document.querySelectorAll('img[data-src]').forEach(image => observer.observe(image));结果:图片进入视口附近时设置 `src` 并取消观察。
数组去重
基本值去重
Array.from(new Set(arr));对象按引用去重,按字段去重用Map(key → item)并明确保留首项还是末项。参考代码 · jsfunction unique(values) { return [...new Set(values)]; } console.log(unique([1,1,NaN,NaN]));结果:`[1,NaN]`;对象按引用去重。
防抖 节流
防抖在连续触发停止一段时间后执行;节流在窗口内最多执行一次。实现时保存计时器/上次时间,提供
cancel,并处理首次与末次执行需求。参考代码 · jsfunction debounce(fn,ms) { let id; return (...args) => { clearTimeout(id); id=setTimeout(()=>fn(...args),ms); }; } function throttle(fn,ms) { let last=-Infinity; return (...args) => { const now=Date.now(); if(now-last>=ms){last=now;fn(...args);} }; }结果:防抖在停止触发后执行;节流在时间窗口内最多执行一次。
函数组合 compose redux-saga koa 洋葱模型
普通
compose(f,g)(x)=f(g(x))从右往左;Koa 洋葱模型是中间件await next()前后包裹下游,需防止next被调用两次。参考代码 · jsfunction compose(...fns) { return x => fns.reduceRight((value,fn)=>fn(value),x); } console.log(compose(x=>x+1,x=>x*2)(3));结果:`7`;Koa 洋葱模型的 `await next()` 需单独实现中间件调度。
sum(x,y)和sum(x)(y)
可让
sum(x,y)在第二参存在时直接返回和,否则返回y=>x+y;判断参数个数用arguments.length,避免把合法的0当作缺失。参考代码 · jsfunction sum(x,y) { return arguments.length === 2 ? x+y : next => x+next; } console.log(sum(2,3),sum(2)(3),sum(2,0));结果:`5 5 2`。
curry柯里化
柯里化把多参数函数拆成逐步接收参数的函数,收集到原函数形参个数后执行;默认参数、剩余参数会影响
fn.length,需约定完成条件。参考代码 · jsfunction curry(fn,...collected) { return (...args) => { const all = [...collected,...args]; return all.length >= fn.length ? fn(...all) : curry(fn,...all); }; } console.log(curry((a,b,c)=>a+b+c)(1)(2)(3));结果:`6`;默认参数和剩余参数会影响 `fn.length`。
设计模式相关
观察者模式
观察者模式:被观察者持有观察者集合,状态变化时逐个通知;与发布订阅相比,观察者通常直接订阅特定对象,发布订阅常通过事件中心解耦双方。退订和通知中抛错策略要明确。
参考代码 · jsclass Subject { #observers = new Set(); subscribe(fn) { this.#observers.add(fn); return () => this.#observers.delete(fn); } notify(value) { for (const fn of [...this.#observers]) fn(value); } } const subject = new Subject(); const off = subject.subscribe(console.log); subject.notify(3); off();结果:输出 `3` 一次。
树-场景题(hot)
DOM2JSON
DOM2JSON:递归读取
nodeType;文本节点输出文本,元素节点输出小写标签、选定属性和childNodes。避免读取事件处理器、私有数据或过大的 DOM 子树。参考代码 · jsfunction dom2json(node) { if (node.nodeType === Node.TEXT_NODE) return node.textContent; if (node.nodeType !== Node.ELEMENT_NODE) return null; return {type:node.tagName.toLowerCase(), attributes:Object.fromEntries([...node.attributes].map(a=>[a.name,a.value])), children:[...node.childNodes].map(dom2json).filter(x=>x!==null)}; } console.log(dom2json(document.createElement('div')).type);结果:`div`;文本节点返回文本。
计算目录树的深度
树深度:空树为 0,叶子为 1,
depth(node)=1+max(depth(child));极深树可用显式栈避免调用栈溢出。参考代码 · jsfunction depth(node) { return node ? 1 + Math.max(0,...(node.children??[]).map(depth)) : 0; } console.log(depth({children:[{children:[{}]}]}));结果:`3`。
树形结构获取路径名
路径名:DFS 时传入祖先路径数组,到目标节点返回
[...path,node.name];需要返回所有匹配时不要找到首项就终止。参考代码 · jsfunction findNames(nodes,id,path=[]) { for (const node of nodes) { const next = [...path,node.name]; if (node.id === id) return next; const found = findNames(node.children??[],id,next); if (found) return found; } return null; } console.log(findNames([{id:1,name:'A',children:[{id:2,name:'B'}]}],2));结果:`['A','B']`。
对象树遍历
对象树遍历:DFS 可用栈,BFS 可用队列;对每个节点处理后遍历
children,用WeakSet防循环。参考代码 · jsfunction walk(nodes,visit) { for (const node of nodes) { visit(node); walk(node.children??[],visit); } } walk([{name:'A',children:[{name:'B'}]}],node=>console.log(node.name));结果:依次输出 `A`、`B`;循环引用需额外检测。
获取树对象属性
“获取树对象属性”缺少目标结构。若按指定键收集属性,遍历节点并
result.push(node[key]),未定义值按需求跳过。参考代码 · jsfunction collect(nodes,key) { const out=[]; (function visit(items){ for(const node of items){ if(key in node) out.push(node[key]); visit(node.children??[]); } })(nodes); return out; } console.log(collect([{name:'A',children:[{name:'B'}]}],'name'));结果:`['A','B']`;这里假设要收集全树指定属性。
查找json中的children路径
查找
children路径:递归访问子节点,维护索引或名称路径;命中目标后返回路径,重复目标时需决定返回一个还是全部。参考代码 · jsfunction childPath(nodes,id,path=[]) { for (let i=0;i<nodes.length;i++) { const next=[...path,i]; if(nodes[i].id===id) return next; const found=childPath(nodes[i].children??[],id,next); if(found) return found; } return null; } console.log(childPath([{id:1,children:[{id:2}]}],2));结果:`[0,0]`,表示从根数组到 `children` 的索引路径。
对象字符串转化成树形结构
对象字符串转树:若输入是 JSON 字符串,先
JSON.parse,再验证节点结构;若是缩进文本,按缩进层级用栈构树。原题缺少样例,无法确定格式。参考代码 · jsfunction parseTreeJSON(text) { const value = JSON.parse(text); if (!Array.isArray(value)) throw TypeError('expected node array'); return value; } console.log(parseTreeJSON('[{"name":"A","children":[]}]')[0].name);结果:`A`;假设输入是 JSON 字符串,缩进文本需另一解析器。
判断有无符合路径和 -> 打印所有路径
DFS 累加沿途数值,命中目标和时输出当前路径;负数存在时不能仅凭当前和大于目标剪枝。
参考代码 · jsfunction pathsWithSum(root,target) { const out=[]; function visit(node,sum,path) { if(!node) return; const next=[...path,node.value], total=sum+node.value; if(!node.children?.length && total===target) out.push(next); for(const child of node.children??[]) visit(child,total,next); } visit(root,0,[]); return out; } console.log(pathsWithSum({value:1,children:[{value:2},{value:3}]},3));结果:`[[1,2]]`;这里统计根到叶的路径。
获取树结构中的name:getName
DFS 遍历每个节点,收集
name;若仅取目标节点名称,应先说明按 id、路径还是其他条件查找。参考代码 · jsfunction getNames(root) { return [root.name,...(root.children??[]).flatMap(getNames)]; } console.log(getNames({name:'A',children:[{name:'B'}]}));结果:`['A','B']`;假设题意是获取整棵树的名称。
实现 JS 原生方法
Array.prototype.forEach
forEach:转换接收者为对象,取长度快照,只访问存在的索引,调用callback.call(thisArg,value,index,object);返回undefined。参考代码 · jsfunction forEachLike(array,callback,thisArg) { if(typeof callback!=='function') throw TypeError('callback'); const obj=Object(array), len=obj.length>>>0; for(let i=0;i<len;i++) if(i in obj) callback.call(thisArg,obj[i],i,obj); } forEachLike([1,,3],console.log);结果:只回调索引 0 和 2,函数返回 `undefined`。
Array.prototype.filter
filter:同样跳过空位,回调为真时将原值追加到新数组;结果数组是密集的。参考代码 · jsfunction filterLike(array,callback,thisArg) { if(typeof callback!=='function') throw TypeError('callback'); const obj=Object(array), out=[], len=obj.length>>>0; for(let i=0;i<len;i++) if(i in obj && callback.call(thisArg,obj[i],i,obj)) out.push(obj[i]); return out; } console.log(filterLike([1,,2,3],x=>x>1));结果:`[2,3]`。
Array.prototype.reduce
reduce:有初始值从索引 0 开始;无初始值时查找首个存在的索引作为累加器,空数组且无初始值抛TypeError。参考代码 · jsfunction reduceLike(array,callback,...initial) { if(typeof callback!=='function') throw TypeError('callback'); const obj=Object(array), len=obj.length>>>0; let i=0, acc; if(initial.length) acc=initial[0]; else { while(i<len && !(i in obj)) i++; if(i===len) throw TypeError('empty'); acc=obj[i++]; } for(;i<len;i++) if(i in obj) acc=callback(acc,obj[i],i,obj); return acc; } console.log(reduceLike([1,,3],(a,b)=>a+b,0));结果:`4`;空数组无初值会抛 `TypeError`。
Array.prototype.fill
fill(value,start,end):把负索引换算成从末尾计,范围截在[0,length],逐个写入原数组并返回原数组;会填补空位。参考代码 · jsfunction fillLike(array,value,start=0,end=array.length) { const len=array.length, index=n=>Math.min(len,Math.max(0,n<0?len+n:n)); for(let i=index(start);i<index(end);i++) array[i]=value; return array; } console.log(fillLike([1,2,3],9,-2));结果:`[1,9,9]`,原数组也被修改。
Array.prototype.includes
includes:按SameValueZero比较,因此NaN可找到,+0/-0相等;空位按undefined处理,支持负起始索引。参考代码 · jsfunction includesLike(array,search,from=0) { const len=array.length; let i=from<0?Math.max(len+from,0):from; for(;i<len;i++){const value=array[i]; if(value===search || (value!==value && search!==search)) return true;} return false; } console.log(includesLike([,NaN],undefined),includesLike([NaN],NaN));结果:`true true`,空位按 `undefined` 比较。
Array.prototype.push
push(...items):从当前length起写入各项,更新长度并返回新长度;数组接近最大长度时可能抛错。参考代码 · jsfunction pushLike(array,...items) { for(const item of items) array[array.length]=item; return array.length; } const a=[1]; console.log(pushLike(a,2,3),a);结果:`3 [1,2,3]`。
Array.prototype.unshift
unshift(...items):从后向前移动旧元素,再写入新元素,返回新长度;需保留原有空位。参考代码 · jsfunction unshiftLike(array,...items) { const old=array.length, count=items.length; for(let i=old-1;i>=0;i--) { if(i in array) array[i+count]=array[i]; else delete array[i+count]; } for(let i=0;i<count;i++) array[i]=items[i]; return array.length; } const a=[2,3]; console.log(unshiftLike(a,1),a);结果:`3 [1,2,3]`。
Array.prototype.copy
标准数组原型没有
copy方法;可能指copyWithin:把数组内区间复制到目标位置,重叠时选择正确方向,原地修改并返回数组。参考代码 · jsfunction copyWithinLike(array,target,start,end=array.length) { const len=array.length, norm=n=>Math.min(len,Math.max(0,n<0?len+n:n)); let to=norm(target), from=norm(start), count=Math.min(norm(end)-from,len-to); const step=from<to && to<from+count ? -1 : 1; if(step<0){from+=count-1;to+=count-1;} while(count-->0){ if(from in array) array[to]=array[from]; else delete array[to]; from+=step; to+=step; } return array; } console.log(copyWithinLike([1,2,3,4],1,2));结果:`[1,3,4,4]`;题名 `copy` 非标准,此处按 `copyWithin` 解释。
Array.prototype.getLevel
标准数组原型没有
getLevel;若指数组嵌套深度,可递归求1+max(子数组深度),空数组/非数组的基准层数先约定。参考代码 · jsfunction getLevel(value) { return Array.isArray(value) ? 1 + Math.max(0,...value.map(getLevel)) : 0; } console.log(getLevel([1,[2,[3]]]));结果:`3`;此处约定最外层数组深度为 1。
实现es6的set集合
Set 可用
Map模拟增删查与迭代,值比较需用SameValueZero且维持插入顺序;直接使用对象键会把不同对象混为一谈。参考代码 · jsclass MiniSet { #items=new Map(); add(x){this.#items.set(x,true);return this;} has(x){return this.#items.has(x);} delete(x){return this.#items.delete(x);} get size(){return this.#items.size;} [Symbol.iterator](){return this.#items.keys();} } console.log([...new MiniSet().add(1).add(1).add(2)]);结果:`[1,2]`;用 `Map` 复用键身份和迭代顺序。
实现es6的map集合
Map 可用原生
Map作为说明其行为的参考;纯 JS 完整复刻需对象键身份、插入顺序、迭代器与删除再插入语义,不能只用普通对象代替。参考代码 · jsclass MiniMap { #entries=[]; #index(key){return this.#entries.findIndex(([k])=>Object.is(k,key) || k===key);} set(key,value){const i=this.#index(key);i<0?this.#entries.push([key,value]):this.#entries[i][1]=value;return this;} get(key){const i=this.#index(key);return i<0?undefined:this.#entries[i][1];} has(key){return this.#index(key)>=0;} delete(key){const i=this.#index(key);if(i<0)return false;this.#entries.splice(i,1);return true;} [Symbol.iterator](){return this.#entries[Symbol.iterator]();} } const key={};console.log(new MiniMap().set(key,3).get(key));结果:`3`;这是教学简化版,完整原生迭代和性能语义尚未覆盖。
String.prototype.zpadStart
zpadStart非标准方法;若意为左补零,String(value).padStart(targetLength,'0'),超过长度保持原字符串。参考代码 · jsfunction zpadStart(value,length){return String(value).padStart(length,'0');} console.log(zpadStart(12,5));结果:`00012`;`zpadStart` 不是标准方法。
Object.assign
Object.assign逐个源对象拷贝自身可枚举的字符串与 Symbol 键到目标,会触发 getter/setter,浅拷贝并返回目标;null/undefined源跳过。参考代码 · jsfunction assignLike(target,...sources){ if(target==null) throw TypeError('target'); const out=Object(target); for(const source of sources) if(source!=null) for(const key of Reflect.ownKeys(Object(source))) { const descriptor=Object.getOwnPropertyDescriptor(Object(source),key); if(descriptor.enumerable) out[key]=source[key]; } return out; } console.log(assignLike({}, {a:1},{b:2}));结果:`{a:1,b:2}`,为浅拷贝。
Object.is
Object.is类似严格相等,但NaN与自身相等,+0与-0不相等;可用x===y ? x!==0 || 1/x===1/y : x!==x && y!==y。参考代码 · jsfunction isLike(x,y){return x===y ? x!==0 || 1/x===1/y : x!==x && y!==y;} console.log(isLike(NaN,NaN),isLike(0,-0));结果:`true false`。
JSON.stringify
JSON.stringify要处理转义、数组和对象、toJSON、replacer、缩进、循环引用及undefined/BigInt等规则;不宜声称简单递归版等价于原生。参考代码 · jsfunction stringifySubset(value){ const seen=new WeakSet(); function encode(x){ if(x===null || typeof x==='boolean' || typeof x==='number') return JSON.stringify(x); if(typeof x==='string') return JSON.stringify(x); if(typeof x!=='object') return undefined; if(seen.has(x)) throw TypeError('cycle'); seen.add(x); const out=Array.isArray(x) ? `[${Array.from(x,v=>encode(v)??'null').join(',')}]` : `{${Object.keys(x).flatMap(k=>{const v=encode(x[k]);return v===undefined?[]:[`${JSON.stringify(k)}:${v}`]}).join(',')}}`; seen.delete(x); return out; } return encode(value); } console.log(stringifySubset({a:1,b:[true,null]}));结果:`{"a":1,"b":[true,null]}`;不覆盖 `toJSON`、replacer 等完整原生语义。
JSON.parse
JSON.parse需词法分析与递归下降解析字符串、数字、布尔、null、数组和对象,并拒绝非法语法;绝不使用eval解析不可信 JSON。参考代码 · jsfunction parseJSON(text){ let i=0; const space=()=>{while(/\s/.test(text[i]??'' ) && i<text.length)i++;}; function value(){ space();const ch=text[i]; if(ch==='"'){let end=i+1;while(end<text.length){if(text[end]==='\\'){end+=2;continue;}if(text[end++]==='"'){const raw=text.slice(i,end);i=end;return JSON.parse(raw);}}throw SyntaxError('string');} if(ch==='['){i++;const out=[];space();if(text[i]===']'){i++;return out;}do{out.push(value());space();if(text[i]===']'){i++;return out;}if(text[i++]!==',')throw SyntaxError('array');}while(true);} if(ch==='{'){i++;const out={};space();if(text[i]==='}'){i++;return out;}do{space();if(text[i]!=='"')throw SyntaxError('key');const key=value();space();if(text[i++]!==':')throw SyntaxError('colon');const item=value();Object.defineProperty(out,key,{value:item,enumerable:true,writable:true,configurable:true});space();if(text[i]==='}'){i++;return out;}if(text[i++]!==',')throw SyntaxError('object');}while(true);} for(const [word,item] of [['true',true],['false',false],['null',null]])if(text.startsWith(word,i)){i+=word.length;return item;} const number=text.slice(i).match(/^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?/); if(number){i+=number[0].length;return Number(number[0]);} throw SyntaxError('value'); } const out=value();space();if(i!==text.length)throw SyntaxError('trailing');return out; } console.log(parseJSON('{"a":[1,true,null]}').a);结果:`[1,true,null]`;字符串转义委托原生 `JSON.parse`,对象与数组由递归下降解析。
trim
trim:删除字符串两端的 ECMAScript 空白和换行字符,不改变中间内容;/^\s+|\s+$/g是常见近似实现。参考代码 · jsfunction trimLike(text){return String(text).replace(/^\s+|\s+$/g,'');} console.log(trimLike(' hi \n'));结果:`hi`;这是常见近似版。
实现new
new:创建以Constructor.prototype为原型的对象,用该对象调用构造函数;若构造函数返回对象/函数就采用返回值,否则返回新对象。箭头函数不能被new。参考代码 · jsfunction newLike(Constructor,...args){ if(typeof Constructor!=='function') throw TypeError('constructor'); const instance=Object.create(Constructor.prototype); const returned=Constructor.apply(instance,args); return returned!==null && (typeof returned==='object'||typeof returned==='function') ? returned : instance; } function Person(name){this.name=name;} console.log(newLike(Person,'Ada').name);结果:`Ada`;类构造函数不能通过 `.apply` 调用,完整语义应使用 `Reflect.construct`。
String.prototype.repeat
repeat(n):验证 n 为非负有限数并转整数,重复字符串 n 次;可用倍增拼接减少次数,过大输出应抛范围错误。参考代码 · jsfunction repeatLike(text,count){ if(!Number.isFinite(count)||count<0) throw RangeError('count'); let n=Math.trunc(count),chunk=String(text),out=''; while(n){if(n%2)out+=chunk;n=Math.floor(n/2);if(n)chunk+=chunk;} return out; } console.log(repeatLike('ab',3));结果:`ababab`。
String.prototype.includes
字符串
includes(search,position)可用indexOf(search,position)!==-1,但需拒绝正则入参,并处理位置转换与空串。参考代码 · jsfunction includesText(text,search,position=0){ if(search instanceof RegExp) throw TypeError('RegExp'); return String(text).indexOf(String(search),Math.max(0,Math.trunc(position)||0))!==-1; } console.log(includesText('hello','ell'));结果:`true`;跨 realm 正则和 `Symbol.match` 需完整原生语义处理。
JS 库函数实现
lodash.chunk
chunk(array,size):要求size为正整数,循环i += size并slice(i,i+size);空数组返回[],不修改输入。参考代码 · jsfunction chunk(array,size){ if(!Number.isInteger(size)||size<1) throw RangeError('size'); const out=[];for(let i=0;i<array.length;i+=size)out.push(array.slice(i,i+size));return out; } console.log(chunk([1,2,3,4,5],2));结果:`[[1,2],[3,4],[5]]`。
JS 工具函数
判断一个对象是否是isPlainObject
isPlainObject:先排除null和非对象,再检查原型为Object.prototype或null;跨 realm 对象需要更谨慎,不能仅看constructor === Object。参考代码 · jsfunction isPlainObject(value){ if(value===null||typeof value!=='object') return false; const proto=Object.getPrototypeOf(value); return proto===null||proto===Object.prototype; } console.log(isPlainObject({}),isPlainObject([]));结果:`true false`;跨 realm 需更完整判断。
手写 nodejs 模块
promisify
promisify(fn):返回函数,在原参数后追加 Node 风格(err,value)=>err?reject(err):resolve(value)回调;调用原函数时保留this,同步抛错也应拒绝。参考代码 · jsfunction promisify(fn){return function(...args){return new Promise((resolve,reject)=>{ fn.call(this,...args,(error,value)=>error?reject(error):resolve(value)); });};} const read=promisify(require('node:fs').readFile); read('package.json','utf8').then(text=>console.log(text.length));结果:文件存在时输出长度;失败时 Promise 拒绝。
正则相关
正则判断手机号,qq,颜色,邮箱
手机号、QQ、颜色、邮箱只能按明确业务规则写正则;例如 CSS 六位十六进制色
^#[0-9a-fA-F]{6}$。手机号号段会变化,邮箱完整规范不适合单个简短正则,优先格式初筛加实际验证。参考代码 · jsconst checks={qq:/^[1-9]\d{4,}$/,hexColor:/^#[0-9a-fA-F]{6}$/,email:/^[^\s@]+@[^\s@]+\.[^\s@]+$/}; console.log(checks.qq.test('12345'),checks.hexColor.test('#aBc123'),checks.email.test('[email protected]'));结果:`true true true`;手机号号段和邮箱有效性需按业务规则与实际验证处理。
字符串的大小写取反
遍历字符,将小写字母转大写、大写转小写,其他不变;只针对 ASCII 可用
/[a-zA-Z]/g。参考代码 · jsfunction swapCase(text){return text.replace(/[A-Za-z]/g,ch=>ch===ch.toLowerCase()?ch.toUpperCase():ch.toLowerCase());} console.log(swapCase('Ab1'));结果:`aB1`。
检验字符串首尾是否含有数字
^\d|\d$测首或尾为数字;若要求首尾都为数字,用^\d.*\d$并单独处理单字符数字。参考代码 · jsconst startsOrEndsWithDigit = text => /^\d|\d$/.test(text); console.log(startsOrEndsWithDigit('1a'),startsOrEndsWithDigit('a2'),startsOrEndsWithDigit('ab'));结果:`true true false`;这里是“首或尾”。
去除字符串空格
只去两端空格用
trim();删除全部空白可用replace(/\s/g,''),仅普通空格用/ /g。参考代码 · jsconsole.log(' a b '.trim(), ' a b '.replace(/\s/g,''));结果:分别是 `'a b'` 与 `'ab'`;前者只去两端。
去除字符串中的字母
ASCII 字母用
replace(/[A-Za-z]/g,'');Unicode 字母可考虑/\p{L}/gu,需目标环境支持。参考代码 · jsconsole.log('a1中B'.replace(/[A-Za-z]/g,''));结果:`1中`;仅删除 ASCII 字母。
正则 驼峰转换
驼峰转下划线:先处理连续大写缩写与单词边界,再
/([a-z0-9])([A-Z])/g插_并转小写;仅简单正则会把XMLHttp拆错。参考代码 · jsfunction snake(text){return text.replace(/([A-Z]+)([A-Z][a-z])/g,'$1_$2').replace(/([a-z0-9])([A-Z])/g,'$1_$2').toLowerCase();} console.log(snake('XMLHttpRequest'));结果:`xml_http_request`。
对象key的驼峰转下划线
递归转换对象键,同时保留数组结构;只处理自身属性,遇循环引用用
WeakMap,并约定键冲突时如何处理。参考代码 · jsfunction snakeKeys(value){ if(Array.isArray(value)) return value.map(snakeKeys); if(value===null||typeof value!=='object') return value; return Object.fromEntries(Object.entries(value).map(([key,item])=>[snake(key),snakeKeys(item)])); } function snake(text){return text.replace(/([A-Z]+)([A-Z][a-z])/g,'$1_$2').replace(/([a-z0-9])([A-Z])/g,'$1_$2').toLowerCase();} console.log(snakeKeys({userName:'Ada'}));结果:`{user_name:'Ada'}`;循环引用需加 `WeakMap`。
判断字符串中是否存在连续的三个数
若指三个连续数字字符,用
/\d{3}/;若指数值连续递增如123,需逐位比较相邻数字差是否为 1。参考代码 · jsconst hasThreeDigits = text => /\d{3}/.test(text); console.log(hasThreeDigits('a123b'),hasThreeDigits('a12b'));结果:`true false`;这里指连续三个数字字符,不要求数值递增。
((2+3)+(3*4))+2---->['(2 + 3)+(3 * 4)', '2 + 3', '3 * 4']
用栈匹配左右括号,记录每对括号对应的原串范围,从内到外抽取;只用正则难以处理任意嵌套层数,输出的空格格式另定。
参考代码 · jsfunction innerParentheses(text){ const stack=[],out=[]; for(let i=0;i<text.length;i++){ if(text[i]==='(')stack.push(i); else if(text[i]===')'){if(!stack.length)throw Error('unbalanced');const start=stack.pop();out.push(text.slice(start,i+1));} } if(stack.length)throw Error('unbalanced');return out; } console.log(innerParentheses('((2+3)+(3*4))+2'));结果:`['(2+3)','(3*4)','((2+3)+(3*4))']`;输出格式与原题样例不同,需先确认括号保留规则。
排序算法
冒泡排序
冒泡:反复比较相邻元素,逆序则交换;一轮无交换可提前结束。平均/最坏 O(n²),稳定。
参考代码 · jsfunction bubble(a){a=[...a];for(let end=a.length-1;end>0;end--){let changed=false;for(let i=0;i<end;i++)if(a[i]>a[i+1]){[a[i],a[i+1]]=[a[i+1],a[i]];changed=true;}if(!changed)break;}return a;} console.log(bubble([3,1,2]));结果:`[1,2,3]`。
选择排序
选择:每轮找到剩余元素最小值并与当前位置交换;O(n²),通常不稳定。
参考代码 · jsfunction selection(a){a=[...a];for(let i=0;i<a.length;i++){let min=i;for(let j=i+1;j<a.length;j++)if(a[j]<a[min])min=j;[a[i],a[min]]=[a[min],a[i]];}return a;} console.log(selection([3,1,2]));结果:`[1,2,3]`。
快速排序
快排:选枢轴,把小于与大于枢轴的元素分区,再递归两边;平均 O(n log n),最坏 O(n²),原地版通常不稳定。
参考代码 · jsfunction quick(a){if(a.length<2)return [...a];const [pivot,...rest]=a;return [...quick(rest.filter(x=>x<pivot)),pivot,...quick(rest.filter(x=>x>=pivot))];} console.log(quick([3,1,2]));结果:`[1,2,3]`;示例用额外数组,非原地快排。
插入排序
插入:依次把当前元素插入前面已排序部分;O(n²),近乎有序时接近 O(n),稳定。
参考代码 · jsfunction insertion(a){a=[...a];for(let i=1;i<a.length;i++){const value=a[i];let j=i-1;while(j>=0&&a[j]>value){a[j+1]=a[j];j--;}a[j+1]=value;}return a;} console.log(insertion([3,1,2]));结果:`[1,2,3]`。
归并排序
归并:二分递归排序后合并两个有序序列;O(n log n),额外 O(n) 空间,可稳定。
参考代码 · jsfunction mergeSort(a){if(a.length<2)return [...a];const mid=a.length>>1,left=mergeSort(a.slice(0,mid)),right=mergeSort(a.slice(mid)),out=[];let i=0,j=0;while(i<left.length&&j<right.length)out.push(left[i]<=right[j]?left[i++]:right[j++]);return [...out,...left.slice(i),...right.slice(j)];} console.log(mergeSort([3,1,2]));结果:`[1,2,3]`,相等元素优先取左边保持稳定。
基数排序
基数:按个位、十位等从低到高做稳定分桶;适用于整数或固定编码键,时间与位数及基数有关,负数需分开处理。
参考代码 · jsfunction radix(a){if(a.some(x=>!Number.isSafeInteger(x)||x<0))throw RangeError('nonnegative integers only');let out=[...a];for(let place=1;out.some(x=>x>=place);place*=10){const buckets=Array.from({length:10},()=>[]);for(const x of out)buckets[Math.floor(x/place)%10].push(x);out=buckets.flat();}return out;} console.log(radix([21,3,12]));结果:`[3,12,21]`;仅处理非负安全整数。
计数排序
计数:统计有限整数范围内各值频次,前缀和可用于稳定输出;时间 O(n+值域),值域大时不适合。
参考代码 · jsfunction counting(a){if(!a.length)return [];const min=Math.min(...a),max=Math.max(...a);if(!a.every(Number.isInteger)||max-min>1e6)throw RangeError('range');const counts=Array(max-min+1).fill(0);for(const x of a)counts[x-min]++;return counts.flatMap((n,i)=>Array(n).fill(i+min));} console.log(counting([3,1,2,1]));结果:`[1,1,2,3]`;大值域不适合该实现。
希尔排序
希尔:按递减 gap 对各组做插入排序,最后 gap=1;性能依赖 gap 序列,通常不稳定。
参考代码 · jsfunction shell(a){a=[...a];for(let gap=Math.floor(a.length/2);gap>0;gap=Math.floor(gap/2))for(let i=gap;i<a.length;i++){const x=a[i];let j=i;while(j>=gap&&a[j-gap]>x){a[j]=a[j-gap];j-=gap;}a[j]=x;}return a;} console.log(shell([3,1,2]));结果:`[1,2,3]`。
桶排序
桶:把值映射到若干桶,桶内排序后依次拼接;分布均匀时较快,偏斜时可能退化,需明确桶边界。
参考代码 · jsfunction bucket(a){if(!a.length)return [];const buckets=Array.from({length:10},()=>[]);for(const x of a){if(x<0||x>=1)throw RangeError('[0,1) only');buckets[Math.floor(x*10)].push(x);}return buckets.flatMap(b=>b.sort((x,y)=>x-y));} console.log(bucket([0.31,0.12,0.25]));结果:`[0.12,0.25,0.31]`;此例只支持 `[0,1)`。
堆排序
堆:建最大堆后反复把堆顶换到末尾并下沉;O(n log n)、额外 O(1) 空间、不稳定。
参考代码 · jsfunction heapSort(a){a=[...a];const down=(start,end)=>{let root=start;while(root*2+1<end){let child=root*2+1;if(child+1<end&&a[child]<a[child+1])child++;if(a[root]>=a[child])break;[a[root],a[child]]=[a[child],a[root]];root=child;}};for(let i=Math.floor(a.length/2)-1;i>=0;i--)down(i,a.length);for(let end=a.length-1;end>0;end--){[a[0],a[end]]=[a[end],a[0]];down(0,end);}return a;} console.log(heapSort([3,1,2]));结果:`[1,2,3]`。
自定义 Hooks 手写题
Hooks怎么封装手势逻辑
手势 Hook 在
pointerdown/move/up/cancel记录起点、位移和时间,计算方向/速度,设阈值避免误触;在 effect 清理监听,并考虑触屏滚动冲突与键盘替代操作。参考代码 · tsxfunction useSwipe(onSwipe: (direction:'left'|'right')=>void) { const start = React.useRef<number|null>(null); return { onPointerDown:(e:React.PointerEvent)=>{ start.current=e.clientX; }, onPointerUp:(e:React.PointerEvent)=>{ if(start.current!==null && Math.abs(e.clientX-start.current)>40) onSwipe(e.clientX<start.current?'left':'right'); start.current=null; }, onPointerCancel:()=>{start.current=null;} }; }结果:横向移动超过 40px 时回调 `left` 或 `right`;需按实际设备补速度和滚动冲突处理。
Hooks实现移动端的滑动轮播插件
轮播 Hook 管理当前索引、拖动位移和动画状态;手势结束按距离/速度切页,索引循环,自动播放计时器在悬停、失焦及卸载时暂停/清理。
参考代码 · tsxfunction useCarousel(length:number){ const [index,setIndex]=React.useState(0); const next=()=>setIndex(i=>(i+1)%length); const prev=()=>setIndex(i=>(i-1+length)%length); return {index,next,prev}; } // const {index,next,prev}=useCarousel(images.length);结果:`next()`/`prev()` 在 `0..length-1` 循环;要求 `length>0`,完整移动端轮播需再接手势和动画。
如何用 Hooks 模拟 componentDidMount 与componentWillUnmount
useEffect(()=>{ setup(); return cleanup; },[])对应挂载后的副作用和清理;开发模式 Strict Mode 可能执行“建立→清理→建立”检查,不能假设只运行一次。参考代码 · tsxReact.useEffect(() => { console.log('setup'); return () => console.log('cleanup'); }, []);结果:挂载后建立、卸载时清理;开发 Strict Mode 可能额外执行一次建立和清理。
实现一个useBodyScrollLock ,当出现弹窗时 阻止背景滚动
弹窗打开时在 effect 保存
body.style.overflow原值并设hidden,清理时恢复;多个弹窗并存需共享锁计数,移动端还要处理滚动穿透与焦点管理。参考代码 · tsxfunction useBodyScrollLock(open:boolean){ React.useEffect(()=>{ if(!open)return; const previous=document.body.style.overflow; document.body.style.overflow='hidden'; return ()=>{document.body.style.overflow=previous;}; },[open]); }结果:单弹窗打开时背景滚动锁定,关闭或卸载时恢复;多弹窗需共享锁计数。
ts 实现 hooks: useInterval
useInterval(callback,delay)用 ref 保存最新回调,effect 随delay创建/清除 interval;delay=null暂停,避免闭包读到旧状态。参考代码 · tsxfunction useInterval(callback:()=>void,delay:number|null){ const latest=React.useRef(callback); React.useEffect(()=>{latest.current=callback;},[callback]); React.useEffect(()=>{if(delay===null)return;const id=setInterval(()=>latest.current(),delay);return ()=>clearInterval(id);},[delay]); }结果:按 `delay` 调用最新回调;传 `null` 暂停。
实现 useQuery
useQuery需先定义是读取 URL 参数还是数据查询。URL 场景可订阅路由变化并从URLSearchParams派生值;数据场景需缓存键、请求状态和失效规则。参考代码 · tsxfunction useQuery(name:string){ const [value,setValue]=React.useState(()=>new URLSearchParams(location.search).get(name)); React.useEffect(()=>{const update=()=>setValue(new URLSearchParams(location.search).get(name));addEventListener('popstate',update);return ()=>removeEventListener('popstate',update);},[name]); return value; }结果:读取当前 URL 查询参数,并响应浏览器前进/后退;这里把 `useQuery` 解释为 URL 查询 Hook。
实现useRequest
useRequest至少返回{data,error,loading,run,cancel};每次请求持有AbortController或版本号,卸载/新请求时防止旧结果覆盖,处理错误与重复提交。参考代码 · tsxfunction useRequest<T>(load:(signal:AbortSignal)=>Promise<T>){ const [state,setState]=React.useState<{data?:T;error?:Error;loading:boolean}>({loading:false}); const controller=React.useRef<AbortController|null>(null); const run=React.useCallback(async()=>{controller.current?.abort();const c=new AbortController();controller.current=c;setState({loading:true});try{const data=await load(c.signal);if(!c.signal.aborted)setState({data,loading:false});}catch(error){if(!c.signal.aborted)setState({error:error as Error,loading:false});}},[load]); React.useEffect(()=>()=>controller.current?.abort(),[]); return {...state,run,cancel:()=>controller.current?.abort()}; }结果:`run()` 更新加载、成功或错误状态;新请求或卸载取消旧请求。
组件设计题(Vue/React/JS均可)
全选
全选:源状态是已选 id 集合;全选取当前可选项 id,反选移除它们;“部分选择”状态从数量派生,并设置原生 checkbox 的
indeterminate。参考代码 · tsxfunction SelectAll({ids}:{ids:string[]}){ const [selected,setSelected]=React.useState<Set<string>>(new Set()); const all=ids.length>0 && ids.every(id=>selected.has(id)); return <><input type="checkbox" aria-label="全选" checked={all} onChange={()=>setSelected(all?new Set():new Set(ids))}/>{ids.map(id=><label key={id}><input type="checkbox" checked={selected.has(id)} onChange={()=>setSelected(old=>{const next=new Set(old);next.has(id)?next.delete(id):next.add(id);return next;})}/>{id}</label>)}</>; }结果:勾选全选选中所有 id,再次点击清空;部分选择的 `indeterminate` 需用 ref 设置。
轮播图
轮播图:索引、上一张/下一张、自动播放和交互暂停;按钮可键盘操作,有可访问名称;动画尊重减少动态效果偏好。
参考代码 · tsxfunction Carousel({images}:{images:string[]}){ const [index,setIndex]=React.useState(0); return <div><button onClick={()=>setIndex(i=>(i-1+images.length)%images.length)}>上一张</button><img src={images[index]} alt={`第 ${index+1} 张`}/><button onClick={()=>setIndex(i=>(i+1)%images.length)}>下一张</button></div>; }结果:点击按钮在图片列表中循环切换;需保证 `images` 非空。
根据response渲染table
表格:校验响应结构,列配置决定渲染,稳定 id 作 key;提供加载、空、错误、分页状态,不将不可信字段当 HTML 注入。
参考代码 · tsxfunction DataTable({rows}:{rows:Array<{id:string;name:string}>}){ return <table><thead><tr><th scope="col">名称</th></tr></thead><tbody>{rows.map(row=><tr key={row.id}><td>{row.name}</td></tr>)}</tbody></table>; }结果:逐行显示 `name`,React 默认转义文本;加载、空、错误、分页由外层状态管理。
歌词滚动功能(hot)
歌词滚动:解析时间戳并按时间排序,用二分查找当前播放时间对应行;只在行变化时滚到可视区域,用户手动滚动时暂停自动跟随。
参考代码 · jsfunction lyricAt(lines,seconds){ let left=0,right=lines.length-1,result=-1; while(left<=right){const mid=(left+right)>>1;if(lines[mid].time<=seconds){result=mid;left=mid+1;}else right=mid-1;} return result; } console.log(lyricAt([{time:0,text:'A'},{time:5,text:'B'}],6));结果:`1`,表示当前高亮第二行;滚动时只在索引变化时调用 `scrollIntoView`。
实现一个 百度搜索框 (搜索提示)
搜索提示:输入防抖,给每次请求版本号或取消前次请求,确保只展示最新结果;键盘上下/回车/Escape、空结果与
combobox/listbox语义要完整。参考代码 · tsxfunction Suggest({search}:{search:(q:string,signal:AbortSignal)=>Promise<string[]>}){ const [query,setQuery]=React.useState(''),[items,setItems]=React.useState<string[]>([]); React.useEffect(()=>{if(!query.trim()){setItems([]);return;}const c=new AbortController();const id=setTimeout(()=>search(query,c.signal).then(setItems).catch(e=>{if(e.name!=='AbortError')setItems([]);}),250);return ()=>{clearTimeout(id);c.abort();};},[query,search]); return <><input role="combobox" aria-expanded={items.length>0} value={query} onChange={e=>setQuery(e.target.value)}/><ul role="listbox">{items.map(x=><li role="option" aria-selected="false" key={x}>{x}</li>)}</ul></>; }结果:输入停止 250ms 后搜索;旧请求被取消。完整组合框还需键盘导航和焦点管理。
实现todos
Todos:任务 id、文本、完成状态为核心状态;支持增删改查与过滤,持久化时处理坏数据;列表项用稳定 id。
参考代码 · tsxfunction Todos(){ const [items,setItems]=React.useState<Array<{id:string;text:string;done:boolean}>>([]),[text,setText]=React.useState(''); return <><form onSubmit={e=>{e.preventDefault();if(text.trim())setItems(old=>[...old,{id:crypto.randomUUID(),text,done:false}]);setText('');}}><input value={text} onChange={e=>setText(e.target.value)}/><button>添加</button></form><ul>{items.map(item=><li key={item.id}><label><input type="checkbox" checked={item.done} onChange={()=>setItems(old=>old.map(x=>x.id===item.id?{...x,done:!x.done}:x))}/>{item.text}</label><button onClick={()=>setItems(old=>old.filter(x=>x.id!==item.id))}>删除</button></li>)}</ul></>; }结果:可添加、切换完成和删除任务;编辑和持久化仍需补充。
计时器或倒计时组件
倒计时:记录结束时刻,用
Date.now()差值计算剩余,而不是每秒减 1;卸载清理 timer,切后台后回来仍能校准。参考代码 · tsxfunction Countdown({end}:{end:number}){ const [remaining,setRemaining]=React.useState(()=>Math.max(0,end-Date.now())); React.useEffect(()=>{const id=setInterval(()=>setRemaining(Math.max(0,end-Date.now())),250);return ()=>clearInterval(id);},[end]); return <output>{Math.ceil(remaining/1000)} 秒</output>; }结果:显示剩余秒数;后台恢复后根据结束时间校准。
设计一个公会入驻信息提交页
入驻提交页:把字段分组、必填与格式验证、保存草稿、提交中禁重复、失败可恢复;上传材料和隐私告知依业务要求设计。
参考代码 · tsxfunction JoinForm({submit}:{submit:(name:string)=>Promise<void>}){ const [name,setName]=React.useState(''),[busy,setBusy]=React.useState(false),[error,setError]=React.useState(''); return <form onSubmit={async e=>{e.preventDefault();if(!name.trim()||busy)return;setBusy(true);setError('');try{await submit(name.trim());}catch(err){setError(String(err));}finally{setBusy(false);}}}><label>公会名称<input required value={name} onChange={e=>setName(e.target.value)}/></label><button disabled={busy}>{busy?'提交中':'提交'}</button>{error&&<p role="alert">{error}</p>}</form>; }结果:必填校验、提交中禁重复、失败提示;完整入驻字段与隐私文案需按业务规则补齐。
编码实现宽高不相同图片的自适配排列
不同尺寸图片:保留原始宽高比,可用 CSS Grid/Masonry 或按原尺寸计算布局;图片预留尺寸避免 CLS,懒加载并限制单张最大宽度。
参考代码 · html<style>.gallery{display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:12px}.gallery img{width:100%;height:auto;display:block}</style> <div class="gallery"><img src="a.jpg" width="800" height="600" alt="示例一"><img src="b.jpg" width="600" height="900" alt="示例二"></div>结果:不同宽高图片在网格中保持比例;宽高属性帮助预留空间。
列表子元素顺序反转
反转列表顺序:若只改变显示顺序用
[...items].reverse(),不要原地改 props;交互和屏幕阅读顺序应与视觉顺序一致,谨慎使用flex-direction:row-reverse。参考代码 · tsxfunction Reversed({items}:{items:Array<{id:string;text:string}>}){ return <ul>{[...items].reverse().map(item=><li key={item.id}>{item.text}</li>)}</ul>; }结果:视觉和 DOM 顺序都反转,原始 `items` 不变。
遍历树组件
树组件:递归节点组件或扁平可见节点列表,按 id 管理展开/选中;大树用虚拟列表,键盘上下/左右导航和
aria-expanded要支持。参考代码 · tsxtype TreeNode={id:string;name:string;children?:TreeNode[]}; function Tree({nodes}:{nodes:TreeNode[]}){ return <ul role="tree">{nodes.map(node=><li role="treeitem" key={node.id}>{node.name}{node.children?.length?<Tree nodes={node.children}/>:null}</li>)}</ul>; }结果:递归显示节点;完整树组件还需展开状态与箭头键导航。
选项卡
选项卡:管理 active id,按钮用
role=tab、内容用tabpanel,箭头键切换焦点并支持 Home/End;切换时决定是否保留面板状态。参考代码 · tsxfunction Tabs({tabs}:{tabs:Array<{id:string;label:string;content:string}>}){ const [active,setActive]=React.useState(tabs[0]?.id); return <><div role="tablist">{tabs.map(tab=><button role="tab" aria-selected={active===tab.id} key={tab.id} onClick={()=>setActive(tab.id)}>{tab.label}</button>)}</div>{tabs.map(tab=>active===tab.id?<div role="tabpanel" key={tab.id}>{tab.content}</div>:null)}</>; }结果:点击标签切换面板;完整无障碍模式需加焦点和方向键处理。
拖拽
拖拽:原生 Drag and Drop 或 Pointer Events;更新列表必须依据稳定 id,提供键盘移动方式、拖拽预览、取消和越界处理。
参考代码 · tsxfunction MoveList({items}:{items:string[]}){ const [order,setOrder]=React.useState(items); function move(from:number,to:number){setOrder(old=>{const next=[...old];next.splice(to,0,...next.splice(from,1));return next;});} return <ul>{order.map((x,i)=><li key={x}>{x}<button disabled={i===0} onClick={()=>move(i,i-1)}>上移</button></li>)}</ul>; }结果:可用键盘操作的按钮重排列表;指针拖拽需再接 Pointer Events。
HTML CSS 手写题
实现圆形环状进度条
环形进度条可用 SVG
<circle>的stroke-dasharray/stroke-dashoffset按比例绘制;提供文本百分比与role="progressbar"、aria-valuenow/min/max,不要只靠颜色表达进度。参考代码 · html<svg viewBox="0 0 120 120" role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="75" aria-label="完成进度"><circle cx="60" cy="60" r="50" fill="none" stroke="#ddd" stroke-width="10"/><circle cx="60" cy="60" r="50" fill="none" stroke="#315ac7" stroke-width="10" stroke-dasharray="314.16" stroke-dashoffset="78.54" transform="rotate(-90 60 60)"/><text x="60" y="66" text-anchor="middle">75%</text></svg>结果:绘制约 75% 的环形进度并提供可读文本。
JavaScript 与浏览器原理
事件循环中,宏任务、微任务和浏览器绘制的执行顺序是什么?
一轮事件循环取一个任务执行,随后清空微任务队列,再有机会更新渲染;实际调度由宿主环境决定,不能认为每个宏任务之后必定绘制。
参考代码 · jsconsole.log('sync'); setTimeout(()=>console.log('task'),0); Promise.resolve().then(()=>console.log('microtask'));结果:依次输出 `sync`、`microtask`、`task`;浏览器是否在两个任务间绘制由宿主决定。
持续追加微任务为什么会让页面无法响应输入?如何处理长任务?
微任务会在渲染前持续清空,递归追加可能使输入和绘制饿死;把大工作分片并主动让出主线程,可用任务队列、
scheduler.yield()(有兼容性处理)或 Worker。参考代码 · jsasync function work(items){ for(let i=0;i<items.length;i+=100){items.slice(i,i+100).forEach(expensive);await new Promise(r=>setTimeout(r,0));} }结果:每处理 100 项后让出主线程,给输入和绘制机会;`expensive` 是项目实际工作函数。
闭包如何形成?在实际项目中怎样排查闭包导致的内存占用?
函数保留创建处词法环境即形成闭包;内存排查用 Heap Snapshot 比较保留对象与引用链,检查未清理监听、计时器和长生命周期缓存。
参考代码 · jsfunction makeCounter(){let count=0;return ()=>++count;} const counter=makeCounter();console.log(counter(),counter());结果:`1 2`;返回函数保留了创建处的 `count`,长生命周期引用可用 Heap Snapshot 排查。
原型链、class 和继承之间是什么关系?
class是基于原型的语法;实例沿[[Prototype]]链查找方法,extends建立实例和构造器继承链,方法通常放在原型而非每个实例上。参考代码 · jsclass Parent{greet(){return 'hi';}} class Child extends Parent{} const child=new Child();console.log(child.greet(),Object.getPrototypeOf(Child.prototype)===Parent.prototype);结果:`hi true`,方法沿原型链找到。
箭头函数与普通函数的 this、arguments 和构造行为有何不同?
箭头函数词法绑定
this,无自己的arguments,不能作构造函数;普通函数的this由调用方式决定,可用new,有自己的arguments。参考代码 · jsconst owner={n:2,arrow:()=>this?.n,regular(){return this.n;}}; console.log(owner.regular(),owner.regular.call({n:3}));结果:`2 3`;箭头函数的 `this` 取决于定义处环境,不因 `.call` 改变。
ESM 与 CommonJS 的加载、导出和循环依赖行为有何区别?
ESM 静态声明、实时绑定且支持异步模块;CommonJS 运行时
require、导出对象,循环依赖可能得到尚未初始化的部分导出。两者互操作依运行环境。参考代码 · js// a.mjs: export let count=0; export function inc(){count++} // b.mjs: import {count,inc} from './a.mjs'; inc(); console.log(count);结果:ESM 的 `b.mjs` 输出 `1`,因为导入是实时绑定;CommonJS 模块需在 Node 环境另写 `require` 示例。
浏览器从输入 URL 到页面可交互,经历了哪些关键步骤?
URL 解析后检查缓存/解析 DNS,建立连接与 TLS,发请求收响应;浏览器解析 HTML/CSS、加载关键资源、布局绘制并执行影响交互的脚本。真实顺序含并行与缓存分支。
参考代码 · jsperformance.getEntriesByType('navigation').forEach(n=>console.log({dns:n.domainLookupEnd-n.domainLookupStart,connect:n.connectEnd-n.connectStart,response:n.responseEnd-n.responseStart}));结果:输出当前导航的 DNS、连接、响应阶段耗时;缓存命中时某些阶段为 0。
浏览器如何构建 DOM/CSSOM,并完成布局、绘制与合成?
HTML 构建 DOM、CSS 构建 CSSOM,再形成渲染树;计算样式与布局、绘制图层并合成。样式/几何变化可能触发重算或重排,transform 常只需合成。
参考代码 · jsconst box=document.querySelector('#box'); box.style.transform='translateX(20px)';结果:元素平移;可在 DevTools Performance 面板观察是否只需合成,具体由浏览器实现决定。
如何用 DevTools 定位内存泄漏、长任务和频繁重排?
Performance 面板看长任务和 Layout,Memory Heap Snapshot 看保留链,Performance Monitor 看节点/堆增长;用可复现步骤和前后对比确认原因。
参考代码 · jsnew PerformanceObserver(list=>{for(const entry of list.getEntries())console.log(entry.name,entry.duration);}).observe({type:'longtask',buffered:true});结果:浏览器支持时输出长任务名称与耗时;内存泄漏另用 Heap Snapshot 查保留链。
localStorage、sessionStorage、IndexedDB 和 Cookie 应如何选择?
Cookie 随请求发送,适合少量会话数据并设安全属性;localStorage 持久同步,sessionStorage 按标签页会话,IndexedDB 异步且适合较大结构化数据。敏感令牌避免暴露给脚本。
参考代码 · jslocalStorage.setItem('theme','dark'); sessionStorage.setItem('draft','hello'); console.log(localStorage.getItem('theme'),sessionStorage.getItem('draft'));结果:`dark hello`;大结构化数据改用 IndexedDB,服务端会话通常用安全 Cookie。
TypeScript
any、unknown 和 never 分别适用于什么场景?
any跳过检查,仅边界迁移时谨慎用;unknown是安全的未知值,使用前须收窄;never表示不可能值,可做穷尽检查。参考代码 · tsfunction label(value:unknown){if(typeof value==='string')return value.toUpperCase();return 'unknown';} function assertNever(value:never):never{throw new Error(String(value));} console.log(label('ok'));结果:运行输出 `OK`;`unknown` 用前需收窄,`never` 用于不可能分支。
如何用类型守卫和判别联合安全地收窄联合类型?
用
typeof、in、自定义谓词或判别字段收窄联合;判别联合给每种分支不同字面量kind,避免直接断言类型。参考代码 · tstype Shape={kind:'circle';radius:number}|{kind:'square';side:number}; function area(shape:Shape){if(shape.kind==='circle')return Math.PI*shape.radius**2;return shape.side**2;} console.log(area({kind:'square',side:3}));结果:`9`;判别字段将类型收窄到方形分支。
如何用 never 检查联合类型的分支是否穷尽?
switch(value.kind)的default中写const unreachable: never = value;新增联合成员时编译器会指出遗漏分支。参考代码 · tstype Result={kind:'ok';value:number}|{kind:'error';message:string}; function show(result:Result){switch(result.kind){case 'ok':return result.value;case 'error':return result.message;default:{const unreachable:never=result;return unreachable;}}}结果:新增 `kind` 分支但未处理时,`never` 赋值产生编译错误。
泛型约束、条件类型和 infer 如何用于设计可复用 API?
T extends Constraint限制泛型能力;条件类型按类型关系选择分支,infer从匹配形状提取内部类型,例如从函数提取返回值。参考代码 · tstype Return<F>=F extends (...args:any[])=>infer R?R:never; function first<T extends {id:string}>(items:T[]):T|undefined{return items[0];} type Value=Return<()=>number>; // number结果:`first([{id:'a'}])` 返回该对象;缺少 `id` 的输入编译失败。
实现 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]}。原生工具类型还涉及一些细节,面试应说明边界。参考代码 · tstype MyPartial<T>={[K in keyof T]?:T[K]}; type MyPick<T,K extends keyof T>={[P in K]:T[P]}; type User={id:number;name:string}; const patch:MyPartial<User>={name:'Ada'}; const identity:MyPick<User,'id'>={id:1};结果:两段声明可通过类型检查;未知键会报错。
interface 与 type 在扩展和声明合并方面有何区别?
interface可声明合并且方便扩展对象形状;type可表达联合、交叉、元组、条件类型等,不能同名声明合并。二者多数对象类型场景可互用。参考代码 · tsinterface User{ id:number } interface User{ name:string } // 声明合并 type Id=number|string; const user:User={id:1,name:'Ada'};结果:`User` 同时要求 `id` 与 `name`;`type` 可表达联合类型。
如何为异步请求建模成功、加载、失败三种状态,避免非法状态?
用判别联合:
{status:'loading'} | {status:'success',data:T} | {status:'error',error:Error};只有成功分支才有 data,避免多个独立布尔量形成非法组合。参考代码 · tstype Load<T>={status:'loading'}|{status:'success';data:T}|{status:'error';error:Error}; const state:Load<number>={status:'success',data:3}; if(state.status==='success')console.log(state.data);结果:输出 `3`;加载分支不能意外读取 `data`。
为什么 Vite 能转换 TypeScript,却仍需要单独执行类型检查?
Vite 开发时用转译器去掉 TS 类型以求速度,不执行完整语义类型检查;CI 仍需
tsc --noEmit或vue-tsc --noEmit。参考代码 · json{"scripts":{"build":"vite build","typecheck":"tsc --noEmit","verify":"npm run typecheck && npm run build"}}结果:`verify` 先做完整类型检查,再构建;Vite 单独转译不等于类型检查。
HTML、CSS 与无障碍
Flexbox 与 Grid 分别适合哪些布局?实现响应式卡片网格。
Flex 适合单轴排布,Grid 适合二维行列;响应式卡片可用
grid-template-columns:repeat(auto-fit,minmax(min(100%,16rem),1fr))。参考代码 · css.cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,16rem),1fr));gap:1rem} .card-actions{display:flex;align-items:center;gap:.5rem}结果:卡片二维响应式排布,卡片内操作用单轴 Flex。
BFC 是什么?它如何影响外边距折叠和浮动?
BFC 是独立的块格式化上下文,内部浮动参与高度、与外部浮动布局隔离,内部外边距与外部不折叠;可用
display:flow-root创建。参考代码 · html<style>.container{display:flow-root}.float{float:left;width:40px;height:40px}</style> <div class="container"><div class="float"></div>正文</div>结果:容器高度包含内部浮动元素,并形成独立 BFC。
如何避免字体、图片和异步内容造成布局偏移?
图片声明宽高或
aspect-ratio,字体预加载并考虑替换指标,异步区域预留空间;用真实 CLS 数据找移动元素。参考代码 · html<img src="hero.webp" width="1200" height="600" alt="展示图"> <div style="min-height:160px" aria-busy="true">加载中…</div>结果:图片和异步区域预留空间,减少布局偏移。
如何实现暗色模式,并兼顾系统偏好与用户手动选择?
用
prefers-color-scheme作默认值、用户设置作覆盖并持久化;保证对比度,更新color-scheme让原生控件匹配。参考代码 · css:root{color-scheme:light dark;--bg:#fff;--fg:#111} @media(prefers-color-scheme:dark){:root{--bg:#111;--fg:#fff}} :root[data-theme="light"]{--bg:#fff;--fg:#111;color-scheme:light} :root[data-theme="dark"]{--bg:#111;--fg:#fff;color-scheme:dark} body{background:var(--bg);color:var(--fg)}结果:默认跟随系统;设置 `data-theme` 可覆盖系统偏好。
如何让弹窗具备正确的焦点管理、键盘操作和语义?
优先原生
<dialog>;打开时聚焦合适元素,限制焦点在模态内容内,Escape 可关闭,关闭后回到触发按钮,并标注标题。参考代码 · html<button id="open" onclick="dialog.showModal()">打开弹窗</button> <dialog id="dialog" aria-labelledby="title"><h2 id="title">设置</h2><form method="dialog"><button>关闭</button></form></dialog>结果:原生模态对话框支持 Escape 和焦点约束;复杂场景仍要确认关闭后焦点回到触发按钮。
何时使用原生 HTML 元素,何时才需要 ARIA 属性?
原生按钮、链接、输入自带键盘和语义,优先使用;只有原生语义无法表达时补 ARIA,ARIA 不会自动添加交互行为。
参考代码 · html<button type="button" onclick="save()">保存</button> <a href="/help">帮助</a>结果:按钮和链接自带键盘与语义;`save` 为应用中的保存函数。
如何验证表单错误提示对键盘用户和屏幕阅读器可用?
标签关联控件,错误文本用
aria-describedby关联,提交后聚焦错误摘要或首个错误;仅颜色变化不足够,用键盘和屏幕阅读器实测。参考代码 · html<label for="email">邮箱</label><input id="email" type="email" aria-describedby="email-error" aria-invalid="true"> <p id="email-error" role="alert">请输入有效邮箱</p>结果:输入与标签和错误文本关联;实际提交后还需聚焦错误位置。
如何实现宽屏到移动端的自适应布局,而不依赖固定设备宽度?
用内容驱动的 Grid/Flex、相对单位与容器查询,让内容自然换行;窄视口、缩放到 200% 和长文案都需检查溢出。
参考代码 · css.page{container-type:inline-size} .layout{display:grid;grid-template-columns:minmax(0,16rem) minmax(0,1fr);gap:1rem} @container (max-width:40rem){.layout{grid-template-columns:1fr}}结果:`.layout` 的父容器 `.page` 宽度小于 40rem 时改为单列。
网络、缓存与安全
HTTP 强缓存与协商缓存如何配合?Cache-Control 和 ETag 分别解决什么问题?
Cache-Control指定可缓存性与新鲜期,命中强缓存可不发请求;过期后带If-None-Match与 ETag 协商,未变则 304。带哈希的静态资源适合长期缓存。参考代码 · httpGET /app.abc123.js HTTP/1.1 HTTP/1.1 200 OK Cache-Control: public, max-age=31536000, immutable ETag: "abc123"结果:新鲜期内可用本地强缓存;过期后带 `If-None-Match` 可协商得到 304。
CORS 为什么由浏览器执行?预检请求在什么情况下出现?
CORS 是浏览器同源策略的一部分,服务器通过响应头授予跨源读取权限;非简单方法/头或某些内容类型会先发 OPTIONS 预检,服务器间请求不受浏览器 CORS 限制。
参考代码 · httpOPTIONS /api/data HTTP/1.1 Origin: https://app.example Access-Control-Request-Method: POST HTTP/1.1 204 No Content Access-Control-Allow-Origin: https://app.example Access-Control-Allow-Methods: POST结果:浏览器在跨源非简单请求前预检,许可后才发实际 POST。
从 DNS、TCP/TLS 到 HTTP 响应,HTTPS 请求的关键步骤是什么?
先解析 DNS、建立传输连接(TCP 或 QUIC)、TLS 验证证书并协商密钥,然后发送 HTTP 请求与接收响应;可复用连接并有缓存/CDN 分支。
参考代码 · jsconst n=performance.getEntriesByType('navigation')[0]; console.log({dns:n.domainLookupEnd-n.domainLookupStart,tls:n.connectEnd-n.secureConnectionStart,ttfb:n.responseStart-n.requestStart});结果:输出当前导航部分阶段耗时;复用连接或缓存时可能为 0。
HTTP/2 与 HTTP/3 对前端资源加载有什么影响?
HTTP/2 多路复用和头压缩减少请求开销,但单 TCP 丢包仍可能影响流;HTTP/3 基于 QUIC,可减少部分队头阻塞及连接迁移成本。仍应控制资源大小和优先级。
参考代码 · jsconst response=await fetch('/api'); console.log(response.ok); // DevTools Network 的 Protocol 列可查看 h2 / h3;JS fetch 语义通常不因此变化。结果:输出请求是否成功;协议版本需在网络面板或服务端协商信息中确认。
SSE、WebSocket 与普通 HTTP 请求应如何选择?
普通 HTTP 适合一次性请求;SSE 适合服务端单向事件流与自动重连;WebSocket 适合持续双向低延迟通信,需处理心跳与断线恢复。
参考代码 · jsconst stream=new EventSource('/events');stream.onmessage=e=>console.log(e.data); const socket=new WebSocket('wss://example.com/ws');socket.onopen=()=>socket.send('ping');结果:SSE 单向接收事件,WebSocket 可双向发送;普通 `fetch` 适合单次请求。
如何实现 Token 刷新时的并发去重和失败请求重放?
用单个
refreshPromise让并发 401 共享刷新;刷新成功后重放可安全重试的请求并防无限循环,失败则清理登录态。请求体不可重放时需特殊处理。参考代码 · jslet refreshPromise=null; async function api(url,options={}){ let response=await fetch(url,options); if(response.status!==401)return response; refreshPromise??=fetch('/auth/refresh',{method:'POST',credentials:'include'}).then(r=>{if(!r.ok)throw Error('refresh failed');}).finally(()=>{refreshPromise=null;}); await refreshPromise; response=await fetch(url,options);return response; }结果:并发 401 共用一次刷新;重放非幂等请求前需确认请求体可重放和权限状态。
如何防止 XSS?为何只设置 CSP 仍不够?
默认对不可信文本做上下文编码,避免
innerHTML,对允许的 HTML 用可信清洗库;CSP 是纵深防御,不能替代正确编码和 DOM API 使用。参考代码 · jsconst message='<img src=x onerror=alert(1)>'; document.querySelector('#message').textContent=message;结果:页面显示字面文本,不执行 `onerror`;需要富文本时用受信任清洗器。
Cookie 的 HttpOnly、Secure 和 SameSite 分别解决什么问题?
HttpOnly阻止 JS 读取 Cookie,Secure要求 HTTPS 传输,SameSite限制跨站请求携带;仍需考虑 CSRF、XSS 与会话过期。参考代码 · httpSet-Cookie: session=opaque; HttpOnly; Secure; SameSite=Lax; Path=/结果:脚本不能读取该 Cookie;仅经 HTTPS 发送,并限制部分跨站携带场景。
CSRF 的触发条件是什么?前后端如何配合防护?
站外页面诱导浏览器携带受害者凭据发送有副作用请求,即可形成 CSRF;用 SameSite、CSRF token、Origin/Referer 检查及避免 GET 改数据共同防护。
参考代码 · js// 服务端伪代码:有副作用请求校验 Origin 和 CSRF token。 if(req.method==='POST' && (req.headers.origin!==allowedOrigin || req.headers['x-csrf-token']!==session.csrfToken)){ res.statusCode=403;res.end('Forbidden'); }结果:不满足同源来源和有效 token 的提交返回 403;服务端仍需安全管理 token。
如何设计上传大文件的分片、断点续传、校验和重试?
客户端切块并记录上传 id、分片索引和校验值,服务端幂等接收、查缺补漏与最终合并校验;失败只重传缺失块,限制并发并支持取消。
参考代码 · jsasync function uploadParts(file,uploadId,chunkSize=1024*1024){ for(let start=0,index=0;start<file.size;start+=chunkSize,index++){ const part=file.slice(start,start+chunkSize); const response=await fetch(`/upload/${uploadId}/${index}`,{method:'PUT',body:part}); if(!response.ok)throw Error(`part ${index} failed`); } }结果:按索引顺序上传分片;断点续传需先查询服务器已收到的分片并校验最终文件。
React 与 React 19
React 的 render 与 commit 阶段分别做什么?状态更新何时触发重新渲染?
render 阶段计算下一棵 UI,可被中断/重试;commit 阶段把变化提交到 DOM 并运行相关 effect。状态更新安排重新渲染,同值更新可能被跳过。
参考代码 · tsxfunction Counter(){const [n,setN]=React.useState(0);console.log('render');return <button onClick={()=>setN(x=>x+1)}>{n}</button>;}结果:点击安排重新 render 并在 commit 后更新按钮文字;开发 Strict Mode 可能多次调用 render。
列表 key 为什么必须稳定?错误的 key 会造成什么问题?
key 标识同级列表项身份,重排时帮助保留正确组件状态;用索引作 key 在插入/删除/排序时可能让输入状态错位。
参考代码 · tsxfunction Rows({items}:{items:Array<{id:string;text:string}>}){return <ul>{items.map(x=><li key={x.id}><input defaultValue={x.text}/></li>)}</ul>;}结果:重排列表时输入状态跟随稳定 `id`,而非错误地跟随数组位置。
useEffect 的依赖、清理和闭包过期问题如何排查?
effect 应列全读取的响应式依赖,返回清理函数;闭包过期可通过正确依赖、函数式更新或 ref 解决,避免靠禁用 lint 掩盖问题。
参考代码 · tsxfunction Ticker({id}:{id:string}){React.useEffect(()=>{const timer=setInterval(()=>console.log(id),1000);return()=>clearInterval(timer);},[id]);return null;}结果:`id` 改变时清理旧计时器、建立新计时器,不继续输出旧 `id`。
useMemo、useCallback 和 React.memo 在什么条件下才有收益?
只有计算昂贵、引用稳定性确实影响子组件,且测量证明收益时才用;记忆化本身也有成本,先用 React Profiler 定位。
参考代码 · tsxconst Expensive=React.memo(function Expensive({items}:{items:number[]}){return <p>{items.reduce((a,b)=>a+b,0)}</p>;}); function Parent({items}:{items:number[]}){const sorted=React.useMemo(()=>[...items].sort((a,b)=>a-b),[items]);return <Expensive items={sorted}/>;}结果:`items` 引用不变时复用排序结果并可能跳过子组件渲染;收益须用 Profiler 测量。
useTransition 与 useDeferredValue 分别适合什么交互?
useTransition把特定状态更新标为非紧急并给 pending 状态;useDeferredValue让某个值的消费延后,适合输入即时响应、结果稍后更新。参考代码 · tsxfunction Search({items}:{items:string[]}){const [query,setQuery]=React.useState('');const deferred=React.useDeferredValue(query);return <><input value={query} onChange={e=>setQuery(e.target.value)}/><p>{items.filter(x=>x.includes(deferred)).length}</p></>;}结果:输入值立即变化,较慢的结果统计可延后;`useTransition` 适合把指定状态更新标为非紧急。
如何在 React 中处理表单提交的 pending、错误和乐观更新?
提交时禁重复或标 pending,显示可恢复错误;乐观更新需在失败时回滚或重新同步,尤其注意并发提交的顺序。
参考代码 · tsxfunction Submit({send}:{send:()=>Promise<void>}){const [pending,setPending]=React.useState(false),[error,setError]=React.useState('');return <><button disabled={pending} onClick={async()=>{setPending(true);setError('');try{await send();}catch(e){setError(String(e));}finally{setPending(false);}}}>提交</button>{error&&<p role="alert">{error}</p>}</>;}结果:提交期间按钮禁用,错误可见;乐观更新需在失败时回滚。
useActionState、useFormStatus 和 useOptimistic 各承担什么职责?
useActionState管理 action 结果与 pending,useFormStatus读取所属表单提交状态,useOptimistic在异步期间提供临时乐观视图。参考代码 · tsxfunction SaveForm(){const [state,action,pending]=React.useActionState<{saved:string|null},FormData>(async(_old,form)=>({saved:String(form.get('name')??'')}),{saved:null});return <form action={action}><input name="name"/><button disabled={pending}>保存</button><output>{state.saved??''}</output></form>;}结果:提交后 `state.saved` 显示名称;`useFormStatus` 可在子组件读取状态,`useOptimistic` 可显示临时值。
React 中何时使用 useState、Context 或外部状态库?
局部状态用
useState;跨层共享少量稳定值可用 Context;复杂跨页面共享或高频更新再评估外部状态库,避免把所有状态集中。参考代码 · tsxconst Theme=React.createContext('light'); function App(){const [theme,setTheme]=React.useState('light');return <Theme.Provider value={theme}><button onClick={()=>setTheme('dark')}>切换</button><View/></Theme.Provider>;} function View(){return <p>{React.useContext(Theme)}</p>;}结果:点击后 `View` 显示 `dark`;局部状态仍由 `useState` 管理。
如何定位大型 React 应用的无效渲染并量化优化效果?
用 React Profiler 找提交次数和耗时,确定导致更新的 state/props,改动后对同一交互量化比较;开发模式数据不等于生产模式。
参考代码 · tsxfunction Measured(){const renderStart=performance.now();React.useEffect(()=>console.log('commit elapsed',performance.now()-renderStart));return <div>内容</div>;}结果:输出近似渲染到 effect 的时间;正式比较需用 React Profiler 和相同交互样本。
如何设计一个支持取消、竞态处理和错误状态的异步数据 Hook?
Hook 在 effect 中创建
AbortController,请求开始设 loading,完成时只让当前版本写入 data/error,清理时 abort;给调用方retry与取消入口。参考代码 · tsxfunction useJson(url:string){const [state,setState]=React.useState<{data?:unknown;error?:string;loading:boolean}>({loading:true});React.useEffect(()=>{const c=new AbortController();setState({loading:true});fetch(url,{signal:c.signal}).then(r=>{if(!r.ok)throw Error(String(r.status));return r.json();}).then(data=>setState({data,loading:false})).catch(e=>{if(!c.signal.aborted)setState({error:String(e),loading:false});});return()=>c.abort();},[url]);return state;}结果:URL 改变时取消旧请求,返回加载、成功或失败状态。
React Compiler 的自动记忆化与手动 useMemo、useCallback 有什么关系?如何验证它是否生效?
React Compiler 可自动记忆化组件和 Hook 中部分计算;手动
useMemo/useCallback仍可精确控制特定值的稳定性,但不能把记忆化当作程序正确性的前提。检查构建配置与编译诊断,用 Profiler 对比提交次数和耗时。参考代码 · tsxfunction Total({prices}:{prices:number[]}){return <output>{prices.reduce((a,b)=>a+b,0)}</output>;}结果:总价正确性来自计算本身;若项目启用 React Compiler,可用编译诊断与 Profiler 检查是否自动记忆化。
Vue 3
Vue 3 的 reactive 和 ref 如何跟踪依赖并触发更新?
reactive通过 Proxy 跟踪对象属性读写,ref通过.value跟踪读写;effect 读取时收集依赖,写入触发调度更新。参考代码 · vue<script setup> import {ref,reactive} from 'vue' const count=ref(0),user=reactive({name:'Ada'}) </script> <template><button @click="count++">{{count}} {{user.name}}</button></template>结果:点击后 `count` 增加,模板自动更新。
computed、watch 和 watchEffect 的使用场景及执行时机有何不同?
computed缓存派生值;watch监控指定源并可取新旧值;watchEffect自动追踪同步读取的依赖。默认 watcher 回调通常在父更新后、组件 DOM 更新前运行,需 DOM 更新后用flush:'post'。参考代码 · vue<script setup> import {ref,computed,watch} from 'vue' const n=ref(1),double=computed(()=>n.value*2) watch(n,(next,prev)=>console.log(prev,next)) </script> <template><button @click="n++">{{double}}</button></template>结果:初始显示 `2`,点击后显示 `4`,watch 输出 `1 2`。
Vue 组件之间如何选择 props、emit、provide/inject 和 Pinia?
父子数据用 props/emit,深层依赖注入用 provide/inject,跨页面共享业务状态可用 Pinia;避免把局部临时状态全局化。
参考代码 · vue<!-- Parent.vue --> <Child :value="count" @change="count=$event" /> <!-- Child.vue: defineProps<{value:number}>(); const emit=defineEmits<{change:[value:number]}>() -->结果:子组件发 `change` 后父组件更新 `count`;跨多层共享可用 provide/inject。
v-model 在原生表单和自定义组件上分别如何工作?
原生表单
v-model映射值属性和输入事件;自定义组件默认用modelValueprop 和update:modelValue事件,也可声明命名模型。参考代码 · vue<script setup> const model=defineModel<string>({default:''}) </script> <template><input v-model="model" /></template>结果:父组件 `<Child v-model="name"/>` 与子组件双向同步值。
Vue 的 nextTick 解决了什么问题?什么时候不该依赖它?
nextTick等待 Vue 批量 DOM 更新完成,适合更新数据后读取新 DOM;不应用它代替正确的数据流或异步业务完成通知。参考代码 · vue<script setup> import {ref,nextTick} from 'vue' const n=ref(0) async function update(){n.value++;await nextTick();console.log(document.querySelector('#n')?.textContent)} </script> <template><button @click="update">更新</button><span id="n">{{n}}</span></template>结果:点击后日志为新的 DOM 文本 `1`。
Vue 2 项目迁移 Vue 3 时,如何分阶段处理 API 与依赖兼容?
先升级依赖并运行迁移构建,处理不兼容 API/插件;可逐模块迁移组件,覆盖关键交互测试,再切换应用入口。
参考代码 · js// 迁移时先检查依赖,再让新入口使用 Vue 3 createApp。 import {createApp} from 'vue'; import App from './App.vue'; createApp(App).mount('#app');结果:Vue 3 应用挂载到 `#app`;实际迁移还需逐项处理插件和不兼容 API。
为什么解构 reactive 对象可能丢失响应性?如何修复?
直接解构普通变量会失去 Proxy 属性读取链;使用
toRefs/toRef保留关联,或在消费处继续读取state.key。参考代码 · vue<script setup> import {reactive,toRefs} from 'vue' const state=reactive({count:0}) const {count}=toRefs(state) </script> <template><button @click="count++">{{count}}</button></template>结果:解构后仍保留响应性,点击更新数字。
Vue 列表渲染中 key 的作用是什么?如何避免组件状态错位?
key 帮助 Vue 在列表变动时识别节点与组件身份;用稳定 id,不在重排列表中用索引作 key。
参考代码 · vue<template><ul><li v-for="item in items" :key="item.id"><input :value="item.name"></li></ul></template>结果:按稳定 `id` 识别节点;重排时输入不应错绑到其他项。
Next.js 与服务端渲染
CSR、SSR、SSG 与按需重新生成页面分别适用于什么场景?
CSR 适合强交互且 SEO 要求低的界面;SSR 适合请求时个性化内容;SSG 适合变化少的公开内容;按需重新生成适合可容忍短时间旧内容的公开页面。
参考代码 · tsx// Next.js App Router:静态公开页面可在构建时生成。 export default async function Page(){const posts=await fetch('https://example.com/posts').then(r=>r.json());return <ul>{posts.map((p:{id:string;title:string})=><li key={p.id}>{p.title}</li>)}</ul>;}结果:页面在服务端生成 HTML;具体缓存/重验证由项目版本和配置决定。
Server Component 与 Client Component 的边界如何划分?
Server Component 负责服务端数据读取、保密逻辑和减少客户端 JS;需要 state、事件、浏览器 API 的边界用 Client Component,尽量让客户端边界靠近叶子。
参考代码 · tsx// app/page.tsx (Server Component) import Counter from './Counter'; export default async function Page(){const data=await loadPublicData();return <><h1>{data.title}</h1><Counter/></>;} // Counter.tsx 以 'use client' 开头并使用 useState。结果:标题由服务端输出,计数交互只给客户端组件加载 JS。
Hydration mismatch 有哪些常见原因?如何定位和修复?
服务端与客户端首屏输出不一致会报错,常见于时间、随机数、浏览器状态或非法 HTML 嵌套;定位具体节点,保证首次输出一致,浏览器专有值放 effect 后处理。
参考代码 · tsx'use client'; function Clock(){const [now,setNow]=React.useState<string|null>(null);React.useEffect(()=>setNow(new Date().toISOString()),[]);return <time>{now??'加载时间…'}</time>;}结果:首屏服务端和客户端都先显示占位文案,挂载后更新,避免时间造成 hydration mismatch。
哪些数据可以在服务端读取,哪些交互必须留在客户端?
密钥、数据库读取只在服务端;交互、DOM、localStorage 在客户端。服务端返回给客户端的 props 必须可序列化且不含秘密。
参考代码 · tsx// Server Component 内读取服务端环境变量,不把 secret 传给客户端。 export default async function Page(){const data=await fetchPrivate(process.env.API_SECRET);return <ClientWidget title={data.publicTitle}/>;}结果:客户端只收到 `publicTitle`;密钥不进入可序列化 props。
如何用 Suspense 和流式渲染改善慢数据页面的体验?
用 Suspense 为慢数据区块设 fallback,让外层内容先到达;流式响应逐步发送边界内容,仍需有错误边界和可用的加载状态。
参考代码 · tsximport {Suspense} from 'react'; export default function Page(){return <><h1>文章</h1><Suspense fallback={<p>正文加载中…</p>}><SlowArticle/></Suspense></>;}结果:标题和 fallback 可先返回,慢正文随后流式到达;`SlowArticle` 是异步服务端组件。
如何设计公开页面与个性化页面的缓存策略,避免数据串用?
公共数据可按明确标签/时效缓存;含用户身份的数据按请求隔离或不缓存,缓存键必须包含影响输出的身份/权限维度,防止串用。
参考代码 · tsxasync function userProfile(userId:string){return fetch(`/api/users/${encodeURIComponent(userId)}`,{cache:'no-store'}).then(r=>r.json());}结果:个性化资料每次读取,不作为公开共享缓存;服务端仍须校验当前用户权限。
如何让服务端渲染页面同时具备良好的 SEO 和可交互性?
服务端输出可抓取的标题、正文与元数据,客户端对需要的区域进行 hydration;减少阻塞脚本并确保交互在加载前有可理解的状态。
参考代码 · tsxexport const metadata={title:'产品说明',description:'公开的产品介绍'}; export default function Page(){return <main><h1>产品说明</h1><p>可抓取的正文</p><ClientAction/></main>;}结果:服务端 HTML 有标题与正文,`ClientAction` 承担交互。
Next.js 的 Cache Components 与 use cache 如何划分静态、缓存和请求时数据?
Cache Components 让可缓存区域显式使用
use cache并设置生命周期/失效策略;依请求变化的区域保持动态,使用 Suspense 边界。具体行为随 Next.js 版本和配置变化,应以项目版本验证。参考代码 · tsximport {Suspense} from 'react'; async function getPublicPosts(){'use cache';const response=await fetch('https://example.com/posts');return response.json();} async function PostList(){const posts=await getPublicPosts();return <ul>{posts.map((p:{id:string;title:string})=><li key={p.id}>{p.title}</li>)}</ul>;} export default function Page(){return <Suspense fallback={<p>加载中…</p>}><PostList/></Suspense>;}结果:公开帖子数据可缓存;真实缓存寿命与失效策略须按项目 Next.js 版本配置。
性能、工程化与测试
LCP、INP、CLS 各衡量什么?如何用真实用户数据定位问题?
LCP 衡量最大内容渲染,INP 衡量交互响应,CLS 衡量意外布局偏移;按设备/页面分组看真实用户分位数,再用实验室工具找根因。
参考代码 · jsnew PerformanceObserver(list=>{for(const e of list.getEntries())console.log(e.name,e.value??e.duration);}).observe({type:'largest-contentful-paint',buffered:true});结果:浏览器支持时输出 LCP 候选;INP、CLS 需采集对应指标并按真实用户分组统计。
首屏图片、字体与脚本分别如何优化,并如何验证收益?
图片用合适尺寸/格式并优先加载 LCP 图,字体减少字重并控制交换,脚本按需拆分/延后;用同口径真实用户指标和网络瀑布图验证。
参考代码 · html<link rel="preload" as="image" href="hero.webp"> <img src="hero.webp" width="1200" height="600" fetchpriority="high" alt="首页主图"> <script type="module" src="app.js"></script>结果:主图更早请求且预留尺寸;收益需用同口径 LCP 和网络瀑布验证。
长列表和聊天记录应如何虚拟化?动态高度会带来什么问题?
只渲染视口及缓冲区项目;动态高度需测量并维护位置索引,插入历史消息时保持滚动锚点,避免跳动。
参考代码 · jsfunction visibleRange(scrollTop,rowHeight,viewportHeight,total){const start=Math.max(0,Math.floor(scrollTop/rowHeight)-3);const end=Math.min(total,Math.ceil((scrollTop+viewportHeight)/rowHeight)+3);return {start,end};} console.log(visibleRange(500,50,300,1000));结果:`{start:7,end:19}`;动态高度时要测量行高并维持滚动锚点。
Vite 开发服务器与生产构建的工作方式有何区别?
Vite 开发时按需通过原生 ESM 提供模块并快速 HMR;生产构建打包、优化、拆分和压缩资源。TypeScript 类型检查需另跑。
参考代码 · json{"scripts":{"dev":"vite","build":"vite build","typecheck":"tsc --noEmit"}}结果:开发走按需模块与 HMR,构建生成优化产物;类型检查需单独运行。
Tree shaking 失效的常见原因是什么?如何检查打包体积?
副作用标记错误、CommonJS、不支持静态分析的动态访问或整个库导入可影响 tree shaking;用构建分析报告和产物确认实际体积。
参考代码 · jsconst add=(a,b)=>a+b; const unused=()=>42; console.log(add(1,2));结果:`3`;生产构建可能移除未调用的 `unused`,需检查实际产物确认。
如何为组件设计单元测试、交互测试和端到端测试的边界?
纯逻辑用单元测试,组件行为用用户交互测试,跨页面关键流程用 E2E;避免三层重复测试同一实现细节。
参考代码 · tsx// Testing Library 示例:验证用户可见行为。 function Counter(){const [count,setCount]=React.useState(0);return <button onClick={()=>setCount(x=>x+1)}>{count}</button>;} render(<Counter/>); await user.click(screen.getByRole('button',{name:'增加'})); expect(screen.getByRole('button',{name:'1'})).toBeVisible();结果:点击后按钮显示 `1`;需在 Testing Library 测试环境运行。
如何测试异步请求的加载、失败、取消和重复提交?
用可控的请求 mock 验证 loading→success/error、取消后不更新、重复提交行为;断言用户可见状态与请求次数。
参考代码 · jsconst controller=new AbortController(); const promise=fetch('/api',{signal:controller.signal}); controller.abort(); await expect(promise).rejects.toMatchObject({name:'AbortError'});结果:测试验证取消分支;`expect` 需在 Vitest/Jest 环境中运行,网络请求应使用 mock。
Monorepo 中如何划分共享包、版本与依赖边界?
共享包只放跨应用稳定能力,明确包之间依赖方向与发布版本;避免应用互相引用内部源码,CI 按变更范围构建测试。
参考代码 · json{"name":"@study/ui","private":true,"exports":{".":"./src/index.ts"},"peerDependencies":{"react":"^19"}}结果:共享 UI 包只暴露指定入口,React 由使用方提供;版本范围需与实际工作区匹配。
如何制定前端错误监控、Source Map 和发布回滚方案?
采集错误与发布版本、匿名会话线索;Source Map 私有上传并限权限;发布采用可回退产物/开关,白屏率异常时快速回滚。
参考代码 · jswindow.addEventListener('error',e=>report({message:e.message,release:APP_RELEASE})); window.addEventListener('unhandledrejection',e=>report({message:String(e.reason),release:APP_RELEASE}));结果:上报带版本的错误;`report` 须脱敏,Source Map 应受控上传,故障时回滚对应版本。
如何量化一次性能优化,避免只看本地 Lighthouse 分数?
明确目标指标与样本,固定页面、设备和网络条件比较实验室数据,同时看线上同口径 p75/p95 与错误率,避免只凭一次 Lighthouse 分数。
参考代码 · jsconst p75=values=>[...values].sort((a,b)=>a-b)[Math.ceil(values.length*.75)-1]; console.log(p75([100,200,300,400]));结果:`300`;比较优化前后需同设备、同页面和同流量口径。
AI 应用前端与流式交互
如何用 fetch、ReadableStream 和 TextDecoder 逐块解析模型流式响应?
fetch后从response.body.getReader()循环读块,用TextDecoder.decode(value,{stream:true})保留跨块字符;SSE 还需缓冲到完整事件边界再解析,不能假定一块就是一条消息。参考代码 · jsasync function readStream(response,onText){ const reader=response.body.getReader(),decoder=new TextDecoder(); try{while(true){const {done,value}=await reader.read();if(done)break;onText(decoder.decode(value,{stream:true}));}onText(decoder.decode());} finally{reader.releaseLock();} }结果:逐块得到解码文本;SSE 的 `data:` 边界还需跨块缓冲解析。
SSE 流断开、用户取消和网络重连时,界面状态如何恢复?
维护连接状态与最后事件 id;断线按可重放协议重连并去重,用户取消用
AbortController且不自动重连;服务端不支持重放时不能假称续传。参考代码 · jsconst controller=new AbortController(); async function start(){try{const response=await fetch('/events',{signal:controller.signal});if(!response.ok)throw Error(String(response.status));await readStream(response,console.log);}catch(error){if(controller.signal.aborted)console.log('cancelled');else console.error('retryable error',error);}}结果:用户取消显示 `cancelled`,网络失败进入重试状态;续传需服务器提供游标。
模型输出 Markdown 时,如何处理不完整片段和不可信内容?
对不完整 Markdown 可先按文本或增量解析展示,完成后再稳定渲染;禁止直接注入模型生成 HTML,链接与代码展示也要做安全处理。
参考代码 · jsconst partialMarkdown='**未结束的流'; const preview=document.createElement('pre'); preview.textContent=partialMarkdown;结果:流未结束时安全显示原始 Markdown 文本;最终富文本渲染需可信解析和 HTML 清洗。
聊天消息持续增长时,如何控制渲染成本和自动滚动行为?
长列表虚拟化或分段,流式文本按帧/批次更新;仅当用户停留底部时自动跟随,用户向上阅读则显示“跳到底部”按钮。
参考代码 · jsfunction shouldStick(container){return container.scrollHeight-container.scrollTop-container.clientHeight<80;} const stick=shouldStick(chat);appendMessage(message);if(stick)requestAnimationFrame(()=>chat.scrollTop=chat.scrollHeight);结果:用户原本接近底部时自动跟随;向上阅读历史时不抢滚动位置。
如何避免旧请求的流式内容覆盖用户最新发起的会话?
每次会话/请求分配版本 id 并取消旧流;收到片段时先核对当前 id,再写入对应消息,切换会话时清理 reader。
参考代码 · jslet version=0,controller; async function ask(prompt){const current=++version;controller?.abort();controller=new AbortController();const response=await fetch('/chat',{method:'POST',body:prompt,signal:controller.signal});if(current!==version)return;await readStream(response,text=>{if(current===version)append(text);});}结果:新会话取消旧请求,旧流片段不会覆盖当前内容。
工具调用或 Agent 运行较久时,如何展示进度、错误和可取消状态?
明确“排队/运行/等待工具/完成/失败/已取消”状态,显示可理解的进度与重试入口;取消要传到底层并处理服务端仍在执行的情况。
参考代码 · tsxfunction RunStatus({run,cancel}:{run:{state:'running'|'error'|'done';step?:string;error?:string};cancel:()=>void}){return <section aria-live="polite"><p>{run.state==='running'?`正在执行:${run.step}`:run.state==='error'?`失败:${run.error}`:'已完成'}</p>{run.state==='running'&&<button onClick={cancel}>取消</button>}</section>;}结果:显示步骤、错误或完成状态;运行中可取消。
如何审查 AI 生成代码的正确性、安全性与可维护性?
先核对需求与数据边界,再审查输入校验、依赖、授权、错误处理与复杂度;运行测试和真实交互,对关键逻辑保留人工复核。
参考代码 · js// 对 AI 生成的纯函数先做可复现断言,再做依赖和安全审查。 const generated=(a,b)=>a+b; console.assert(generated(0,0)===0); console.assert(generated(-2,3)===1);结果:两个边界断言通过;不能据此证明所有输入或外部依赖安全。
你如何在实际项目中使用 AI 工具,并验证它产出的代码?
可以描述具体任务、给 AI 的上下文范围、所采用与拒绝的建议,以及测试/审查证据;面试重点是你如何验证,而非仅说“用 AI 提效”。
参考代码 · js// 工作流:保留输入、候选补丁、检查结果和人工审查结论。 const checks=['typecheck','unit','build'],results={typecheck:'passed',unit:'passed',build:'passed'}; console.log(checks.every(name=>results[name]==='passed'));结果:`true`;真实 `results` 必须来自实际 CI,不可凭模型描述代替。
系统设计与项目深挖
如何设计一个高并发搜索建议框,并处理防抖、取消和结果乱序?
输入防抖并设最小长度,取消旧请求或用版本号丢弃旧结果;缓存短期热门查询,限制服务端速率,完整处理键盘操作、加载和空态。
参考代码 · jslet timer,controller,version=0; function suggest(query){clearTimeout(timer);controller?.abort();const current=++version;timer=setTimeout(async()=>{controller=new AbortController();try{const r=await fetch(`/suggest?q=${encodeURIComponent(query)}`,{signal:controller.signal});if(current===version)render(await r.json());}catch(e){if(e.name!=='AbortError')showError(e);}},250);}结果:防抖、取消和版本校验共同避免旧结果覆盖新输入;`render/showError` 为界面函数。
如何设计后台系统的路由、按钮和数据权限?前后端职责如何划分?
前端路由/按钮权限仅控制体验,后端对每个资源和操作强制鉴权;权限模型可用角色加资源动作,并避免把敏感数据先发给无权用户。
参考代码 · js// 服务端必须独立校验;前端权限只影响展示。 app.delete('/users/:id',authenticate,requirePermission('users:delete'),async(req,res)=>{await deleteUser(req.params.id);res.sendStatus(204);});结果:无权限请求在服务端被拒绝;按钮隐藏不能替代接口鉴权。
如何设计大型前端应用的组件边界和状态管理方案?
按业务领域划分组件与状态,局部状态留局部,跨域状态经明确接口共享;区分服务端缓存数据与 UI 临时状态,防止全局 store 成为耦合中心。
参考代码 · tsxfunction SearchPage(){const [query,setQuery]=React.useState('');return <><SearchBox value={query} onChange={setQuery}/><SearchResults query={query}/></>;}结果:只在最近共同父组件共享查询状态;跨页面共享再评估 Context 或状态库。
如何解释一次真实项目的性能瓶颈、改动、指标和取舍?
用“场景与基线→证据→改动→前后指标→取舍”叙述真实案例,例如 p75 LCP 从多少降到多少;没有实测数据就如实说明。
参考代码 · jsconst before={lcpP75:3200,inpP75:280},after={lcpP75:2400,inpP75:190}; console.log(before.lcpP75-after.lcpP75,before.inpP75-after.inpP75);结果:样例改善 `800ms` 与 `90ms`;真实项目需填真实同口径数据并说明取舍。
如何设计稳定的文件上传体验,包括进度、失败恢复和重复提交?
分片上传、进度计算、断点查询和幂等完成接口;网络失败可重试单片,重复提交用上传 id/幂等键防止重复文件,最终校验完整性。
参考代码 · jsasync function upload(file,id,onProgress){const chunk=1024*1024;for(let start=0,i=0;start<file.size;start+=chunk,i++){const response=await fetch(`/upload/${id}/${i}`,{method:'PUT',body:file.slice(start,start+chunk)});if(!response.ok)throw Error(`failed part ${i}`);onProgress(Math.min(file.size,start+chunk)/file.size);}}结果:逐片上传并更新进度;服务端需要上传 id 幂等和最终校验。
如何设计实时聊天页面的消息顺序、断线重连和历史分页?
消息用服务端序号或时间加稳定 id 排序,发送中临时 id 与回执对账;断线按游标补消息,历史分页向上插入并保持滚动位置。
参考代码 · jsconst byId=new Map(); function receive(message){byId.set(message.id,message);return [...byId.values()].sort((a,b)=>a.seq-b.seq);}结果:按服务端序号展示且重复消息不会重复;断线时用最后序号请求缺失消息。
WebView 与原生页面通信时,如何设计 JS Bridge 的权限和错误处理?
JS Bridge 采用白名单方法、版本化请求/响应 id、来源校验与权限控制;每次调用有超时和结构化错误,敏感能力必须由原生端复核授权。
参考代码 · jsconst pending=new Map();let nextId=0; function callNative(method,payload){if(!['openCamera'].includes(method))return Promise.reject(Error('denied'));const id=++nextId;return new Promise((resolve,reject)=>{const timer=setTimeout(()=>{pending.delete(id);reject(Error('timeout'));},5000);pending.set(id,{resolve,reject,timer});window.NativeBridge.postMessage(JSON.stringify({id,method,payload}));});}结果:只允许白名单方法,5 秒无回执时拒绝;原生侧仍须校验来源和权限。
当线上页面出现白屏时,你会如何定位、止损和复盘?
先确认影响范围并回滚/切流止损,再查发布版本、静态资源加载、JS 错误、接口和 CDN;复现根因、补监控与回归测试,记录恢复时间和预防措施。
参考代码 · js// 线上白屏排查用版本与资源加载证据定位,回滚命令按实际平台执行。 window.addEventListener('error',e=>report({release:APP_RELEASE,url:e.filename,message:e.message}));结果:错误事件携带发布版本和资源 URL;止损后检查部署、CDN、接口,再复盘根因。