面试官:说说深浅拷贝?我:从Object.assign()到递归深拷贝的奇幻之旅 🚀
面试官:请谈谈你对深浅拷贝的理解?
我:(眼睛一亮)那我们就从Object.assign()这个神奇的API开始说起吧!
一、Object.assign():浅拷贝的魔法棒 ✨
1.1 基础概念
Object.assign()是ES6提供的对象操作方法,用于将一个或多个源对象的可枚举属性复制到目标对象。它就像复印机一样,能快速获取源对象的属性:
const target = { a: 1 };
const source = { b: 2 };
const result = Object.assign(target, source);
console.log(result); // { a: 1, b: 2 }
console.log(result === target); // true
1.2 特性揭秘
这个复印机有些有趣的特性:
- 后来居上:属性冲突时,后面的源对象会覆盖前面的
Object.assign({ a: 1 }, { a: 2, b: 3 }, { b: 4 });
// { a: 2, b: 4 } - 后来者居上!
- 特殊值处理:
null和undefined会被忽略
Object.assign({}, null, undefined); // 安全通过,无事发生
- 原型链限制:不会复制原型链上的属性
const parent = { inherited: 'from parent' };
const child = Object.create(parent);
child.own = 'mine';
Object.assign({}, child); // { own: 'mine' } - 只复制自己的财产
1.3 业务场景:参数合并神器
在实际开发中,Object.assign()是处理配置合并的利器:
function createUser(options) {
const defaults = {
name: '匿名用户',
age: 18,
isAdmin: false
};
// 用户配置覆盖默认配置
return Object.assign({}, defaults, options);
}
const user = createUser({ name: '小明', age: 20 });
console.log(user);
// { name: '小明', age: 20, isAdmin: false }
二、深浅拷贝的深渊 🌌
2.1 赋值 vs 浅拷贝 vs 深拷贝
| 方式 | 特点 | 示例 |
|---|---|---|
| 赋值 | 共享内存地址 | const b = a |
| 浅拷贝 | 只复制第一层 | Object.assign(), slice() |
| 深拷贝 | 完全独立的内存副本 | JSON.parse(JSON.stringify()) |
2.2 浅拷贝的陷阱
当对象有嵌套结构时,浅拷贝就会露出獠牙:
如果源对象的属性值是引用类型,那么目标对象的属性值会指向源对象的属性值的内存地址,改变目标对象的属性值,会影响源对象的属性值,不安全
const source = {
profile: { name: '小明' },
hobbies: ['篮球', '足球']
};
const shallowCopy = Object.assign({}, source);
// 修改拷贝后的对象
shallowCopy.profile.name = '小红';
shallowCopy.hobbies.push('游泳');
console.log(source.profile.name); // '小红' - 源数据被污染!
console.log(source.hobbies); // ['篮球', '足球', '游泳']
2.3 数组的浅拷贝方法
除了Object.assign(),数组也有自己的浅拷贝方式:
// 方法1:slice()
const arr1 = [1, 2, {a: 3}];
const copy1 = arr1.slice();
// 方法2:concat()
const copy2 = arr1.concat();
// 方法3:展开运算符
const copy3 = [...arr1];
concat()用于合并数组,但不传参数时,相当于复制原数组并返回新数组。slice()不传参数时,等价于slice(0),表示从头到尾复制,返回一个新数组。
slice()、concat() 和 ... 都会返回一个全新的数组,是实现数组浅拷贝的常用且有效的方式。其中 展开运算符 ... 是现代 JavaScript 中最推荐的写法,语法清晰、可读性强。
三、深拷贝的圣杯 🏆
3.1 JSON大法 - 简单粗暴
最简单的深拷贝方法:
深拷贝原理是:通过序列化再反序列化,切断所有引用关系,从而生成一个完全独立的新对象。简单有效,但有局限,不能处理函数(不知道怎么序列化函数)、symbol也不会拷贝、undefined 会被忽略、循环引用等特殊值。
const source = {
profile: { name: '小明' },
hobbies: ['篮球', '足球']
};
const deepCopy = JSON.parse(JSON.stringify(source));
适用场景
- 拷贝纯数据对象(只包含数组、对象、字符串、数字、布尔值)
- 临时使用,追求简洁
- 不涉及函数、日期、循环引用等复杂结构
3.2 JSON方法的局限性
但这种方法有三大致命缺陷:
- 函数丢失 - 函数不会被复制
- 特殊值消失 -
undefined、Symbol类型会消失 - 循环引用崩溃 - 遇到循环引用会抛出错误
const problemObj = {
func: () => console.log('hello'),
undef: undefined,
sym: Symbol('id'),
// 循环引用
self: null
};
problemObj.self = problemObj;
JSON.parse(JSON.stringify(problemObj));
// 报错: Converting circular structure to JSON
四、手写深拷贝:征服复杂对象 🛠️
4.1 基础版本实现
解决JSON方法的局限性,我们需要自己打造深拷贝工具:
function deepClone(source) {
// 基本类型直接返回
if (source === null || typeof source !== 'object') {
return source;
}
// 处理数组
if (Array.isArray(source)) {
return source.map(item => deepClone(item));
}
// 处理普通对象
const clone = {};
for (const key in source) {
if (source.hasOwnProperty(key)) {
clone[key] = deepClone(source[key]);
}
}
return clone;
}
hasOwnProperty在深拷贝时,我们通常 只关心对象“自己定义的属性” ,而不希望把继承来的属性也拷贝过来(比如
toString、constructor等)。否则可能会:
- 拷贝冗余/无用的属性
- 引发性能问题
- 甚至导致无限递归(某些特殊对象) 所以用
hasOwnProperty来过滤掉继承属性,只遍历“自有属性”。
4.2 高级版本:解决循环引用
基础版本遇到循环引用会栈溢出,我们需要使用WeakMap来记录已拷贝对象:
function deepClone(source, map = new WeakMap()) {
// 基本类型直接返回
if (source === null || typeof source !== 'object') {
return source;
}
// 解决循环引用
if (map.has(source)) {
return map.get(source);
}
// 初始化克隆对象
const clone = Array.isArray(source) ? [] : {};
map.set(source, clone);
// 递归拷贝
for (const key in source) {
if (source.hasOwnProperty(key)) {
clone[key] = deepClone(source[key], map);
}
}
return clone;
}
// 测试循环引用
const obj = { a: 1 };
obj.self = obj;
const clone = deepClone(obj);
console.log(clone.self === clone); // true - 完美解决!
4.3 终极版本:支持各种类型
完善各种特殊类型的处理:
function deepClone(source, map = new WeakMap()) {
// 处理基本类型
if (source === null || typeof source !== 'object') {
return source;
}
// 处理循环引用
if (map.has(source)) return map.get(source);
// 处理特殊对象类型
switch (true) {
case source instanceof Date:
return new Date(source);
case source instanceof RegExp:
return new RegExp(source);
case Array.isArray(source):
const arrClone = [];
map.set(source, arrClone);
source.forEach((item, i) => {
arrClone[i] = deepClone(item, map);
});
return arrClone;
case source instanceof Map:
const mapClone = new Map();
map.set(source, mapClone);
source.forEach((value, key) => {
mapClone.set(key, deepClone(value, map));
});
return mapClone;
case source instanceof Set:
const setClone = new Set();
map.set(source, setClone);
source.forEach(value => {
setClone.add(deepClone(value, map));
});
return setClone;
default:
// 普通对象
const objClone = Object.create(Object.getPrototypeOf(source));
map.set(source, objClone);
// 处理Symbol属性
const symbols = Object.getOwnPropertySymbols(source);
[...Object.keys(source), ...symbols].forEach(key => {
objClone[key] = deepClone(source[key], map);
});
return objClone;
}
}
五、面试技巧:如何惊艳面试官 💫
当面试官问到深浅拷贝时,可以这样展示:
-
从API切入:先展示
Object.assign()的熟练使用"我在项目中常用Object.assign()处理配置合并..." -
深入原理:解释浅拷贝的局限性
"但要注意它只是浅拷贝,嵌套对象会共享引用..." -
解决方案:展示深拷贝的各种方案
"对于简单场景可以用JSON方法,但要注意它的局限性..." -
终极武器:手写完整深拷贝
"在复杂场景下,我通常会实现这样的深拷贝函数..." -
性能考量:补充优化思路
"对于超大对象,可以考虑使用循环代替递归避免栈溢出..."
六、总结:拷贝的艺术 🎨
| 拷贝方式 | 使用场景 | 注意事项 |
|---|---|---|
| 赋值操作 | 简单数据类型传递 | 共享内存地址 |
| Object.assign() | 单层对象合并/拷贝 | 嵌套对象仍是引用 |
| JSON方法 | 简单对象深拷贝 | 丢失函数/Symbol/循环引用问题 |
| 手写深拷贝 | 复杂对象/需要完整复制 | 注意循环引用和特殊类型处理 |
💡 黄金法则:根据你的业务场景选择拷贝方式:
- 简单数据?直接用
=赋值- 单层对象?
Object.assign()或展开运算符- 简单嵌套?
JSON.parse(JSON.stringify())- 复杂对象?上终极深拷贝函数