← 返回手写题目录

🗂️ 对象手写题 Top 10

深浅拷贝 · 扁平化 · 嵌套取值 · 属性过滤 · 合并 · 比较 · 遍历

拷贝相关(最高频)

1手写深拷贝(处理循环引用、特殊类型)

场景:复制表单数据到编辑器,修改副本不能影响原数据。必考,深拷贝几乎 100% 问。

function deepClone(obj, visited = new WeakMap()) { // ① 基本类型 / null / 函数 直接返回 if (obj === null || typeof obj !== 'object') return obj // ② 处理循环引用:已拷贝过就直接返回 if (visited.has(obj)) return visited.get(obj) // ③ 处理特殊类型 if (obj instanceof Date) return new Date(obj) if (obj instanceof RegExp) return new RegExp(obj) if (obj instanceof Map) { const copy = new Map() visited.set(obj, copy) obj.forEach((v, k) => copy.set(deepClone(k, visited), deepClone(v, visited))) return copy } // ④ 创建副本,先存入 visited(防止递归时再次进入) const clone = Array.isArray(obj) ? [] : {} visited.set(obj, clone) // ⑤ 递归拷贝每个自有属性 Reflect.ownKeys(obj).forEach(key => { clone[key] = deepClone(obj[key], visited) }) return clone } // 测试循环引用 const a = { name: 'test' } a.self = a // 自引用 JSON.stringify(deepClone(a)) // 不报错,能正常拷贝
三个关键点:① WeakMap 处理循环引用;② 处理 Date/RegExp/Map/Set 等特殊类型;③ Reflect.ownKeys 拷贝 Symbol 属性。生产环境直接用 structuredClone(obj)
2浅拷贝的几种实现

场景:只复制一层,嵌套对象还是引用。理解浅拷贝是理解深拷贝的前提。

// 方法1:扩展运算符(最常用) const copy = { ...obj } // 方法2:Object.assign const copy = Object.assign({}, obj) // 方法3:手写 for in function shallowClone(obj) { if (!obj || typeof obj !== 'object') return obj const copy = Array.isArray(obj) ? [] : {} for (const key in obj) { if (obj.hasOwnProperty(key)) copy[key] = obj[key] } return copy } // 浅拷贝的"坑":嵌套对象还是引用 const original = { a: 1, nested: { b: 2 } } const copy = { ...original } copy.nested.b = 99 console.log(original.nested.b) // 99 —— 原对象也被改了!
对比记忆:浅拷贝只复制第一层;深拷贝递归复制所有层。浅拷贝适合扁平对象,嵌套对象必须深拷贝。
3对象扁平化({a:{b:{c:1}}} → {'a.b.c':1})

场景:表单嵌套数据提交到后端要扁平化;配置文件 key 用点分隔。

function flattenObj(obj, prefix = '', result = {}) { for (const key in obj) { if (!obj.hasOwnProperty(key)) continue const newKey = prefix ? `${prefix}.${key}` : key if (typeof obj[key] === 'object' && obj[key] !== null && !Array.isArray(obj[key])) { // 是对象就递归,前缀拼接 flattenObj(obj[key], newKey, result) } else { result[newKey] = obj[key] } } return result } flattenObj({ a: { b: { c: 1 } }, d: 2 }) // { 'a.b.c': 1, 'd': 2 } // 反操作:还原嵌套对象('a.b.c' → a:{b:{c:1}}) function unflattenObj(flat) { const result = {} for (const key in flat) { const keys = key.split('.') keys.reduce((acc, k, i) => { if (i === keys.length - 1) acc[k] = flat[key] // 最后一层赋值 else acc[k] = acc[k] || {} // 中间层建空对象 return acc[k] }, result) } return result }
应用:qs 库的 stringify/parse 就是这套逻辑,前后端数据传输常用。

取值与遍历

4get 嵌套取值(lodash.get 实现)

场景:取深层属性 obj.user.address.city,任何一层为空都不会报错。前端取后端数据防 undefined 必备。

// 简易版:用 reduce 一层层取,支持点路径 'a.b.c' function get(obj, path, defaultVal) { const paths = Array.isArray(path) ? path : path.split('.') const result = paths.reduce((acc, key) => acc?.[key], obj) return result === undefined ? defaultVal : result } const obj = { user: { name: 'Tom', address: { city: '北京' } } } get(obj, 'user.address.city') // '北京' get(obj, 'user.phone.number', '未知') // '未知'(不会报错) get(obj, 'user.age') // undefined // 进阶版:支持数组下标 'users[0].name' function getPro(obj, path, defaultVal) { const paths = path.replace(/\[(\d+)\]/g, '.$1').split('.') const result = paths.reduce((acc, key) => acc?.[key], obj) return result === undefined ? defaultVal : result } getPro(obj, 'users[0].name') // 'users.0.name' 再 reduce
核心acc?.[key] 可选链是灵魂,避免 Cannot read property of undefined
5对象属性过滤 pick / omit(lodash)

场景:API 请求只发白名单字段(pick);密码字段不返回前端(omit)。

// pick:只保留指定字段 function pick(obj, keys) { return keys.reduce((acc, key) => { if (key in obj) acc[key] = obj[key] return acc }, {}) } const user = { name: 'Tom', age: 18, password: '123', email: 'a@b.com' } pick(user, ['name', 'age']) // { name:'Tom', age:18 } // omit:排除指定字段 function omit(obj, keys) { return Object.keys(obj).reduce((acc, key) => { if (!keys.includes(key)) acc[key] = obj[key] return acc }, {}) } omit(user, ['password']) // { name, age, email } —— 排除敏感字段 // 解构语法糖(推荐生产用) const { password, ...safeUser } = user // safeUser 不含 password
实战:返回用户信息给前端前,omit(user, ['password', 'salt', 'token']) 是必做操作,防止敏感数据泄露。
6对象深合并(deepMerge)

场景:合并多层配置对象,比如默认配置 + 用户配置。Object.assign 只合并第一层。

function deepMerge(target, ...sources) { if (!sources.length) return target const source = sources.shift() for (const key in source) { if (isObject(source[key]) && isObject(target[key])) { // 两边都是对象 → 递归合并 deepMerge(target[key], source[key]) } else { // 否则直接覆盖(source 覆盖 target) target[key] = source[key] } } return deepMerge(target, ...sources) } function isObject(item) { return item && typeof item === 'object' && !Array.isArray(item) } // 用例:合并默认配置和用户配置 const defaults = { api: { baseUrl: '/api', timeout: 5000 }, retry: 3 } const userConfig = { api: { timeout: 10000 }, // 只改 timeout,baseUrl 保留 debug: true } deepMerge({}, defaults, userConfig) // { api: { baseUrl:'/api', timeout:10000 }, retry:3, debug:true }
区别:Object.assign / 扩展运算符是浅合并,深层会被整体覆盖。deepMerge 是递归合并,保留各层默认值。
7对象深度比较(isEqual)

场景:判断两个对象内容是否相同(=== 比较的是引用)。React.memo、表单 dirty 判断常用。

function isEqual(a, b) { // ① 基本类型或同一引用 if (a === b) return true // ② 其中一个不是对象,或为 null if (typeof a !== 'object' || a === null || typeof b !== 'object' || b === null) return false // ③ 比较键的数量(快速失败) const keysA = Object.keys(a), keysB = Object.keys(b) if (keysA.length !== keysB.length) return false // ④ 递归比较每个属性 return keysA.every(key => Object.prototype.hasOwnProperty.call(b, key) && isEqual(a[key], b[key]) ) } isEqual({ a: 1, b: { c: 2 } }, { a: 1, b: { c: 2 } }) // true isEqual([1, [2, 3]], [1, [2, 3]]) // true isEqual({ a: 1 }, { a: 2}) // false
优化点:① 先比 === 快速通过;② 比键数量快速失败;③ 递归。可优化加 WeakMap 处理循环引用。

综合应用

8深度遍历对象(deepForEach / 找出所有某值)

场景:找出对象里所有等于某值的路径;批量改字段名;统计对象大小。

// 深度遍历:对每个叶子节点调用回调 function deepForEach(obj, callback, prefix = '') { Object.keys(obj).forEach(key => { const path = prefix ? `${prefix}.${key}` : key if (typeof obj[key] === 'object' && obj[key] !== null) { deepForEach(obj[key], callback, path) // 递归 } else { callback(obj[key], path, obj) // 叶子节点调回调 } }) } // 用例1:找出所有等于某值的路径 const data = { a: 'Tom', b: { c: 'Tom', d: 'Jerry' } } const found = [] deepForEach(data, (val, path) => { if (val === 'Tom') found.push(path) }) // found = ['a', 'b.c'] // 用例2:批量替换 null 为空字符串(后端数据清洗) deepForEach(formData, (val, path, parent) => { if (val === null) { const lastKey = path.split('.').pop() parent[lastKey] = '' } })
价值:这是表单数据清洗、字段重命名、空值过滤的通用工具,业务里非常实用。
9数组转树 / 树转数组(业务高频)

场景:后端返回扁平数组(带 pid),前端要构建成树给 Tree 组件。这是前端面试必考题。

// 列表 → 树(用 Map 索引,O(n)) function listToTree(list) { const map = {} // id → 节点 const tree = [] // ① 先建索引:每个节点加 children 数组 list.forEach(item => { map[item.id] = { ...item, children: [] } }) // ② 再建父子关系 list.forEach(item => { if (item.pid) { map[item.pid].children.push(map[item.id]) // 挂到父节点 } else { tree.push(map[item.id]) // 顶层节点 } }) return tree } const list = [ { id: 1, name: '总部', pid: null }, { id: 2, name: '技术部', pid: 1 }, { id: 3, name: '前端组', pid: 2 } ] listToTree(list) // [{ id:1, name:'总部', children:[{ id:2, name:'技术部', children:[{id:3,...}]}] }] // 树 → 列表(深度优先遍历) function treeToList(tree) { const result = [] function dfs(nodes) { for (const node of nodes) { const { children, ...rest } = node result.push(rest) if (children?.length) dfs(children) } } dfs(tree) return result }
关键:用 Map 建索引是 O(n)。如果双层 for 循环嵌套找父节点是 O(n²),面试会被追问性能。
10深拷贝补充:JSON 方法 / structuredClone 对比

场景:面试官常问"为什么不用 JSON.parse(JSON.stringify())?"要知道每种方法的边界。

// 方法1:JSON 序列化(最简单但有6个坑) const copy = JSON.parse(JSON.stringify(obj)) // ❌ 坑: // 1. 函数、undefined 会丢失 // 2. Symbol 属性会丢失 // 3. Date 变成字符串 // 4. RegExp 变成空对象 {} // 5. NaN/Infinity 变成 null // 6. 循环引用直接报错 // 方法2:structuredClone(现代浏览器原生,2022+) const copy = structuredClone(obj) // ✅ 支持:循环引用、Date、RegExp、Map、Set、ArrayBuffer // ❌ 不支持:函数(报错)、DOM 节点、Symbol 属性 // 方法3:自己手写(见 Q1)—— 面试必背 // 选型对照表: // ┌─────────────────┬────────┬──────────────────┬─────────┐ // │ 方法 │ 函数 │ 循环引用 │ 特殊类型 │ // ├─────────────────┼────────┼──────────────────┼─────────┤ // │ JSON │ ❌ │ ❌(报错) │ ❌ │ // │ structuredClone │ ❌ │ ✅ │ ✅ │ // │ 手写 deepClone │ 可处理 │ ✅ │ ✅ │ // └─────────────────┴────────┴──────────────────┴─────────┘
面试答题:JSON 方法简单但有 6 个坑;structuredClone 是现代方案但部分老项目兼容性差;生产级深拷贝要手写以处理所有边界。答出这三种的区别就是满分。
🎯 对象题复习重点:① 深拷贝(必考,循环引用是难点)② 数组转树(前端业务必考)③ get/omit/pick 三件套 ④ isEqual 深比较。
对象题核心就一个字:递归。能把递归写顺,对象题基本都能过。