一、深拷贝与浅拷贝的区别

1、浅拷贝

浅拷贝只复制对象的一层属性。如果属性值是基本数据类型(如字符串、数字、布尔值等),则直接复制其值;如果属性值是引用数据类型(如数组、对象等),则复制其引用(即地址),而不是复制实际的对象或数组本身。因此,在浅拷贝后,原始对象和复制对象会共享相同的引用类型属性。

举个例子:

let original = { a: 1, b: { c: 2 } };
let shallowCopy = Object.assign({}, original);

shallowCopy.b.c = 20;
console.log(original.b.c); // 输出:20

在上面的例子中,修改 shallowCopy 的属性 b.c 同时也改变了 original 中对应的值,因为它们指向同一个内存地址。

2、深拷贝

与浅拷贝相反,深拷贝会复制对象的所有层级,创建一个完全独立的副本。这意味着原始对象和复制对象不会共享任何引用类型的属性。无论多么复杂的对象结构,深拷贝都会递归复制其所有层级,生成一个结构完全相同但完全独立的对象。

let original = { a: 1, b: { c: 2 } };
// 假设deepClone是一个有效的深拷贝函数
let deepCopy = deepClone(original); 

deepCopy.b.c = 20;
console.log(original.b.c); // 输出:2

在这个例子中,修改 deepCopy 的属性 b.c 不会影响到 original 中对应的值,因为 deepCopyoriginal 的一个完全独立的副本。

总结差异

  • 浅拷贝只复制对象的第一层属性,而深拷贝会复制对象的所有层级。
  • 浅拷贝中,如果原始对象的属性值是引用类型,则复制的是这个引用,而不是引用的值本身,导致原始对象和复制对象的这些属性实际上是共享的。
  • 深拷贝创建了一个完全独立的对象副本,不共享任何属性或嵌套的引用,因此更加占用内存空间,并可能涉及更复杂的复制过程。

二、浅拷贝的使用

浅拷贝的方法有多种。这些技术主要复制对象的一级属性,如果属性值是引用类型(例如数组或其他对象),则复制的是引用地址而不是真实的独立对象。

1. 使用展开运算符(Spread Operator)

const original = { a: 1, b: { c: 2 } };
const copy = { ...original };

展开运算符 ... 可以用来复制对象的所有可枚举属性到一个新的对象中。

2. 使用 Object.assign()

const original = { a: 1, b: { c: 2 } };
const copy = Object.assign({}, original);

Object.assign() 方法可以用来将所有可枚举属性的值从一个或多个源对象复制到目标对象,并返回目标对象。

3. 数组的 slice() 方法

对于数组,可以使用 slice() 方法实现浅拷贝。

const originalArray = [1, 2, { a: 3 }];
const copiedArray = originalArray.slice();

这里的 slice() 方法没有指定开始和结束参数,因此会返回原数组的一个拷贝。但请注意,对于数组内部的对象,拷贝的仍然是引用。

4. 数组的 concat() 方法

const originalArray = [1, 2, { a: 3 }];
const copiedArray = originalArray.concat();

slice() 类似,concat() 在这种用法下也会返回数组的一个浅拷贝。注意的是: 数组中都是简单数据类型,可以说是深拷贝,如果数组中有复杂数据类型,它就浅拷贝

5. 使用Array.from()对数组进行浅拷贝

const originalArray = [1, 2, { a: 3 }];
const copiedArray = Array.from(originalArray);

Array.from() 方法可以从一个类似数组或可迭代的对象创建一个新的、浅拷贝的数组实例。

注意事项

以上方法都能实现浅拷贝,但它们只针对对象的第一层属性。如果对象中包含嵌套对象,则这些嵌套对象不会被复制,复制的只是嵌套对象的引用。因此,修改原始对象或复制对象中的嵌套对象可能会影响到另一个对象。

三、深拷贝的使用

1. JSON 方法

利用 JSON.stringify()JSON.parse() 可以快速实现一个对象的深拷贝。但是,它不支持复制函数、循环引用等。

