浅谈JavaScript里面的深拷贝,浅拷贝

94 阅读6分钟

前言

在具体说明深浅拷贝之前,先具体说一下js里数据主要分为两种数据类型

1.基本数据类型

number, boolean, null, undefined, string

2.引用数据类型

数组 函数 对象

他们各自在内存中的存储方式不一样

引用数类型里面的元素会在内存中开辟空间,存储在堆中,通过在栈中的一个地址引用来访问堆中的元素

而基本数据类型存储在栈中可以直接通过变量来进行访问

什么是深拷贝与浅拷贝

浅拷贝:将原对象或原数组的引用直接赋给新对象,新数组,新对象/数组只是原对象的一个引用

创建了一个的数据,如果是基本数据类型,就是复制了里面的值

如果是引用数据类型那就是复制了对象的引用

下面上图来方便理解

浅拷贝.png

复制了一个新的引用来访问到堆里面的元素,里面的元素依旧是原来的元素,并不会发生改变

如果改变了其中的元素就会影响到另外一个引用,因为他们之间是共享的

为什么要使用深浅拷贝

我们希望在改变新的数组(对象)的时候,不改变原数组(对象)

浅拷贝方法

数组浅拷贝

方式1:Object.assign()

Object.assign(target, ...sources);
  • target:目标对象,即要将属性复制到的对象。
  • ...sources:一个或多个源对象,即要从中复制属性的对象。可以传入多个源对象,属性将按照参数顺序进行复制,如果有重复的属性,后面的源对象会覆盖前面的源对象的属性。

返回值: Object.assign() 方法返回目标对象,即更新后的目标对象

    // const person = ['张三', '李四', '王五']
    // const person1 = [...person]
    // console.log(person);
    // console.log(person1);

方式2: ... 展开运算符

在数组中使用展开运算符时,它可以将数组元素解开,使其成为独立的值,从而可以用于创建新的数组、函数参数传递等操作。

const obj1 = { a: 1, b: 2 };
const obj2 = { c: 3, ...obj1 };

console.log(obj2); // { a: 1, b: 2, c: 3 }

方式3:slice()

array.slice(start, end)
  • start:必需,表示开始提取元素的索引。如果为负数,则表示从末尾往前数的索引(-1 表示最后一个元素)。
  • end:可选,表示结束提取元素的索引。如果省略该参数,slice() 方法会提取从 start 到数组末尾的所有元素。如果为负数,则表示从末尾往前数的索引(-1 表示最后一个元素)。
const fruits = ['apple', 'banana', 'orange', 'grape', 'kiwi'];

const slicedFruits1 = fruits.slice(1, 3);
console.log(slicedFruits1); // ['banana', 'orange']

const slicedFruits2 = fruits.slice(2);
console.log(slicedFruits2); // ['orange', 'grape', 'kiwi']

const slicedFruits3 = fruits.slice(-3, -1);
console.log(slicedFruits3); // ['orange', 'grape']

需要注意的是,slice() 方法不会修改原始数组,而是返回一个新的数组。如果省略参数并直接调用 slice(),它将会复制整个原始数组,从而创建一个新的与原数组一样的数组

方式4:lodash的浅拷贝

这个就非常的方便了,调用lodash的浅拷贝方法,传入需要拷贝的数组或是对象就可以,非常的人性化

let obj1 = { a: 1, b: { c: { d: 1 } }, d: [1, 2, 3] };
let obj2 = _.clone(obj1); 
console.log(obj1.b.d === obj2.b.d); // true

方式5:concat()

array.concat(value1, value2, ..., valueN)

value1, value2, ..., valueN:可选参数,可以是数组或者任意其他类型的值。这些值将会依次添加到合并后的数组中。

    const person = ['张三', '李四', '王五']
    const person1 = [].concat(person)
    console.log(person);//['张三', '李四', '王五']
    console.log(person1);//['张三', '李四', '王五']

对象浅拷贝

            //对象赋值
            let obj1 = {
                name: '洛洛历险记',
                arr: [1, [2, 3], 4]
            }

            let obj2 = obj1

            obj2.name = '霹雳火'//更改属性值

            obj2.arr[1] = [5, 6, 7]

            console.log(obj1) // obj1 { name: '霹雳火', arr: [ 1, [ 5, 6, 7 ], 4 ] }

            console.log(obj2) // obj2 { name: '霹雳火', arr: [ 1, [ 5, 6, 7 ], 4 ] }
            console.log(obj2 === obj1);  //true

