JavaScript中的浅拷贝与深拷贝

344 阅读8分钟

拷贝

拷贝在JavaScript中是一种常见的操作,它有助于提高代码的可读性、可维护性和安全性。在函数中传递对象或数组时,如果希望函数内部的修改不影响到外部变量,可以通过传递对象的拷贝来实现,还具有等等许多用处,而拷贝分为浅拷贝和深拷贝

let a = 10
let b = a
a = 20
console.log(b)//10


let obj = {
    age : 18
}
let obj2 = obj
obj.age = 20
console.log(obj2.age)//20

这两段代码分别为浅拷贝和深拷贝最基本的实现

浅拷贝

浅拷贝是指将原对象或数组的值复制到一个新的对象或数组中,但是新的对象或数组的属性或元素依然是原对象或数组的引用,这意味着当我们修改其中一个对象或数组时,另一个对象或数组也会受到影响(拷贝后的对象受原对象的影响)因此,浅拷贝通常情况下只针对引用类型。下面是常见的浅拷贝方法:

  1. Object.create(obj)
  2. Object.assign(obj1,obj2)
  3. [].concat(arr)
  4. 数组解构...arr
  5. arr.slice(0)
  6. arr.toReverse().reverse

1. Object.create(obj)

Object.create() 方法可以用于创建一个新对象,并将原对象作为新对象的原型

let obj = {
    a: 1
}
let obj2 = Object.create(obj)
//obj.a = 2
console.log(obj2.a);

2. Object.assign(obj1,obj2)

Object.assign() 是 JavaScript 中的一个内置方法,用于将一个或多个源对象的可枚举属性复制到目标对象中。此方法会返回目标对象。如果目标对象中的属性具有相同的键,则它们会被源对象中的属性覆盖。这个方法常用于合并对象或者克隆对象。

括号中的第一个对象为目标对象,身后可以有一个或多个对象,其可枚举的属性将被复制到目标对象中。

let obj = {
    a: 1,
    b: [1,2,3]
}
let obj2 = Object.assign({},obj)
obj.b.push(4)
console.log(obj2);//{a:1,b:[1,2,3,4]}

3. [].concat(arr)

[].concat(arr) 是 JavaScript 中用来合并数组的一个方法。这里使用了数组的 concat() 方法,并以一种简写形式出现,它的功能是将一个或多个数组(或非数组值)连接到一起,返回一个新的数组,原数组不会被改变。

那如果咱们要实现拷贝的效果时,就只需要将一个空数组和要拷贝的数组合并起来,便得到了一个和拷贝的数组一样的新数组

let arr = [1,2,3]
let arr2 = [].concat(arr)
arr.push(4)
console.log(arr2);//[1,2,3]
let arr = [1,2,3,{a:1}]
let arr2 = [].concat(arr)
arr[3].a = 2
console.log(arr2);//[1,2,3,{a:2}]

4. 数组解构...arr

...arr这个表达式利用展开操作符从 arr 数组中取出所有元素,并将这些元素直接插入到一个新的数组字面量中

let arr1 = [1, 2, 3];
let arr2 = [...arr1, 4, 5]; 
console.log(arr2); // [1, 2, 3, 4, 5]

其浅拷贝的实现:解构所需要的数组

let arr = [1,2,3,{a:1}]
let arr2 = [...arr]
arr[3].a = 2
console.log(arr2);//[1,2,3,{a:2}]

5. arr.slice(0)

slice() 方法是 JavaScript 数组对象的一个方法,用于从数组中提取指定区间的元素创建一个新的数组,并不会对原数组产生影响。它接受两个参数:开始索引和结束索引,当参数为0时,则会复制整个原数组 arr。

arr.slice(0) 和 [...arr] 的作用大差不差都能实现数组的浅拷贝

let arr = [1,2,3,{a:1}]
let arr2 = arr.slice(0)
arr[3].a = 2
console.log(arr2);//[1,2,3,{a:2}]

6. arr.reverse().reverse()

reverse() 是 JavaScript 数组的一个原生方法。 作用:用于颠倒数组中元素的顺序。换句话说,它会将数组的第一个元素移动到最后,最后一个元素移动到最前,以此类推。

那么如果我们将数组反转之后再反转不就得到了需要拷贝的数组了吗

let arr = [1,2,3,{a:1}]
let arr2 = arr.reverse().reverse()
arr[3].a = 2
console.log(arr2);//[1,2,3,{a:2}]

手搓一个函数体具有浅拷贝的一样的作用

function shallowCopy(obj){
    let newobj = {};
    for(let key in obj){ 
        // 判断是否是自身属性(即显式的属性,而不是继承的属性)
        if(obj.hasOwnProperty(key)){ 
            newobj[key] = obj[key];
        }
    }
    return newobj;
}

let obj = {
    a: 1,
    b: {n:2}
}
console.log(shallowCopy(obj));
  • shallowCopy原理
  1. for..in 遍历对象,将属性复制到新对象中
  2. 借助hasOwnProperty判断是否是自身属性(即显式的属性,而不是继承的属性)
  3. 仅复制一层属性,如果属性值是复杂数据类型(如对象、数组),则复制它们的引用而非实际值。

深拷贝

