在JavaScript中,对象的拷贝是一项常见的操作。浅拷贝和深拷贝是两种常用的拷贝方式。浅拷贝只复制对象的引用,而深拷贝创建了一个全新的对象,包含与原始对象相同的值和结构。深拷贝和浅拷贝各有适用的场景和注意事项。本文将详细介绍如何实现一个完整而优雅的深拷贝函数,处理循环引用和特殊类型,优化性能,并探讨深拷贝和浅拷贝的应用场景、注意事项和相关属性。
首先我们来认识一下浅拷贝,什么是浅拷贝呢?
浅拷贝是指将一个对象的属性值复制到另一个对象,如果属性值是基本数据类型(如数字、字符串、布尔等),则直接复制该值;如果属性值是引用数据类型(如对象、数组等),则复制的是其引用地址,而不是实际的对象本身。
浅拷贝的实现方式有哪些?
1、Object.assign()
const target = { a: 1, b: 2 };
const source = { b: 4, c: 5 };
const returnedTarget = Object.assign(target, source);
console.log(target);
// { a: 1, b: 4, c: 5 }
console.log(returnedTarget === target);
// true
语法:
Object.assign(target, ...sources)
参数:
target:需要应用源对象属性的目标对象,修改后将作为返回值。
sources:一个或多个包含要应用的属性的源对象。
如果目标对象与源对象具有相同的键(属性名),则目标对象中的属性将被源对象中的属性覆盖,后面的源对象的属性将类似地覆盖前面的源对象的同名属性。
Object.assign()
方法只会拷贝源对象可枚举的的自有属性到目标对象。该方法在源对象上使用 [[Get]]
,在目标对象上使用 [[Set]]
,因此它会调用 getter 和 setter。故它对属性进行赋值,而不仅仅是复制或定义新的属性。如果合并源对象包含 getter 的新属性到原型中,则可能不适合使用此方法。
2、扩展运算符(…)
let source = { name: '张三', age: 30,sex:'女' };
let shallowCopy = { ...source };
扩展运算符是三个点(…),它可以将一个数组或对象展开成多个元素,或将多个元素合并成一个组或数对象。
扩展运算符的作用
扩展运算符可以用于以下场景:
数组的展、合并、复制和解构赋值
对象的展开、合并、复制和解构赋值
函数参数的传递和返回值的处理
扩展运算符的使用限制
扩展运算符只能用于可迭代对象(如数组、字符串、Set、Map等),不能用于普通对象。
扩展运算符的性能问题
性能问题主要是由于扩展运算符在使用时会创建新的数组或对象,导致内存占用增加和对象复制的开销。特别是在大型数据集上使用扩展运算符时,会占用较多的内存并导致性能下降。另外,使用扩展运算符进行浅拷贝时,对于嵌套的对象或数组,仅会复制引用而不是真正的拷贝。这可能导致某些意外的副作用,因为原始对象的修改会影响到被复制的对象。
3、Array.prototype.concat()(用于数组的浅拷贝)
const array1 = ['a', 'b', 'c'];
const array2 = ['d', 'e', 'f'];
const array3 = array1.concat(array2);
console.log(array3);
// Array ["a", "b", "c", "d", "e", "f"]
语法:
concat()
concat(value0)
concat(value0, value1)
concat(value0, value1, /* … ,*/ valueN)
参数:数组和/或值,将被合并到一个新的数组中。如果省略了所有 valueN
参数,则 concat
会返回调用此方法的现存数组的一个浅拷贝。
concat
方法创建一个新数组。该数组将首先由调用它的对象中的元素填充。然后,对于每个参数,它的值将被连接到数组中——对于普通对象或基元,参数本身将成为最终数组的一个元素;对于属性Symbol.isConcatSpreadable设置为真的数组或类数组对象,参数的每个元素都将是独立地添加到最终数组中。concat
方法不会递归到嵌套数组参数中。
concat()
方法是一种复制方法。它不会更改 this
或作为参数提供的任何数组,而是返回包含与原始数组中的元素相同的元素的浅拷贝。
如果任何源数组是稀疏数组,concat()
方法会保留空槽。
concat()
方法是通用的。this
值的处理方式与其他参数相同(除了它会先转换为对象),这意味着普通对象将直接添加到结果数组中,而 @@isConcatSpreadable
属性为真值的类数组对象将展开并添加到数组中。
这几个方法比较常用。
后面呢在讲一下深拷贝。什么是深拷贝呢?
深拷贝是指将一个对象从堆内存中完全复制一份到栈内存中,新对象与原对象是完全独立的,修改新对象不会影响原对象。
深拷贝的实现方法有哪些?
1、递归实现
function deepClone(source) {
if (typeof source !== 'object' || source == null) {
return source;
}
const target = Array.isArray(source) ? [] : {};
for (const key in source) {
if (Object.prototype.hasOwnProperty.call(source, key)) {
if (typeof source[key] === 'object' && source[key] !== null) {
target[key] = deepClone(source[key]);
} else {
target[key] = source[key];
}
}
}
return target;
}
2、JSON.stringify()和JSON.parse()
let a = {a:1,b:2}
let b = JSON.parse(JSON.stringify(a))
a.a = 11
a // {a:11,b:2}
b // {a:1,b:2}
但是他有缺陷
let a = {
name: 'Jack',
age: 18,
hobbit: ['sing', {type: 'sports', value: 'run'}],
score: {
math: 'A',
},
run: function() {},
walk: undefined,
fly: NaN,
cy: null,
date: new Date()
}
let b = JSON.parse(JSON.stringify(a))
取不到值为 undefined 的 key;如果对象里有函数,函数无法被拷贝下来;无法拷贝copyObj对象原型链上的属性和方法;对象转变为 date 字符串。
然后,在实际项目中,我们深浅拷贝,一般使用lodash库。