JavaScript

实现浅拷贝和深拷贝的常见方法

浅拷贝:创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址 ,所以如果其中一个对象改变了这个地址,就会影响到另一个对象。

深拷贝:将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象,且修改新对象不会影响原对象

基本数据类型赋值时,其实就是赋数据。定义一个新的基本数据类型变量或者把一个基本类型变量赋给另外一个变量,新变量都会被放置到新的内存位置上,所以不存在深拷贝和浅拷贝的区别。然而,对于引用数据类型,它存储的是存储对象的内存位置的地址,所以复制引用数据类型就会被分为两种类型,即浅拷贝和深拷贝。

let age = 20;
let hisAge = age;
hisAge = 21;
console.log(`age:${age}--hisAge:${hisAge}`);  
//age:20--hisAge:21

浅拷贝只是指向保存新变量值的原始集合结构(对象或数组)的引用地址,即只复制集合结构,而不复制元素。因此,被引用复制的对象是被共享的。

let p = {
     name:"p",
     age:20
}
let pp = p;
pp.name = "pp";
console.log(p);//{name: "pp", age: 20}

数组也是类似的。

 let arr1 = [1,2,3];
let arr2 = arr1;
arr2.push(4);
console.log(`arr1:${arr1}--arr2:${arr2}`);
//arr1:1,2,3,4--arr2:1,2,3,4

这就是对基本数据类型和引用数据类型最基础的操作的不同点。

常见浅拷贝的实现方法

1.拓展运算符(……)

对象中的扩展运算符(…)用于取出参数对象中的所有可遍历属性,拷贝到当前对象之中,是属于浅拷贝的。在React中的编写state或props相关代码时,常常用到拓展运算符把对象的内容复制到某处。

        let bar = { a: 1, b: 2 };
       let baz = { ...bar }; // { a: 1, b: 2 }
       baz.a = 12;
       console.log(`bar:${bar.a}--baz:${baz.a}`);
       //bar:1--baz:12

2.Object.assign()

Object.assign方法用于对象的合并,将源对象(source)的所有可枚举属性,复制到作为第一个参数的目标对象(target)。注意的是,修改复制好后的目标对象的基本数据类型,是不会影响到源对象的对应的基本数据类型的,但是修改复制过来的引用数据类型,就会改变源对象的对应的引用数据类型,也就是她们的引用数据类型的value保存的是同一个地址变量。所以这是一个浅拷贝。

        let bar = { a: 1, b: 2,attr:{type:'little'} };
       let baz = Object.assign({c:3}, bar); // { a: 1, b: 2 }
       baz.a = 12;
       baz.attr.type = 'tiny';
       console.log(`bar.a:${bar.a}--baz.a:${baz.a}--bar.c:${bar.c}--baz.c:${baz.c}--bar.attr:${bar.attr.type}--baz.attr:${baz.attr.type}`);
     // bar.a:1--baz.a:12--bar.c:undefined--baz.c:3--bar.attr:tiny--baz.attr:tiny

3.slice()和Array.from()

使用数组内置的.slice()方法和Array.from实现的都是浅拷贝的复制。其实应该说,这些拷贝方法,遇到嵌套的属性,只会对第一层进行深拷贝,但是嵌套于里面的引用类型数据,都是复制地址罢了。

常见深拷贝方法

深拷贝只是将源对象的所有属性复制到目标对象中。换句话说,基元类型和引用类型属性都将被分配到新的内存位置。两者的引用类型的value保存的地址变量不是同一个。这样,即使源对象不存在,目标对象仍然存在于内存中。

1.JSON.parse/stringify

利用JSON.stringify 将js对象序列化(JSON字符串),再使用JSON.parse来反序列化(还原)js对象来做到深拷贝。

let originalObject = {name: "apple", price: {chennai: 120}};
let clonedObject=JSON.parse(JSON.stringify(originalObject));
clonedObject.name ="orange";
clonedObject.price.chennai = 100;
//clonedObject = {name: "orange", price: {chennai: 100}}
//originalObject = {name: "apple", price: {chennai: 120}}

但是JSON.stringify()只能序列化对象的可枚举的自有属性,例如 如果obj中的对象是有构造函数生成的, 则使用JSON.parse(JSON.stringify(obj))深拷贝后,会丢弃对象的constructor。还有就是一些特殊的属性被序列化后会转化成不理想的形式,如obj里有NaN,序列化后显示的是null。

2.Lodash

Lodash是一个提供多个实用函数的JavaScript库,Lodash库中最常用的函数之一是cloneDeep()方法。该方法有助于对对象进行深度克隆,不管如何嵌套都可以,还能克隆JSON.stringify()方法中限制的非序列化属性。

手写实现深拷贝

因为属于浅拷贝的方法都是没有真正复制到嵌套于内层的引用数据类型数据,所以要实现深拷贝,需要递归进行。

    let deepClone = (initalObj) => {
    const obj = {};
    if(typeof initalObj !== 'object'){
      return initalObj
    }
    for (const key in initalObj) {
      if (typeof initalObj[key] === 'object') {
        //对数组特殊处理
        if (Array.isArray(initalObj[key])) {
          //用map方法返回新数组,将数组中的元素递归
          obj[key] = initalObj[key].map(item => this.deepClone(item))
        } else {
          //递归返回新的对象
          obj[key] = this.deepClone(initalObj[key]);
        }
      } else if (typeof initalObj[key] === 'function') {
        //返回新函数
        obj[key] = initalObj[key].bind(obj);
      } else {
        //基本类型直接返回
        obj[key] = initalObj[key];
      }
    }
    return obj;
  }

const obj = {
  a: 1,
  b: {},
  c: { d: {}, g: () => {} },
  e: () =>{},
  f: function () {}
}
const newObj = deepClone(obj);
newObj.a === obj.a  //true
newObj.b === obj.b  //false
newObj.c === obj .false  //false
newObj.c.d === obj.c.d  //false
newObj.c.g === obj.c.g  //false
newObj.e === obj.e  //false
newObj.f === obj.f  //false

参考:手写深拷贝代码 JavaScript之对象序列化详解

发表评论

邮箱地址不会被公开。 必填项已用*标注