2025大厂手写题 5 个 “隐形加分点”:写对不算赢,细节才是 Offer 密码

116 阅读9分钟

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;
}

面试官加分点

  1. WeakMap处理循环引用,而非Map—— 弱引用不阻塞垃圾回收,体现内存管理意识。
  2. 兼容Date/RegExp等特殊类型,不是只处理普通对象 / 数组。
  3. 拷贝Symbol属性和原型链,覆盖更多边缘场景。
  4. 代码带注释、参数类型说明,可读性拉满。

二、防抖节流:加个 “取消功能”,瞬间超越 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;
}

面试官加分点

  1. 支持immediate参数,满足 “首次立即执行” 场景(如搜索框聚焦时触发)。
  2. 提供cancel方法,解决 “组件卸载前取消未执行回调” 的实际业务问题。
  3. apply绑定this和传递参数,避免上下文丢失。
  4. 节流区分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'}]
});

面试官加分点

  1. 支持suppressError容错配置,解决实际业务中 “部分请求失败仍需后续处理” 的场景。
  2. 校验参数是否为 “可迭代对象”(而非仅数组),兼容Set等类型。
  3. 分离成功 / 失败结果,附带索引信息,方便定位问题。
  4. 处理空数组边界 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>) {}

面试官加分点

  1. 不仅能手写基础工具类型,还能组合实现复杂需求(如MyOmitPick+Exclude)。
  2. 结合真实业务场景(API 类型提取、表单参数约束),说明工具类型的价值。
  3. as const增强类型推导精度,体现 TS 进阶能力。
  4. 代码带类型注释,逻辑清晰易读。

五、React Hooks 手写:useCallback/useMemo 不止 “缓存”,要懂 “依赖优化”

React Hooks 手写题(如useDebounceuseRequest)是 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)} />;
}

面试官加分点

  1. useRef存储定时器和状态,解决 Hooks 闭包陷阱(避免拿到旧值)。
  2. useCallback缓存函数,减少不必要的重渲染。
  3. 依赖数组显式声明,组件卸载时清除副作用,避免内存泄漏。
  4. 支持手动取消,适配 “组件卸载前取消异步操作” 的业务场景。

📌 最后:大厂手写题的 “加分心法”

2025 年大厂手写题的核心考察逻辑,早已从 “能不能实现” 变成 “能不能优雅实现”。记住三个关键:

  1. 边界先行:先处理空值、异常、特殊类型,再写核心逻辑 —— 体现鲁棒性思维。
  2. 场景落地:每个功能都要想 “实际业务中会用到吗”,比如防抖的取消功能、Promise 的容错设计。
  3. 可读性优先:代码带注释、变量命名清晰、结构分层 —— 面试官每天看几十份代码,清爽的代码会格外加分。

其实这些加分点都不是高深技巧,而是把 “工程思维” 融入手写题中。多站在 “如何让代码更稳定、更易用” 的角度思考,就能轻松超越大部分候选人。

最后想问:你在手写题中还遇到过哪些面试官追问的 “加分项”?评论区聊聊,点赞最高的送一份《2025 大厂手写题高频考点清单》(含本文所有加分版代码 + 面试追问话术)~