拷贝分为深拷贝(deep copy)和浅拷贝(shallow copy),这两种都为编程中常见的概念,特别是在处理数据结构和对象时很重要,本文将阐述这两者之间的区别,让你对这种初步认识起来像是复制粘贴似的玩意有更深的认知。

深拷贝与浅拷贝

首先需要知道,拷贝(copy) 这一概念通常只针对引用类型,是指将一个对象的值复制(或引用)到另一个对象中的操作,按修改原对象的值是否会影响到新对象分为浅拷贝深拷贝

浅拷贝:

基于原对象,拷贝得到一个新的对象,原对象中内容的修改会影响新对象

深拷贝:

基于原对象,拷贝得到一个新的对象,原对象中内容的修改不会影响新对象

那么,造成深拷贝与浅拷贝区别的原因是什么呢?答案就在以下两张图中。

深拷贝和浅拷贝的那些事_深拷贝

深拷贝和浅拷贝的那些事_浅拷贝_02

通俗来讲,浅拷贝只复制对象本身,而不复制对象内部的引用对象,这意味着新对象与原对象共享同一组内部对象。而深拷贝则会递归复制所有引用对象,确保所有对象都是独立的

那么,实现深浅拷贝的方法又有哪些呢?

浅拷贝的实现方法:

无论是实现方法还是应用场景,浅拷贝都占了大多数。这是因为浅拷贝的实现相对简单,对于许多应用场景已经足够。浅拷贝的主要优势在于效率和内存使用上,因为它只复制对象本身,而不会递归复制整个对象图。

1.Object.create(obj)

首先是大家喜闻乐见的Object.create(obj)。可以看到,修改obj中的值obj2的值也随之改变,因此这种方法为浅拷贝。

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

2.Object.assign({}, obj)

接下来是Object.assign({}, obj)。这种方法实现拷贝的原理,是将一个或多个原对象(obj)的所有可枚举属性复制到目标对象({})中,并返回目标对象。需要注意的是:这种方法可以改变原对象的引用类型而改变新对象,而原始类型的改变不会影响新对象,正因为如此,它还是被归为浅拷贝。

// 原始对象
const obj = {
    name: '小明',
    like: {
         n: 'running'
    }
  };
  // 浅拷贝得到新对象
  const shallowCopy = Object.assign({}, obj);
  
  // 修改原对象的值
  obj.name = "小红";        // 原对象的原始类型修改不会改变新对象中的值
  obj.like.n = 'swimming'  //  原对象的引用类型修改才会改变新对象中的值
 
  // 输出结果
  console.log('Shallow Copy:', shallowCopy); // 输出Shallow Copy: { name: '小明', like: { n: 'swimming' } }

3.[].concat(arr)  :

接下来要介绍的是数组的拷贝方法[].concat(arr)。它的原理是将arr中的元素合并到[]中,并返回一个新数组。同样的,改变原对象的引用类型会改变新对象,而原始类型的改变不会影响新对象

// 原始数组
const arr = [1, 2, 3,{a:"小明"}];

// 浅拷贝得到新数组
const shallowCopy = [].concat(arr);

// 修改原数组的值
arr[1] = 20;         // 不会改变新对象
arr[3].a = '小红';   // 会改变新对象

// 输出结果
console.log('Shallow Copy Array:', shallowCopy); // 输出Shallow Copy Array: [ 1, 2, 3, { a: '小红' } ]

 4.[...arr]:

数组解构赋值 [...arr],这种方法就是将数组中的元素全部剖析出来。提取元素,除去表壳,通过数组解构赋值创建一个新数组,复制原数组的元素到新数组中。它的执行结果同上种方法极为相似。

// 原始数组
const arr = [1, 2, 3,{a:"小明"}];

// 浅拷贝得到数组
const shallowCopy = [...arr];

// 修改原数组的值
arr[1] = 20;         // 不会改变新对象
arr[3].a = '小红';   // 会改变新对象

// 输出结果
console.log('Shallow Copy Array:', shallowCopy); // 输出Shallow Copy Array: [ 1, 2, 3, { a: '小红' } ]

5.arr.slice() :

slice()方法原本是从现有数组中提取出指定范围的元素(左开右闭),然后返回这些元素组成的新数组,而不会修改原始数组。

使用数组的 slice() 方法创建一个新数组,包含原数组的所有元素,也是一种常见的拷贝方法。

// 原始数组
const arr = [1, 2, 3,{a:"小明"}];

// 浅拷贝得到数组
const shallowCopy = arr.slice(0);

// 修改原数组的值
arr[1] = 20;         // 不会改变新对象
arr[3].a = '小红';   // 会改变新对象

// 输出结果
console.log('Shallow Copy Array:', shallowCopy); // 输出Shallow Copy Array: [ 1, 2, 3, { a: '小红' } ]

6.arr.reverse().reverse():

最后一种方法,arr.reverse().reverse()它实际上是对原数组进行了两次反转操作,结果是将数组的顺序恢复到最初的顺序。