展开运算符

let obj1 = { name: 'Tom', address:{a:100,b:100}}
let obj2= {... obj1}
obj1.address.a = 200;
obj1.name = 'Jerry'
console.log('obj2',obj2) // obj2 { name: 'Tom', address: { a: 200, b: 100 } }

浅拷贝的问题如果遇到多层拷贝还是会影响原来的对象

  // 原始对象
  const obj = {
    name: '张三',
    age: 30,
    hobbies: ['读书', '敲代码'],
  };

  // 使用浅拷贝进行对象复制
  const obj1 = { ...obj };

  console.log(obj); // { 张三 30 [读书,敲代码] }
  console.log(obj1); // { 张三 30 [读书,敲代码] }

  // 修改浅拷贝对象的属性
  obj1.name = '燕子';
  obj1.hobbies.push('碎觉');

  console.log(obj); //{ 张三 30[读书, 敲代码, 碎觉] }
  
  console.log(obj1); //{ 燕子 30 [读书, 敲代码, 碎觉] }

当我们试图修改拷贝的新的对象的方法时,同时这个时候原始对象的方法也被影响到了,因为该方法所指向的的地址是同一个,复制的只是一个地址的引用,里面的内容依旧是共享的

深拷贝

创建一个新的对象和数组,将原对象的各项属性的“值”(数组的所有元素)拷贝过来,是“值”而不是“引用”

创建了一个全新的对象,在栈里的引用地址和堆中的元素,这两个有着自己各自的地址引用和元素,更改其中的一个元素并不会影响到另外一个数据里面的内容

深拷贝.png

JSON.parse(JSON.stringify()) 序列化和反序列化

先将需要拷贝的对象进行JSON字符串化,然后再pase解析出来,赋给另一个变量,实现深拷贝。

这个方法有一些弊端,那就是会忽略函数和undefined,但是一般我们从服务器获取到的数据里面不会出现函数和undefined,所以说可以尽情的享用

let array = [
    { number: 1 },
    { number: 2 },
    { number: 3 }
];
let copyArray = JSON.parse(JSON.stringify(array))
copyArray[0].number = 100;
console.log(array); //  [{number: 1}, { number: 2 }, { number: 3 }]
console.log(copyArray); // [{number: 100}, { number: 2 }, { number: 3 }]

lodash的深拷贝方法

let obj1 = { a: 1, b: { c: { d: 1 } }, d: [1, 2, 3] }; 
let obj2 = _.clone(obj1); 
console.log(obj1.b.d === obj2.b.d); //false

但是如果说你想挑战一下自己的软肋,通过手写递归实现深拷贝,也是可以的

let array = [
   { number: 1 },
   { number: 2 },
   { number: 3 }
];
function copy (obj) {
        let newobj = obj.constructor === Array ? [] : {};
        if(typeof obj !== 'object'){
            return;
        }
        for(let i in obj){
           newobj[i] = typeof obj[i] === 'object' ?
           copy(obj[i]) : obj[i];
        }
        return newobj
}
let copyArray = copy(array)
copyArray[0].number = 100;
console.log(array); //  [{number: 1}, { number: 2 }, { number: 3 }]
console.log(copyArray); // [{number: 100}, { number: 2 }, { number: 3 }]

总结

  1. 浅拷贝是一种复制对象或数组的引用的方法,而不是创建对象或数组的独立副本。
  2. 浅拷贝只复制对象或数组的一级属性,如果对象或数组中包含嵌套的对象或数组,则复制后的对象中的嵌套对象或数组仍然是原始对象的引用,修改其中一个对象的属性会影响到其他对象。
  3. 浅拷贝的方式包括使用展开运算符 { ...obj }Object.assign() 方法、数组的 slice() 方法等。
  4. 如果希望创建对象的独立副本,应该使用深拷贝。深拷贝是一种递归复制对象及其嵌套对象或数组的方法,确保每一层都是独立的,避免了共享引用的问题。可以使用第三方库(如 Lodash 的 cloneDeep 方法)或自己编写递归函数来实现深拷贝。