深拷贝是指将原对象或数组的值复制到一个新的对象或数组中,并且新的对象或数组的属性或元素完全独立于原对象或数组,即它们不共享引用地址。因此,当我们修改其中一个对象或数组时,另一个对象或数组不会受到任何影响(拷贝后的对象不受原对象的影响)。

1.JSON.parse(JSON.stringify(obj)) 2.structuredClone(obj)

1.JSON.parse(JSON.stringify(obj))

JSON.parse(JSON.stringify(obj)) 会产生一个与原对象具有相同数据的新对象实例,但两者之间没有引用关系

其中使用了两种方法JSON.parse()JSON.stringify()

JSON.stringify() 的作用

  • 方法签名JSON.stringify(value[, replacer[, space]])

  • 作用:将 JavaScript 对象或值转换为 JSON 字符串。这对于将对象数据发送到服务器、存储到本地存储或在页面之间传递非常有用。

  • 参数说明

    • value:要转换的 JavaScript 值(通常是一个对象或数组)。
    • replacer:可选,用于转换结果的函数或数组。可以用来过滤或替换结果中的某些值。
    • space:可选,添加缩进、空格或制表符来美化输出(用于提高可读性)。

JSON.parse() 的作用

  • 方法签名JSON.parse(text[, reviver])

  • 作用:将 JSON 字符串转换回 JavaScript 值(通常是对象或数组)。

  • 参数说明

    • text:要解析的 JSON 字符串。
    • reviver:可选,一个转换函数,可用于在还原之前对属性值进行转换处理。

JSON.parse(JSON.stringify(obj))简单来说就是对象先转为字符串再转为对象

let obj = {
    a: 1,
    b: {n: 2}
}

console.log(JSON.stringify(obj)); // 字符串{"a":1,"b":{"n":2}}

let obj2 = JSON.parse(JSON.stringify(obj));//转为字符串再转为对象
console.log(obj2);//{a:1,b:{n:2}}

但是要注意

  1. 无法拷贝bigInt类型
  2. 无法拷贝 undefined、symbol、function函数
  3. 无法处理循环引用
let obj3 = {
    a: 1,
    b: {n: 2},
    c:'c',
    d: true,
    e: undefined,
    f: null,
    g: function(){},
    h: Symbol('h'),
    i: new Date(),
}
//obj3.a = obj3.b
//obj3.b.m = obj3.a
let obj4 = JSON.parse(JSON.stringify(obj3));
console.log(obj4);

得到结果:

0c2568857dd36b29ccea3d639a4fea2.png

而如果进行循环引用时会无法处理报错

obj3.a = obj3.b
obj3.b.m = obj3.a

2.structuredClone(obj)

其与 JSON.stringify() 配合 JSON.parse() 的浅拷贝方法相比,structuredClone() 能够处理更多的数据类型,包括循环引用的对象、正则表达式、日期对象、Error对象等,并且保留了它们的原型链和方法。

需要注意

  1. 无法拷贝 symbol、function函数会报错
  2. 兼容性structuredClone() 是 ES2022 的特性,所以在一些较旧的浏览器或环境中可能不可用
let obj3 = {
    a: 1,
    b: {n: 2},
    c:'c',
    d: true,
    e: undefined,
    i: new Date(),
}
obj3.a = obj3.b
const obj4 = structuredClone(obj3)

console.log(obj4)

23c48bc8ffa39821cc6cfe260390a6b.png

手搓一个函数体具有深拷贝的一样的作用

let obj = {
    a: 1,
    b: {n: 2},
}


function deepCopy(obj) {
    let newObj = {}
    for (let key in obj) {
        if(obj.hasOwnProperty(key)){
            if (typeof obj[key] === 'object') {
                newObj[key] = deepCopy(obj[key])
            } else {
            newObj[key] = obj[key]
            }
        }
    }
    return newObj
}

console.log(deepCopy(obj))
  • deepCopy的原理
  1. for..in 遍历对象,将属性复制到新对象中
  2. 借助hasOwnProperty判断是否是自身属性(即显式的属性,而不是继承的属性)
  3. 原始值直接复制,引用值递归复制
  4. 递归地复制对象的所有层级,包括嵌套的对象和数组,确保源对象和拷贝对象之间完全独立,修改一个对象不会影响到另一个。

与shallowCopy原理大相径庭但得让拷贝后的对象不受原对象的影响。

总结

拷贝分为了深拷贝与浅拷贝

  • 浅拷贝:拷贝后的对象受原对象的影响

  • 深拷贝:拷贝后的对象不受原对象的影响

而在许多情况下会使用到拷贝,它具有许多好处:

  1. 避免引用共享:当一个对象或数组被多个变量引用时,对其中一个变量的修改会影响到其他所有引用该对象的变量。为了防止这种情况,可以使用拷贝来创建对象的独立副本。

  2. 函数参数传递:在某些情况下,你可能希望函数接收到一个对象的副本而不是原始对象的引用,以避免函数内部对对象的修改影响到外部。

  3. 实现不可变性:在某些设计模式中,例如不可变数据结构,拷贝是实现不可变性的关键。每次修改都返回一个新的对象,而不是修改原始对象。

  4. 克隆对象:当你需要一个对象的完全相同副本时,拷贝可以确保新对象与原始对象在结构和内容上完全一致。

  5. 序列化与反序列化:在需要将对象转换为字符串(例如JSON格式)并稍后恢复为对象时,拷贝可以确保转换过程中不会丢失数据。