1

JavaScript 浅拷贝和深拷贝 - Advoster

 1 year ago
source link: https://www.cnblogs.com/yuzhihui/p/17055138.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

JavaScript 浅拷贝和深拷贝

JavaScript 中的拷贝分为两种:浅拷贝和深拷贝。

一、浅拷贝

浅拷贝是指在拷贝过程中,只拷贝一个对象中的指针,而不拷贝实际的数据。所以,浅拷贝中修改新对象中的数据时,原对象中的数据也会被改变。

JavaScript 中浅拷贝可以通过如下几种方式实现:

  • 使用结构赋值的方式,例如 let newObject = {...oldObject}
  • 使用 Object.assign() 方法,例如 let newObject = Object.assign({}, oldObject)

二、深拷贝

深拷贝是指在拷贝过程中,拷贝一个对象中的所有数据,并创建一个新对象,对新对象进行操作并不会影响到原对象。

1、常规场景

JavaScript 中深拷贝可以通过如下几种方式实现:

  • 使用 JSON.parse(JSON.stringify(object)) 方法

需要注意的是:该方法会忽略 undefined 以及正则表达式类型的属性。

const A = { a: 7788, b: undefined, c: new RegExp(/-/ig) },
    B = JSON.parse(JSON.stringify(A));

console.log('A', A);
console.log('B', B);

642487-20230116103935989-1431002823.png

  • 使用递归的方式,手动拷贝对象的每一层
function deepCopy(obj) {
    if (typeof obj !== 'object' || obj === null) {
        return obj;
    }
    let copy;
    if (Array.isArray(obj)) {
        copy = [];
        for (let i = 0; i < obj.length; i++) {
            copy[i] = deepCopy(obj[i]);
        }
    } else {
        copy = {};
        for (let key in obj) {
            copy[key] = deepCopy(obj[key]);
        }
    }
    return copy;
}

const objA = { a: 123 },
    objB = { b: 456 };

// 浅拷贝
const objC = {...objA};
console.log('objA.a', objA.a);  // objA.a 123
console.log('objC.a', objA.a);  // objC.a 123
objC.a = 788;
console.log('objA.a', objA.a);  // objA.a 788
console.log('objC.a', objC.a);  // objC.a 788

// 深拷贝
const objD = deepCopy(objB);
console.log('objB.b', objB.b);  // objB.b 456
console.log('objD.b', objD.b);  // objD.b 456
objD.b = 899;
console.log('objB.b', objB.b);  // objB.b 456
console.log('objD.b', objD.b);  // objD.b 899

这个函数接受一个参数 obj,如果它不是对象或者是 null,那么直接返回该参数。如果它是数组,则创建一个新数组并递归复制每一项。否则,创建一个新对象并递归复制每一个属性。

  • 使用 lodash 类库的_.cloneDeep函数、 underscore 中的 _.clone() 函数等第三方库

2、特定场景一:内置对象类型的深拷贝

JavaScript 中复制内置对象类型(例如 Date,RegExp 等)的深拷贝可以使用特定的构造函数来重新创建该对象。

例如,对于 Date 对象,可以使用 new Date(originalDate.getTime()) 来创建一个新的日期对象,其中 originalDate.getTime() 返回原始日期对象的时间戳。

对于 RegExp 对象,可以使用 new RegExp(originalRegExp) 或 new RegExp(originalRegExp.source, originalRegExp.flags) 来创建一个新的正则表达式对象。

下面是一个使用构造函数来复制内置对象类型的深拷贝示例:


function deepCopy(obj) {
    let copiedObjects = new WeakMap();
    if (typeof obj !== 'object' || obj === null) {
        return obj;
    }
    if (copiedObjects.has(obj)) {
        return copiedObjects.get(obj);
    }
    let copy;
    if (obj instanceof Date) {
        copy = new Date(obj.getTime());
    } else if (obj instanceof RegExp) {
        copy = new RegExp(obj);
    } else if (Array.isArray(obj)) {
        copy = [];
    } else {
        copy = {};
    }
    copiedObjects.set(obj, copy);
    for (let key in obj) {
        if (obj.hasOwnProperty(key)) {
            if (typeof obj[key] === 'object' && obj[key] !== null) {
                copy[key] = deepCopy(obj[key], copiedObjects);
            } else {
                copy[key] = obj[key];
            }
        }
    }
    return copy;
}

这个示例的深拷贝函数首先检查当前对象是否为内置对象类型,如果是,则使用相应的构造函数重新创建该对象,否则创建一个普通对象或数组。然后进行递归复制每一个属性。

需要注意的是,使用构造函数复制内置对象类型只适用于部分内置对象类型,对于其他的内置对象类型,可能需要使用其他的方法来进行复制,或者使用第三方库来进行复制。

总之,深拷贝复制内置对象类型需要考虑使用构造函数来重新创建对象,如果需要对这些对象进行深拷贝操作,可以使用上述方法或其他库来实现。

3、特定场景二:自定义对象类型的深拷贝

JavaScript 中自定义对象的深拷贝可以使用同样的递归方式实现,可以使用 WeakMap 方法。

function deepCopy(obj) {
    let copiedObjects = new WeakMap();
    if (typeof obj !== 'object' || obj === null) {
        return obj;
    }
    if (copiedObjects.has(obj)) {
        return copiedObjects.get(obj);
    }
    let copy;
    if (obj instanceof MyCustomObject) {
        copy = new MyCustomObject();
    } else if (Array.isArray(obj)) {
        copy = [];
    } else {
        copy = {};
    }
    copiedObjects.set(obj, copy);
    for (let key in obj) {
        if (obj.hasOwnProperty(key)) {
            if (typeof obj[key] === 'object' && obj[key] !== null) {
                copy[key] = deepCopy(obj[key], copiedObjects);
            } else {
                copy[key] = obj[key];
            }
        }
    }
    return copy;
}

