从 Object.assign () 开场:浅拷贝的 "入门级选手"
面试时一提到 "深浅拷贝",面试官十有八九会先甩来一句:"聊聊 Object.assign () 吧?" 别慌,这可不是单纯考 API,藏着对 "拷贝" 的底层考察呢。
先明确它的定义:Object.assign () 会把一个或多个源对象的 "可枚举属性" 复制到目标对象,最后返回修改后的目标对象。划重点:它不会返回全新对象,而是直接修改目标对象本身。
咱们拿代码举例,直观感受下:
// 目标对象和两个源对象
const target = { a: 1 };
const source1 = { b: 2 };
const source2 = { c: 3 };
// 用Object.assign()合并
const result = Object.assign(target, source1, source2);
// 打印看看:result和target是同一个对象!
console.log(result); // {a:1, b:2, c:3}
console.log(target); // {a:1, b:2, c:3}
console.log(result === target); // true——果然是同一个对象
要是源对象和目标对象有同名属性呢?简单,后面的会覆盖前面的:
const target = { a: 1, b: 2 };
const source = { b: 3, c: 4 };
Object.assign(target, source);
console.log(target); // {a:1, b:3, c:4}——source的b把target的b覆盖了
但最关键的是它的 "浅拷贝" 特性 ——只拷贝表层属性,遇到嵌套对象(深层属性)只会复制引用。比如这样:
const target = { a: 1 };
const source = {
b: { name: '小安', hobbies: ['篮球', '足球'] }, // 嵌套对象
c: 1
};
// 用Object.assign()拷贝
Object.assign(target, source);
// 试着修改target的嵌套属性
target.b.name = '安安';
target.b.hobbies.push('唱歌');
// 再改个表层属性
target.c = 2;
// 看看源对象source的变化:
console.log(source.b.name); // "安安"——嵌套对象的属性被改了!
console.log(source.b.hobbies); // ["篮球", "足球", "唱歌"]——嵌套数组也被改了
console.log(source.c); // 1——表层属性没被改
瞧见没?嵌套的b对象其实是 "共享引用",改了 target 里的,source 里的也跟着变;但表层的c是直接复制值,改了不影响源对象。这就是浅拷贝的 "小心机"—— 只负责 "表面功夫"~
浅拷贝与深拷贝的区别:先搞懂 "赋值" 和 "引用"
很多人分不清 "赋值" 和 "拷贝",其实核心在 JavaScript 的 "数据类型存储方式":
-
基本数据类型(number、string 等) :值存在栈内存,赋值时直接复制值。比如
let a=1; let b=a; b=2;,a 还是 1,互不影响。 -
引用数据类型(对象、数组等) :值存在堆内存,变量里存的是 "堆内存地址"(引用)。直接赋值时,复制的是引用 —— 比如
let obj1={name:'张三'}; let obj2=obj1;,obj1 和 obj2 指向同一个对象,改 obj2 的属性,obj1 也会变。
而 "拷贝" 的目的,就是让新变量和原变量脱离关联:
- 浅拷贝:只复制 "表层"。如果属性是基本类型,直接复制值;如果是引用类型,复制引用(所以嵌套对象还是会关联)。
- 深拷贝:不管嵌套多少层,都彻底复制一份新的,新对象和原对象完全独立,改谁都不影响对方。
浅拷贝的常用方法:不止 Object.assign ()
除了 Object.assign (),这些场景也常用浅拷贝,面试可能会问:
-
数组的 slice () :返回新数组,但嵌套数组仍关联
const arr = [1, 2, [3, 4]]; const newArr = arr.slice(); newArr[2][0] = 99; console.log(arr); // [1,2,[99,4]]——嵌套数组被改了 -
数组的 concat () :和 slice () 类似,表层拷贝
const arr = [1, 2, [3, 4]]; const newArr = [].concat(arr); newArr[2][0] = 99; console.log(arr); // [1,2,[99,4]]——同样关联 -
扩展运算符(...) :对象和数组都能用,表层拷贝
// 数组 const arr = [1, 2, [3, 4]]; const newArr = [...arr]; // 对象 const obj = { a: 1, b: { c: 2 } }; const newObj = { ...obj };
这些方法的核心是 "只拷贝第一层",适合结构简单的对象 / 数组 —— 要是数据里没嵌套,浅拷贝足够用了~
简单深拷贝:JSON.parse (JSON.stringify ()) 的 "甜蜜陷阱"
如果需要深拷贝,很多人第一反应是JSON.parse(JSON.stringify(源对象))—— 这招确实简单,不用手写逻辑,比如:
const source = {
b: { name: '小安', hobbies: ['篮球', '足球'] },
c: 1
};
// 深拷贝
const newObj = JSON.parse(JSON.stringify(source));
// 修改newObj的属性
newObj.b.name = '安安';
newObj.c = 2;
// 看看源对象:完全没变化!
console.log(source.b.name); // "小安"
console.log(source.c); // 1
但它有个大问题:对 "特殊值" 不友好。因为 JSON 的序列化规则很严格,遇到这些情况会 "翻车":
-
函数、Symbol、undefined:直接忽略(序列化后消失)
const arr = [ { name: '张三', hobbies: ['篮球'] }, function() { console.log('我是函数'); } // 函数 ]; const newArr = JSON.parse(JSON.stringify(arr)); console.log(newArr); // [ {name: '张三', hobbies: ['篮球'] }, null ]——函数没了,变成null -
循环引用:直接报错(比如对象自己引用自己)
const obj = { a: 1 }; obj.self = obj; // 循环引用:obj的self属性指向自己 JSON.parse(JSON.stringify(obj)); // 报错:Converting circular structure to JSON
所以这招只适合 "纯数据对象"(没有特殊值、没有循环引用),别指望它能搞定所有场景哦~
高级深拷贝:手写实现(面试官的 "终极大招")
当面试官问 "你能手写一个深拷贝吗",其实是在考这几点:递归能力、边界处理(循环引用、特殊类型)。咱们一步步来。
第一步:基础递归(处理对象和数组)
先搞定最基本的:如果是对象 / 数组,就递归拷贝每个属性;如果是基本类型,直接返回。代码如下:
function clone(source) {
// 先判断类型:如果是对象(且不是null,因为typeof null也是'object')
if (typeof source === 'object' && source !== null) {
// 数组和对象要分开处理:数组初始化[],对象初始化{}
let cloneTarget = Array.isArray(source) ? [] : {};
// 遍历属性,递归拷贝
for (const key in source) {
// 只拷贝自身属性(排除原型链上的)
if (source.hasOwnProperty(key)) {
cloneTarget[key] = clone(source[key]);
}
}
return cloneTarget;
} else {
// 基本类型直接返回(值类型,不会关联)
return source;
}
}
测试下普通嵌套对象:
const target = {
a: 1,
b: { c: 2 },
d: [3, 4]
};
const newObj = clone(target);
newObj.b.c = 99;
newObj.d[0] = 88;
console.log(target.b.c); // 2——没被改,成功!
console.log(target.d[0]); // 3——也没被改,不错~
但这版有个致命问题:遇到循环引用会栈溢出。比如拷贝obj.self = obj,递归会无限循环,直接报错。
第二步:用 WeakMap 解决循环引用
循环引用的核心是 "重复拷贝同一个对象",所以我们需要一个 "缓存容器":拷贝前先查缓存,若已拷贝过,直接返回拷贝后的对象;没拷贝过就存起来。
推荐用WeakMap做缓存(比 Map 好,键是弱引用,不影响垃圾回收):
function clone(target, map = new WeakMap()) {
if (typeof target === 'object' && target !== null) {
// 先查缓存:如果之前拷贝过这个对象,直接返回
if (map.has(target)) {
return map.get(target);
}
// 初始化拷贝目标(数组/对象)
let cloneTarget = Array.isArray(target) ? [] : {};
// 存到缓存:key是原对象,value是拷贝后的对象
map.set(target, cloneTarget);
// 递归拷贝属性
for (const key in target) {
if (target.hasOwnProperty(key)) {
cloneTarget[key] = clone(target[key], map); // 把map传下去
}
}
return cloneTarget;
} else {
return target;
}
}
再测循环引用:
const obj = { a: 1 };
obj.self = obj; // 循环引用
const newObj = clone(obj);
console.log(newObj.self === newObj); // true——拷贝成功,没报错!
这样就解决了循环引用问题~
第三步:扩展处理特殊对象(可选进阶)
实际开发中可能遇到 Date、RegExp 等特殊对象,它们的拷贝需要特殊处理(比如 Date 直接复制时间戳,RegExp 要保留 flags)。可以再加一层类型判断:
function clone(target, map = new WeakMap()) {
// 处理Date
if (target instanceof Date) {
return new Date(target);
}
// 处理RegExp
if (target instanceof RegExp) {
return new RegExp(target.source, target.flags);
}
// 剩下的逻辑和之前一样(对象/数组/基本类型)
if (typeof target === 'object' && target !== null) {
if (map.has(target)) {
return map.get(target);
}
let cloneTarget = Array.isArray(target) ? [] : {};
map.set(target, cloneTarget);
for (const key in target) {
if (target.hasOwnProperty(key)) {
cloneTarget[key] = clone(target[key], map);
}
}
return cloneTarget;
} else {
return target;
}
}
这样的手写深拷贝就比较完善了 —— 面试时能写出带循环引用处理的版本,基本就能让面试官点头了~
面试官的考察逻辑:从 API 到底层,一环扣一环
为啥面试官总揪着深浅拷贝不放?其实是想通过 "拷贝" 这个小切入点,考察你的知识链:
-
API 细节:比如 Object.assign () 是否返回新对象?是否拷贝不可枚举属性?(答案:不返回新对象,只拷贝可枚举属性)
-
业务场景:你是否真的用过?比如用 Object.assign () 合并配置项(用户传的参数 + 默认参数):
function createUser(options) { const defaults = { name: '匿名', age: 18 }; // 用空对象当目标,避免修改defaults return Object.assign({}, defaults, options); } -
底层原理:是否理解引用类型的存储机制?(堆内存 + 栈引用)
-
边界处理:是否考虑到循环引用、特殊类型?(这是区分 "会用" 和 "懂原理" 的关键)
总结:别死记,按场景选方法
最后给个 "拷贝工具选型指南",实际开发 / 面试都能用:
-
简单数据类型:直接用
=赋值,足够了。 -
浅结构对象 / 数组(无嵌套):用 Object.assign ()、扩展运算符(
...),简单高效。 -
纯数据深拷贝(无函数、循环引用):用
JSON.parse(JSON.stringify()),省事。 -
复杂深拷贝(有嵌套、特殊类型、循环引用):手写深拷贝(按上面的步骤),或用成熟库(比如 lodash 的
_.cloneDeep)。
深浅拷贝看似基础,实则藏着对 JavaScript 内存模型的理解。下次面试官再问,别只说 "Object.assign () 是浅拷贝",把 "场景 + 原理 + 实现" 串起来讲,保管能让他眼前一亮~