function deepClone(obj) {
    return JSON.parse(JSON.stringify(obj));
}

const obj = { a: 1, b: { c: 2 } };
const clonedObj = deepClone(obj);
console.log(clonedObj); // { a: 1, b: { c: 2 } }

注意:但它有几个明显的弊端,这些限制使得它不能在所有情况下都适用:

(1)丢失函数:如果对象中含有函数,这些函数不会被克隆。JSON.stringify() 在遇到函数时会将其忽略,因此在克隆后的对象中,这些位置将不再有任何函数存在。

const obj = {
    data: "data",
    func: function() { console.log(this.data); }
};
const clonedObj = JSON.parse(JSON.stringify(obj));
// clonedObj 不包含 func 属性。

(2)无法复制特殊对象:某些特殊的对象类型(如 Date, RegExp, Map, Set 等)在经过 JSON.stringify()JSON.parse() 处理后会失去其原始类型信息,变成普通的对象或者其他结构。

const obj = { date: new Date() };
const clonedObj = JSON.parse(JSON.stringify(obj));
// clonedObj.date 变为了字符串,而不是一个 Date 对象。

(3)循环引用问题:如果对象具有循环引用,即对象直接或间接地引用自身,则 JSON.stringify() 会抛出错误,因为它无法处理循环引用的情况。

const obj = {};
obj.self = obj;
// 这里会抛出错误
const clonedObj = JSON.parse(JSON.stringify(obj));

(4)忽略 Symbol 属性:由于 JSON.stringify() 不能序列化 Symbol 类型的键名,这些属性会被完全忽略,在克隆的对象中不会保留。

(5)忽略 undefined、Function 和 RegExp:undefined 以及函数和正则表达式等值在序列化过程中会被忽略或者改变。

(6)忽略原型链:克隆的对象不会保持同一个原型链,克隆后的对象是一个纯净的字面量对象,没有继承任何原型方法。

2. 使用递归

这种方法允许你更细致地控制各种数据类型如何被复制,并且可以处理循环引用的问题。

function deepClone(obj, hash = new WeakMap()) {
    if (obj === null) return null;
    if (typeof obj !== "object") return obj;
    if (obj instanceof Date) return new Date(obj);
    if (obj instanceof RegExp) return new RegExp(obj);
    
    if (hash.has(obj)) return hash.get(obj);
    
    const cloneObj = new obj.constructor();
    hash.set(obj, cloneObj);
    
    for (const key in obj) {
        if (obj.hasOwnProperty(key)) {
            cloneObj[key] = deepClone(obj[key], hash);
        }
    }
    return cloneObj;
}




const complexObj = {
    numbers: [1, 2, 3],
    person: {
        name: "John",
        address: {
            city: "New York",
            coordinates: {
                lat: 40.7128,
                lng: -74.0060
            }
        }
    }
};

const clonedComplexObj = deepClone(complexObj);

console.log(clonedComplexObj);
/* 输出:
{
  numbers: [ 1, 2, 3 ],
  person: {
    name: 'John',
    address: { city: 'New York', coordinates: { lat: 40.7128, lng: -74.006 } }
  }
}
*/
console.log(clonedComplexObj.person === complexObj.person); // 输出: false
console.log(clonedComplexObj.person.address === complexObj.person.address); // 输出: false

3. 使用 structuredClone()

从JavaScript的某些版本开始,提供了structuredClone()方法,这是一个执行深拷贝的原生方法,也能很好地处理各种内置类型、循环引用等。

const obj = { a: 1, b: { c: 2 } };
const clonedObj = structuredClone(obj);
console.log(clonedObj); // { a: 1, b: { c: 2 } }

4. 利用第三方库Lodash

  • 使用 lodash 的 _.cloneDeep() 方法:
const _ = require('lodash');
const obj = { a: 1, b: { c: 2 } };
const clonedObj = _.cloneDeep(obj);
console.log(clonedObj); // { a: 1, b: { c: 2 } }