同样的,它也能由此实现浅拷贝。

const arr = [1, 2, 3, 4];
    arr.reverse(); // 第一次反转,原数组变为 [4, 3, 2, 1]
    arr.reverse(); // 第二次反转,原数组再次变为 [1, 2, 3, 4]

手写一个浅拷贝

好了,这下浅拷贝原理思路一下子清晰了,下面就让我们手写一个浅拷贝。

let obj = {
    name: '小明',
    like: {
        a: 'food'
    }
}
function shallow(obj){
    let newObj = {}
    for(let key in obj){
        if(obj.hasOwnProperty(key)) {
            newObj[key] = obj[key]
        }
    }

    return newObj
}

let obj2 = shallow(obj)

console.log(obj2);    //输出{ name: '小明', like: { a: 'food' } }
obj.like.a = 'beer'   //修改原对象的值
console.log(obj2);    //输出{ name: '小明', like: { a: 'beer' } }

实现原理:

  1. 借助 for in 遍历原对象,将原对象属性增加在新对象中
  2. 因为 for in 会遍历到对象隐式具有的属性,通常要使用obj.hasOwnProperty(key)来判断要拷贝的属性是不是对象显示具有的

其中的shallow函数遍历了原始对象obj的所有可枚举属性,并将它们复制到一个新的空对象newObj中。需要注意的是,这里只复制了对象的第一层属性,如果属性的值是对象,那么复制的是对象的引用,这也是这个函数能实现浅拷贝的原因。

深拷贝的实现方法:

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

JSON.parse(JSON.stringify(obj)) 会产生一个与原对象具有相同数据的新对象实例,且两者之间没有引用关系,但这种方法还是存在以下限制:

  1. 不能识别 BigInt 类型。
  2. 不能拷贝 undefinedSymbolfunction
  3. 不能处理循环引用。
let obj = {
    name: '小明',
    age: 18,
    like: {
      n:'running'
    },
    a: true,
    b: undefined,
    c:null,
    d: Symbol(1),
    f: function() {}
}

let obj2 = JSON.parse(JSON.stringify(obj));
obj.like.n = 'swimming';

console.log(obj);
console.log(obj2);

输出的结果为:

深拷贝和浅拷贝的那些事_深拷贝_03

可以看到这种方法没能识别到undefinedSymbolfunction三种类型。而且如果我们添加上e:123n这个大整形的话则会报错。

2.structuredClon():

第二种也是JS官方推出的一种深拷贝方法structuredClon()可谓是千呼万唤始出来啊。

// 原始对象
let obj={
    name: '小明',
    like: {
         n: 'running'
    }
}
// 深拷贝得到对象
const shallowCopy = structuredClone(obj);

// 修改原对象的值
obj.name = "小红";        
obj.like.n = 'swimming'  

// 输出结果
console.log('Shallow Copy:', shallowCopy); //输出Shallow Copy: { name: '小明', like: { n: 'running' } }

无论是修改原对象中的原始类型还是引用类型,新对象愣是纹丝不动,深拷贝的成果堪称完美。

手写一个深拷贝

又到了我们的惯例手写环节,这不,就让我们手写实现一个深拷贝。

let obj = {
    name: '小明',
    like: {
        a: 'food'
    }
}

function deep(obj) {
    let newObj = {}
    for (let key in obj) {
        if (obj.hasOwnProperty(key)) {//规避隐式原型属性
            //判断是不是对象
            if (obj[key] instanceof Object) {
                //typeof obj[key]==='object' && obj[key]!==bull
                newObj[key] = deep(obj[key])
            }
            else {
                newObj[key] = obj[key]
            }
        }
    }
    return newObj
}



const obj2 = deep(obj)

console.log(obj2);    //输出 { name: '小明', like: { a: 'food' } }
obj.name = '小红'     //修改原对象的值
obj.like.a = 'beer'
console.log(obj2);    //输出 { name: '小明', like: { a: 'food' } }

实现原理

  1. 借助for in 遍历原对象,将原对象属性增加在新对象中
  2. 因为 for in 会遍历到对象隐式具有的属性,通常要使用obj.hasOwnProperty(key)来判断要拷贝的属性是不是对象显示具有的
  3. 如果遍历到的属性值是原始值类型,直接往新对象中赋值,如果是引用类型,递归创建新的子对象

最后:

总结一下就是:

浅拷贝方法:

  1. Object.create(x)
  2. Object.assign({}, a)
  3. [].concat(arr)
  4. 数组解构 let newArr = [...arr]
  5. arr.slice()
  6. arr.toReversed().reverse()

深拷贝方法:

  1. JSON.parse(JSON.stringify(obj))
  • 不能识别 BigInt 类型
  • 不能拷贝 undefined Symbol function
  • 不能处理循环引用
  1. structuredClone();

希望大家能通过本文对深浅拷贝能有进一步认知。