这个函数首先检查当前对象是否为自定义对象,如果是,则创建一个新的自定义对象,否则创建一个普通对象或数组。然后进行递归复制每一个属性。

注意,如果自定义对象中包含循环引用,需要使用 WeakMap 来避免出现死循环。

4、特定场景三:对象中存在函数或循环引用

对于函数,通常会忽略它们,因为函数不能被复制,而是需要重新定义。

对于循环引用,可以使用 WeakMap 来存储已经复制过的对象。每次遇到循环引用时,可以检查 WeakMap 中是否已经有该对象的副本,如果有,则直接使用副本,而不是重新创建。

function deepCopy(obj) {
    let copiedObjects = new WeakMap();
    if (typeof obj !== 'object' || obj === null) {
        return obj;
    }
    if (copiedObjects.has(obj)) {
        return copiedObjects.get(obj);
    }
    let copy;
    if (Array.isArray(obj)) {
        copy = [];
    } else {
        copy = {};
    }
    copiedObjects.set(obj, copy);
    for (let key in obj) {
        if (obj.hasOwnProperty(key)) {
            if (typeof obj[key] === 'object' && obj[key] !== null) {
                copy[key] = deepCopy(obj[key], copiedObjects);
            } else {
                copy[key] = obj[key];
            }
        }
    }
    return copy;
}

这是使用 WeakMap 的一种示例,这个示例的深拷贝函数递归地复制对象,但检查 WeakMap 中是否已经存在该对象的副本,如果存在则直接使用副本,而不是重新创建。

此外,使用 JSON.parse(JSON.stringify(obj)) 方法会自动忽略函数和循环引用,但是会忽略 undefined 以及正则表达式类型的属性。

还有,还可以使用第三方库,如 lodash 中的 _.cloneDeep() 函数、 underscore 中的 _.clone() 函数等来实现对象中存在函数或循环引用的深拷贝。

5、特定场景四:对象中有对其他对象的引用或者包含 Symbol 属性的对象

对于对象中有对其他对象的引用,可以使用 WeakMap 来存储已经复制过的对象。每次遇到对其他对象的引用时,可以检查 WeakMap 中是否已经有该对象的副本,如果有,则直接使用副本,而不是重新创建。

对于对象中包含 Symbol 属性的对象,可以使用 Object.getOwnPropertySymbols() 方法来获取该对象所有的 Symbol 属性,然后使用 Object.getOwnPropertyDescriptor() 方法来获取这些 Symbol 属性的值,最后将这些值赋给新对象。


function deepCopy(obj) {
    let copiedObjects = new WeakMap();
    if (typeof obj !== 'object' || obj === null) {
        return obj;
    }
    if (copiedObjects.has(obj)) {
        return copiedObjects.get(obj);
    }
    let copy;
    if (Array.isArray(obj)) {
        copy = [];
    } else {
        copy = {};
    }
    copiedObjects.set(obj, copy);
    for (let key in obj) {
        if (obj.hasOwnProperty(key)) {
            if (typeof obj[key] === 'object' && obj[key] !== null) {
                copy[key] = deepCopy(obj[key], copiedObjects);
            } else {
                copy[key] = obj[key];
            }
        }
    }
    let symbols = Object.getOwnPropertySymbols(obj);
    symbols.forEach(symbol => {
        let descriptor = Object.getOwnPropertyDescriptor(obj, symbol);
        Object.defineProperty(copy, symbol, descriptor);
    });
    return copy;
}

这是使用 WeakMap 和 Symbol 属性的一种示例,这个示例的深拷贝函数首先检查 WeakMap 中是否已经存在该对象的副本,如果存在则直接使用副本,而不是重新创建。然后使用 Object.getOwnPropertySymbols() 方法获取该对象所有的 Symbol 属性,最后使用 Object.getOwnPropertyDescriptor() 方法获取这些 Symbol 属性的值,并将这些值赋给新对象。

这种方法可以保证深拷贝对象中包含的所有属性,包括对其他对象的引用和 Symbol 属性,但还是不能复制内置对象类型,这些对象类型是不可枚举的。

三、循环引用

JavaScript 中的循环引用指的是两个或多个对象之间相互引用的情况。这种情况通常发生在将一个对象赋给另一个对象的属性时,同时还将另一个对象赋给第一个对象的属性。

以下是一个示例:

let obj1 = {};
let obj2 = {};

obj1.prop = obj2;
obj2.prop = obj1;

这样就会产生一个循环引用,因为 obj1 和 obj2 相互引用。

循环引用可能导致 JavaScript 引擎无法正确处理内存,并导致内存泄漏。因此,在编写 JavaScript 代码时需要特别注意避免循环引用。如果您需要在两个对象之间建立关系,可以使用弱引用来避免循环引用。

在深拷贝中遇到循环引用就会导致死循环,因此需要使用特殊的算法来解决这个问题。可以使用递归算法和深度优先遍历来实现深拷贝,在遍历过程中跟踪已经遍历过的对象,如果遇到循环引用就直接返回已经遍历过的对象的引用。

总的来说,在使用浅拷贝和深拷贝时,需要根据需求和对象的结构来进行选择。通常来说,如果需要对对象进行修改并且不希望对原对象造成影响,那么应该使用深拷贝。如果只是需要读取对象中的数据而不需要修改,那么可以使用浅拷贝。在实现深拷贝时,需要特别注意循环引用和特殊属性问题。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK