2025大厂手写题 5 个 “隐形加分点”:写对不算赢,细节才是 Offer 密码
看来=了大厂面试复盘,发现一个扎心真相:手写题 “写出来” 只能拿 30 分,“写得漂亮” 才能拿 Offer。
2025 年大厂面试早已告别 “能跑就行” 的时代,面试官更看重代码背后的工程思维 —— 边界处理、性能优化、鲁棒性设计,这些藏在细节里的 “加分点”,才是区分 “搬砖工” 和 “工程师” 的关键。
今天就分享 5 个高频手写题的 “隐形加分技巧”,覆盖深拷贝、防抖节流、Promise 工具、TS 工具类型等必考点,每个都附 “基础版 vs 加分版” 代码对比,帮你在面试中快速脱颖而出~
一、深拷贝:不止递归,WeakMap + 特殊类型才是满分答案
深拷贝是大厂面试 “常客”,但多数人只会写基础递归,一遇到循环引用、特殊类型就翻车。面试官真正想考察的,是你对 JS 数据类型和内存机制的理解。
基础版(只能拿 30 分)
javascript
// 问题:无法处理循环引用、Date/RegExp等特殊类型
function deepClone(obj) {
if (typeof obj !== 'object' || obj === null) return obj;
let newObj = Array.isArray(obj) ? [] : {};
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
newObj[key] = deepClone(obj[key]);
}
}
return newObj;
}
加分版(直接拉满)
javascript
/**
* 增强版深拷贝:处理循环引用、特殊类型,兼顾性能
* @param {any} obj 要拷贝的对象
* @param {WeakMap} hash 存储已拷贝对象,解决循环引用
*/
function deepClone(obj, hash = new WeakMap()) {
// 1. 基础类型和null直接返回
if (typeof obj !== 'object' || obj === null) return obj;
// 2. 处理循环引用:已拷贝过直接返回副本
if (hash.has(obj)) return hash.get(obj);
// 3. 处理特殊对象类型
if (obj instanceof Date) {
const newDate = new Date(obj);
hash.set(obj, newDate);
return newDate;
}
if (obj instanceof RegExp) {
const newReg = new RegExp(obj.source, obj.flags);
hash.set(obj, newReg);
return newReg;
}
// 4. 数组/对象初始化,存入hash(关键:递归前缓存,避免循环)
const newObj = Array.isArray(obj) ? [] : Object.create(Object.getPrototypeOf(obj));
hash.set(obj, newObj);
// 5. 拷贝自身属性(含Symbol)
const keys = [...Object.keys(obj), ...Object.getOwnPropertySymbols(obj)];
keys.forEach(key => {
newObj[key] = deepClone(obj[key], hash);
});
return newObj;
}
面试官加分点
- 用
WeakMap处理循环引用,而非Map—— 弱引用不阻塞垃圾回收,体现内存管理意识。 - 兼容
Date/RegExp等特殊类型,不是只处理普通对象 / 数组。 - 拷贝
Symbol属性和原型链,覆盖更多边缘场景。 - 代码带注释、参数类型说明,可读性拉满。
二、防抖节流:加个 “取消功能”,瞬间超越 80% 候选人
防抖节流是前端性能优化必考点,基础版实现不难,但面试官一定会追问 “如何手动取消”“是否支持立即执行”,这些扩展功能才是加分关键。
基础版(仅满足基本需求)
javascript
// 防抖基础版:无取消、无立即执行
function debounce(fn, delay) {
let timer = null;
return function(...args) {
clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
}
加分版(大厂级实现)
javascript
/**
* 完整版防抖:支持立即执行、手动取消、参数穿透
* @param {Function} fn 目标函数
* @param {number} delay 延迟时间
* @param {boolean} immediate 是否立即执行(默认false)
*/
function debounce(fn, delay, immediate = false) {
let timer = null;
let isInvoked = false; // 标记是否已执行
const debounced = function(...args) {
// 清除上一个定时器
if (timer) clearTimeout(timer);
// 立即执行逻辑:第一次触发直接执行
if (immediate && !isInvoked) {
fn.apply(this, args);
isInvoked = true;
return;
}
// 延迟执行逻辑
timer = setTimeout(() => {
fn.apply(this, args);
timer = null;
isInvoked = false; // 重置标记
}, delay);
};
// 手动取消功能(面试官高频追问)
debounced.cancel = function() {
clearTimeout(timer);
timer = null;
isInvoked = false;
};
return debounced;
}
// 节流同理:增加leading/trailing配置,支持取消
function throttle(fn, delay, options = { leading: true, trailing: true }) {
let timer = null;
let lastTime = 0;
const throttled = function(...args) {
const now = Date.now();
// 跳过第一次执行(如果禁用leading)
if (!lastTime && !options.leading) lastTime = now;
const remaining = delay - (now - lastTime);
// 时间差达标,直接执行
if (remaining <= 0 || remaining > delay) {
if (timer) {
clearTimeout(timer);
timer = null;
}
fn.apply(this, args);
lastTime = now;
} else if (!timer && options.trailing) {
// 延迟执行剩余时间(trailing逻辑)
timer = setTimeout(() => {
fn.apply(this, args);
lastTime = options.leading ? Date.now() : 0;
timer = null;
}, remaining);
}
};
throttled.cancel = function() {
clearTimeout(timer);
timer = null;
lastTime = 0;
};
return throttled;
}
面试官加分点
- 支持
immediate参数,满足 “首次立即执行” 场景(如搜索框聚焦时触发)。 - 提供
cancel方法,解决 “组件卸载前取消未执行回调” 的实际业务问题。 - 用
apply绑定this和传递参数,避免上下文丢失。 - 节流区分
leading(首次执行)和trailing(末次执行),覆盖更多场景。
三、Promise 工具:Promise.all 不止 “全成或全败”,要懂容错设计
手写Promise.all/Promise.race是高频考点,但 2025 年面试更倾向 “场景化考察”—— 比如 “如何实现支持部分失败的 Promise.all”,考验你的工程设计能力。
基础版(仅实现核心功能)
javascript
// 问题:一个失败就整体失败,无容错能力
function myPromiseAll(promises) {
return new Promise((resolve, reject) => {
if (!Array.isArray(promises)) reject(new TypeError('参数必须是数组'));
const results = [];
let count = 0;
promises.forEach((p, index) => {
Promise.resolve(p).then(res => {
results[index] = res;
count++;
if (count === promises.length) resolve(results);
}).catch(err => reject(err));
});
});
}
加分版(支持容错 + 类型校验 + 进度反馈)
javascript
/**
* 增强版Promise.all:支持部分失败、类型校验、进度反馈
* @param {Iterable} iterable 可迭代对象(如数组)
* @param {Object} options 配置项:suppressError(是否容错)
* @returns {Promise<{success: any[], failed: any[]}>} 成功/失败结果分离
*/
function myPromiseAll(iterable, options = { suppressError: false }) {
// 1. 类型校验:必须是可迭代对象
if (typeof iterable[Symbol.iterator] !== 'function') {
return Promise.reject(new TypeError('参数必须是可迭代对象'));
}
const promises = Array.from(iterable);
const success = [];
const failed = [];
let completedCount = 0;
const total = promises.length;
return new Promise((resolve) => {
// 空数组直接resolve
if (total === 0) return resolve({ success, failed });
promises.forEach((p, index) => {
Promise.resolve(p)
.then(res => {
success[index] = { index, value: res };
})
.catch(err => {
if (options.suppressError) {
failed[index] = { index, reason: err };
} else {
// 不禁用容错时,一个失败直接reject
return Promise.reject(err);
}
})
.finally(() => {
completedCount++;
// 所有任务完成后resolve
if (completedCount === total) {
resolve({
success: success.filter(Boolean), // 过滤空值
failed: failed.filter(Boolean)
});
}
});
});
});
}
// 用法示例:容错模式(部分失败不影响整体)
myPromiseAll([Promise.resolve(1), Promise.reject('err'), Promise.resolve(3)], {
suppressError: true
}).then(({ success, failed }) => {
console.log('成功结果:', success); // [{index:0, value:1}, {index:2, value:3}]
console.log('失败结果:', failed); // [{index:1, reason:'err'}]
});
面试官加分点
- 支持
suppressError容错配置,解决实际业务中 “部分请求失败仍需后续处理” 的场景。 - 校验参数是否为 “可迭代对象”(而非仅数组),兼容
Set等类型。 - 分离成功 / 失败结果,附带索引信息,方便定位问题。
- 处理空数组边界 case,避免逻辑漏洞。
四、TypeScript 工具类型:不止 “会用”,要能 “手写 + 业务落地”
2025 年大厂面试 TS 占比飙升,手写工具类型是区分 “TS 新手” 和 “TS 高手” 的关键,尤其要结合业务场景说明用法。
高频工具类型手写(加分版)
typescript
// 1. 手写Partial(核心:映射类型+可选修饰符)
type MyPartial<T> = {
[P in keyof T]?: T[P];
};
// 2. 手写ReturnType(核心:条件类型+infer推导)
type MyReturnType<T extends (...args: any[]) => any> = T extends (...args: any[]) => infer R
? R
: never;
// 3. 手写Omit(核心:Pick+Exclude组合)
type MyOmit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
// 4. 业务扩展:提取API返回值并剔除敏感字段(面试加分场景)
async function fetchUser() {
return { id: 1, name: '张三', token: 'xxx-xxx', password: '123' } as const;
}
// 提取返回值类型,剔除token和password敏感字段
type SafeUser = MyOmit<MyReturnType<typeof fetchUser>, 'token' | 'password'>;
// 结果:{ id: 1; name: '张三' }
// 5. 业务扩展:表单更新参数(Partial实战)
interface UserForm {
name: string;
age: number;
email: string;
}
// 更新时只需传部分字段
function updateUser(id: number, data: MyPartial<UserForm>) {}
面试官加分点
- 不仅能手写基础工具类型,还能组合实现复杂需求(如
MyOmit=Pick+Exclude)。 - 结合真实业务场景(API 类型提取、表单参数约束),说明工具类型的价值。
- 用
as const增强类型推导精度,体现 TS 进阶能力。 - 代码带类型注释,逻辑清晰易读。
五、React Hooks 手写:useCallback/useMemo 不止 “缓存”,要懂 “依赖优化”
React Hooks 手写题(如useDebounce、useRequest)是 2025 年大厂新热点,考察的不是 API 记忆,而是对 Hooks 依赖、闭包陷阱的理解。
加分版:useDebounce(避坑 + 优化)
javascript
import { useState, useEffect, useRef, useCallback } from 'react';
/**
* 增强版useDebounce:解决闭包陷阱、支持立即执行、手动取消
* @param {Function} fn 目标函数
* @param {number} delay 延迟时间
* @param {any[]} deps 依赖数组
* @param {boolean} immediate 是否立即执行
*/
function useDebounce(fn, delay = 300, deps = [], immediate = false) {
const timerRef = useRef(null);
const isInvokedRef = useRef(false);
// 用useCallback缓存函数,避免依赖变化导致重复创建
const debouncedFn = useCallback((...args) => {
// 清除上一个定时器
if (timerRef.current) clearTimeout(timerRef.current);
// 立即执行逻辑
if (immediate && !isInvokedRef.current) {
fn.apply(this, args);
isInvokedRef.current = true;
return;
}
// 延迟执行逻辑
timerRef.current = setTimeout(() => {
fn.apply(this, args);
isInvokedRef.current = false;
timerRef.current = null;
}, delay);
}, [fn, delay, immediate]);
// 依赖变化时清除定时器(避免闭包陷阱)
useEffect(() => {
return () => {
if (timerRef.current) clearTimeout(timerRef.current);
};
}, deps);
// 手动取消方法
const cancel = useCallback(() => {
if (timerRef.current) clearTimeout(timerRef.current);
timerRef.current = null;
isInvokedRef.current = false;
}, []);
return [debouncedFn, cancel];
}
// 用法示例
function SearchInput() {
const [value, setValue] = useState('');
// 搜索函数防抖
const [debouncedSearch, cancelSearch] = useDebounce(
(val) => console.log('搜索:', val),
500,
[value] // 依赖value变化
);
useEffect(() => {
debouncedSearch(value);
// 组件卸载时取消
return cancelSearch;
}, [debouncedSearch, value]);
return <input value={value} onChange={(e) => setValue(e.target.value)} />;
}
面试官加分点
- 用
useRef存储定时器和状态,解决 Hooks 闭包陷阱(避免拿到旧值)。 - 用
useCallback缓存函数,减少不必要的重渲染。 - 依赖数组显式声明,组件卸载时清除副作用,避免内存泄漏。
- 支持手动取消,适配 “组件卸载前取消异步操作” 的业务场景。
📌 最后:大厂手写题的 “加分心法”
2025 年大厂手写题的核心考察逻辑,早已从 “能不能实现” 变成 “能不能优雅实现”。记住三个关键:
- 边界先行:先处理空值、异常、特殊类型,再写核心逻辑 —— 体现鲁棒性思维。
- 场景落地:每个功能都要想 “实际业务中会用到吗”,比如防抖的取消功能、Promise 的容错设计。
- 可读性优先:代码带注释、变量命名清晰、结构分层 —— 面试官每天看几十份代码,清爽的代码会格外加分。
其实这些加分点都不是高深技巧,而是把 “工程思维” 融入手写题中。多站在 “如何让代码更稳定、更易用” 的角度思考,就能轻松超越大部分候选人。
最后想问:你在手写题中还遇到过哪些面试官追问的 “加分项”?评论区聊聊,点赞最高的送一份《2025 大厂手写题高频考点清单》(含本文所有加分版代码 + 面试追问话术)~