20. 说说你对深拷贝和浅拷贝的理解?
问题解析
深拷贝和浅拷贝是JavaScript中处理对象复制的核心概念。理解它们的区别、实现方式以及应用场景,对于避免程序中的意外副作用和数据污染至关重要。
核心概念
数据类型存储
JavaScript中存在两大数据类型:
- 基本类型:Number、String、Boolean、Undefined、Null、Symbol,存储在栈中
- 引用类型:Object、Array、Function等,对象存储在堆中,引用存储在栈中
浅拷贝(Shallow Copy)
浅拷贝,指的是创建新的数据,这个数据有着原始数据属性值的一份精确拷贝。
特点:
- 如果属性是基本类型,拷贝的就是基本类型的值
- 如果属性是引用类型,拷贝的就是内存地址
- 即浅拷贝是拷贝一层,深层次的引用类型则共享内存地址
深拷贝(Deep Copy)
深拷贝开辟一个新的栈,两个对象属性完全相同,但是对应两个不同的地址,修改一个对象的属性,不会改变另一个对象的属性。
特点:
- 递归拷贝对象的所有层级
- 新旧对象完全独立,互不影响
- 会创建新的内存空间存储所有数据
详细解答
浅拷贝的实现方式
1. Object.assign
const obj = {
age: 18,
nature: ['smart', 'good'],
names: {
name1: 'fx',
name2: 'xka'
},
love: function () {
console.log('fx is a great girl');
}
};
const newObj = Object.assign({}, obj);
// 测试
newObj.age = 20;
console.log(obj.age); // 18,基本类型互不影响
newObj.names.name1 = 'new name';
console.log(obj.names.name1); // 'new name',引用类型共享内存
2. Array.prototype.slice()
const fxArr = ["One", "Two", "Three"];
const fxArrs = fxArr.slice(0);
fxArrs[1] = "love";
console.log(fxArr); // ["One", "Two", "Three"]
console.log(fxArrs); // ["One", "love", "Three"]
// 但对于嵌套数组
const arr = [[1, 2], [3, 4]];
const arrCopy = arr.slice();
arrCopy[0][0] = 'changed';
console.log(arr[0][0]); // 'changed',嵌套对象仍是浅拷贝
3. Array.prototype.concat()
const fxArr = ["One", "Two", "Three"];
const fxArrs = fxArr.concat();
fxArrs[1] = "love";
console.log(fxArr); // ["One", "Two", "Three"]
4. 拓展运算符
const fxArr = ["One", "Two", "Three"];
const fxArrs = [...fxArr];
fxArrs[1] = "love";
console.log(fxArr); // ["One", "Two", "Three"]
// 对象
const obj = { a: 1, b: { c: 2 } };
const objCopy = { ...obj };
objCopy.b.c = 3;
console.log(obj.b.c); // 3,嵌套对象仍是浅拷贝
深拷贝的实现方式
1. JSON.parse(JSON.stringify())
const obj2 = JSON.parse(JSON.stringify(obj1));
// 优点:简单快捷
// 缺点:
// 1. 会忽略 undefined
// 2. 会忽略 symbol
// 3. 会忽略函数
// 4. 不能处理循环引用
// 5. 不能处理 Date、RegExp、Map、Set等特殊对象
const obj = {
name: 'A',
name1: undefined,
name3: function() {},
name4: Symbol('A')
};
const obj2 = JSON.parse(JSON.stringify(obj));
console.log(obj2); // {name: "A"}
2. Lodash的cloneDeep
const _ = require('lodash');
const obj1 = {
a: 1,
b: { f: { g: 1 } },
c: [1, 2, 3]
};
const obj2 = _.cloneDeep(obj1);
console.log(obj1.b.f === obj2.b.f); // false
3. jQuery.extend()
const $ = require('jquery');
const obj1 = {
a: 1,
b: { f: { g: 1 } },
c: [1, 2, 3]
};
const obj2 = $.extend(true, {}, obj1);
console.log(obj1.b.f === obj2.b.f); // false
4. 手写递归实现
function deepClone(obj, hash = new WeakMap()) {
// 如果是null或者undefined,不进行拷贝操作
if (obj === null) return obj;
// 处理Date
if (obj instanceof Date) return new Date(obj);
// 处理RegExp
if (obj instanceof RegExp) return new RegExp(obj);
// 处理基本类型
if (typeof obj !== "object") return obj;
// 处理循环引用
if (hash.get(obj)) return hash.get(obj);
// 找到的是所属类原型上的constructor,指向当前类本身
let cloneObj = new obj.constructor();
hash.set(obj, cloneObj);
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
// 实现一个递归拷贝
cloneObj[key] = deepClone(obj[key], hash);
}
}
return cloneObj;
}
完整的深拷贝实现
function deepClone(obj, hash = new WeakMap()) {
// 处理null
if (obj === null) return null;
// 处理基本类型
if (typeof obj !== 'object') return obj;
// 处理Date
if (obj instanceof Date) return new Date(obj);
// 处理RegExp
if (obj instanceof RegExp) return new RegExp(obj);
// 处理Map
if (obj instanceof Map) {
const mapCopy = new Map();
obj.forEach((value, key) => {
mapCopy.set(deepClone(key, hash), deepClone(value, hash));
});
return mapCopy;
}
// 处理Set
if (obj instanceof Set) {
const setCopy = new Set();
obj.forEach(value => {
setCopy.add(deepClone(value, hash));
});
return setCopy;
}
// 处理循环引用
if (hash.has(obj)) return hash.get(obj);
// 处理数组
if (Array.isArray(obj)) {
const arrCopy = [];
hash.set(obj, arrCopy);
obj.forEach((item, index) => {
arrCopy[index] = deepClone(item, hash);
});
return arrCopy;
}
// 处理普通对象
const objCopy = {};
hash.set(obj, objCopy);
Object.keys(obj).forEach(key => {
objCopy[key] = deepClone(obj[key], hash);
});
// 拷贝Symbol类型的key
Object.getOwnPropertySymbols(obj).forEach(sym => {
objCopy[sym] = deepClone(obj[sym], hash);
});
return objCopy;
}
深入理解
浅拷贝与深拷贝的区别
// 浅拷贝示例
const obj1 = {
name: 'init',
arr: [1, [2, 3], 4],
};
const obj3 = shallowClone(obj1);
obj3.name = "update";
obj3.arr[1] = [5, 6, 7];
console.log('obj1', obj1);
// obj1 { name: 'init', arr: [ 1, [ 5, 6, 7 ], 4 ] }
console.log('obj3', obj3);
// obj3 { name: 'update', arr: [ 1, [ 5, 6, 7 ], 4 ] }
// 注意:obj1.arr也被修改了
// 深拷贝示例
const obj4 = deepClone(obj1);
obj4.name = "update";
obj4.arr[1] = [5, 6, 7];
console.log('obj1', obj1);
// obj1 { name: 'init', arr: [ 1, [ 2, 3 ], 4 ] }
console.log('obj4', obj4);
// obj4 { name: 'update', arr: [ 1, [ 5, 6, 7 ], 4 ] }
// obj1保持不变
内存示意图
浅拷贝:
栈内存 堆内存
obj1 ----------> {name: 'init', arr: [1, [2,3], 4]}
^
obj3 --------------|
深拷贝:
栈内存 堆内存
obj1 ----------> {name: 'init', arr: [1, [2,3], 4]}
obj3 ----------> {name: 'init', arr: [1, [2,3], 4]} (新的内存空间)
最佳实践
1. 根据场景选择
// 只有一层基本类型,浅拷贝即可
const simpleObj = { a: 1, b: 2 };
const copy = { ...simpleObj };
// 有多层嵌套,需要深拷贝
const nestedObj = { a: { b: { c: 1 } } };
const deepCopy = _.cloneDeep(nestedObj);
2. 使用结构化克隆(现代浏览器)
// structuredClone是浏览器内置的深拷贝方法
const original = {
date: new Date(),
map: new Map([[1, 'one']]),
nested: { a: 1 }
};
const clone = structuredClone(original);
// 限制:
// 1. 不能克隆函数
// 2. 不能克隆DOM节点
// 3. 不能克隆某些特殊对象
3. 性能考虑
// 对于大对象,深拷贝可能很耗时
// 考虑使用不可变数据模式
// Immutable.js
import { Map } from 'immutable';
const map1 = Map({ a: 1, b: 2, c: 3 });
const map2 = map1.set('b', 50);
// map1保持不变,map2是新的对象,但共享未修改的部分
面试要点
- 本质区别:浅拷贝只复制一层,深拷贝递归复制所有层级
- 实现方式:能够列举多种浅拷贝和深拷贝的实现方法
- JSON方法的局限:了解JSON.parse(JSON.stringify())的缺陷
- 循环引用:手写深拷贝时需要处理循环引用
- 特殊对象:Date、RegExp、Map、Set等特殊对象的处理
常见问题
Q:浅拷贝和深拷贝有什么区别? A:浅拷贝只复制对象的第一层属性,如果属性是引用类型,拷贝的是内存地址;深拷贝递归复制所有层级的属性,创建完全独立的新对象。
Q:JSON.parse(JSON.stringify())有什么缺点? A:会忽略undefined、symbol和函数;不能处理循环引用;不能正确处理Date、RegExp、Map、Set等特殊对象。
Q:如何实现一个完美的深拷贝? A:需要递归处理所有数据类型,包括基本类型、对象、数组、Date、RegExp、Map、Set等,同时使用WeakMap处理循环引用。