摘要:对象拷贝,简而言之就是将对象再复制一份,但是,复制的方法不同将会得到不同的结果。

本文分享自华为云社区《js对象深浅拷贝,来,试试看!》,作者: 北极光之夜。。

一.速识概念:

对象拷贝,简而言之就是将对象再复制一份,但是,复制的方法不同将会得到不同的结果。比如直接给新变量赋值为一个对象:

  // 1.建一个对象
  var obj = {
    name: "北极光之夜。",
    like: "aurora",
  };
  // 2. 直接将对象赋值给变量 clone
  var clone = obj;
  // 3.修改obj的like属性
  obj.like = "wind";
  // 4.输出 clone 对象
  console.log(clone);

从输出结果可以看到,我明明改变的是 obj 对象的属性,但是 clone 对象的属性也改变了。这是因为,当创建 obj 对象时,它在堆内存中开辟了一块空间存储对象的内容。而当 clone 直接赋值为 obj 时,clone 并不会再重新开辟一块堆内存,而是 obj 跟 clone 说我把我这内存空间存储的对象的地址给你,这个地址存在栈内存中,你通过栈内存的地址找到堆内存里对象的内容,咱们共用就完事了。所以说, obj 和 clone 指向的都是同一块内容,不管谁改了对象的内容,别人再访问都是改过之后的了。
image.png

所以这不是我们想要的,我不想共用,我想要属于自己的一片天地,我命由我不由你,所以这就需要浅拷贝和深拷贝了。

简单补充: 像一些基本数据类型的变量(Number Boolean String undefined null)被赋值时会直接在栈内存中开辟出了一个新的存储区域用来存储新的变量,不会如对象那样只是把引用给别人。

二.浅拷贝原理与常用方法:

简单来说浅拷贝就是只拷贝一层。什么意思呢 ?比如我有一个对象 obj :

 var obj = {
        name: "北极光之夜。",
        like: "aurora",
      };

我要把它拷贝给变量 b ,原理就是我再重新开辟一块内存,然后我直接看 obj 里有什么属性和值就直接复制一份,比如通过如下方式实现:

     // 1.建一个对象
      var obj = {
        name: "北极光之夜。",
        like: "aurora",
      };
      // 2. 封装一个函数,实现传入一个对象返回一个拷贝后的新对象
      function cloneObj(obj) {
        let clone = {};
        // 3.用 for  in 遍历obj的属性
        for (let i in obj) {
          clone[i] = obj[i];
        }
        return clone;
      }
      // 4.执行函数,将得到一个新对象
      var clone = cloneObj(obj);
      // 5.更改 obj 属性值
      obj.like = "wind";
      // 6.输出
      console.log(clone);

结果:
image.png

可以看到,就是新建一个空对象,还是循环直接赋值给它,这时改变 obj 的like属性值 ,新建的那个对象也不受影响了。但是,如果 obj 是下面这种形式的呢:

 var obj = {
        name: "北极光之夜。",
        like: "aurora",
        num: {
          a: "1",
          b: "2",
        },
      };

此时再用上面那种方法就不行了,如果obj只改变像 name 这种属性还没问题,但是当 obj 改变得是像 num 这种引用类型(对象、数组都是引用类型)的数据时,拷贝的对象还是能被影响,因为浅拷贝只能拷贝一层,如果拷贝的对象里还有子对象的话,那子对象拷贝其是也只是得到一个地址指向而已。这通过上面代码也能看出,就一层循环而已。想要真的达到我命由我不由天的话得用深拷贝,真正的刨根问底。深拷贝见第三大点。下面介绍下浅拷贝常用的方法,当对象只有一层的时候还是用浅拷贝好。

浅拷贝常用的方法:

1.第一种是主要利用 for in 遍历原对象的属性。

// 封装一个函数,实现传入一个对象返回一个拷贝后的新对象
 function cloneObj(obj) {
    let clone = {};
    // 用 for  in 遍历obj的属性
    for (let i in obj) {
      clone[i] = obj[i];
    }
    return clone;
  }

2.可以用Object.keys()方法:

Object.keys() 方法会返回一个由一个给定对象的自身可枚举属性组成的数组。

function cloneObj(obj) {
    let clone = {};
    for (let i of Object.keys(obj)) {
      clone[i] = obj[i];
    }
    return clone;
  }

3.可以用Object.entries()方法:

Object.entries()方法返回一个给定对象自身可枚举属性的键值对数组。

  function cloneObj(obj) {
    let clone = {};
    for (let [key, value] of Object.entries(obj)) {
      clone[key] = value;
    }
    return clone;
  }

4.可用Object.getOwnPropertyNames()配合forEach循环:

Object.getOwnPropertyNames()返回一个由它的属性构成的数组。

 function cloneObj(obj) {
    let clone = {};
    Object.getOwnPropertyNames(obj).forEach(function (item) {
      clone[item] = obj[item];
    });
    return clone;
  }

5.可用Object.defineProperty()方法:

Object.defineProperty(obj, prop, descriptor) 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。obj要定义属性的对象。prop要定义或修改的属性的名称或 Symbol。descriptor要定义或修改的属性描述符。

Object.getOwnPropertyDescriptor():返回指定对象上一个自有属性对应的属性描述符。
属性描述符:JS 提供了一个内部数据结构,用来描述对象的值、控制其行为。称为属性描述符。

function cloneObj(obj) {
        let clone = {};
        Object.getOwnPropertyNames(obj).forEach(function (item) {
          // 获取原本obj每个属性修饰符
          var des = Object.getOwnPropertyDescriptor(obj, item);
          // 把属性修饰符赋值给新对象
          Object.defineProperty(clone, item, des);
        });
        return clone;
      }

还有很多方法,就不一一列举了

三.深拷贝常见方法:

深拷贝就不会像浅拷贝那样只拷贝一层,而是有多少层我就拷贝多少层,要真正的做到全部内容都放在自己新开辟的内存里。可以利用递归思想实现深拷贝。

1.可以如下实现,还是用 for in 循环,如果为属性对象则递归:

function cloneObj(obj) {
        let clone = {};
        for (let i in obj) {
          // 如果为对象则递归更进一层去拷贝
          if (typeof obj[i] == "object" && obj[i] != null) {
            clone[i] = cloneObj(obj[i]);
          } else {
            clone[i] = obj[i];
          }
        }
        return clone;
      }

试一试看:

 // 1.建一个对象
 var obj = {
        name: "北极光之夜。",
        like: "aurora",
        age: {
          a: 1,
          b: 2,
        },
      };

      // 2. 封装一个函数,实现传入一个对象返回一个拷贝后的新对象
      function cloneObj(obj) {
        let clone = {};
        for (let i in obj) {
          // 如果为对象则递归更进一层去拷贝
          if (typeof obj[i] == "object" && obj[i] != null) {
            clone[i] = cloneObj(obj[i]);
          } else {
            clone[i] = obj[i];
          }
        }
        return clone;
      }
      // 4.执行函数,将得到一个新对象
      var clone = cloneObj(obj);
      // 5.更改 obj 属性值
      obj.age.a = "666";
      // 6.输出
      console.log(clone);

结果如下,拷贝成功,原对象改变无法使新对象也改变:
image.png

2.如果对象里面有数组怎么办,数组也跟对象一样是引用类型,那么我们可以在开头加个判断它是对象还是数组,数组的话赋空数组,一样遍历拷贝:

 function cloneObj(obj) {
        // 通过原型链判断 obj 是否为数组
        if (obj instanceof Array) {
          var clone = [];
        } else {
          var clone = {};
        }
        for (let i in obj) {
          // 如果为对象则递归更进一层去拷贝
          if (typeof obj[i] == "object" && obj[i] != null) {
            clone[i] = cloneObj(obj[i]);
          } else {
            clone[i] = obj[i];
          }
        }
        return clone;
      }

试一试看:

var obj = {
        name: "北极光之夜。",
        like: "aurora",
        age: {
          a: [1, 2, 3],
          b: 2,
        },
      };

      // 2. 封装一个函数,实现传入一个对象返回一个拷贝后的新对象
      function cloneObj(obj) {
        // 先判断 obj 是否为数组
        if (obj instanceof Array) {
          var clone = [];
        } else {
          var clone = {};
        }
        for (let i in obj) {
          // 如果为对象则递归更进一层去拷贝
          if (typeof obj[i] == "object" && obj[i] != null) {
            clone[i] = cloneObj(obj[i]);
          } else {
            clone[i] = obj[i];
          }
        }
        return clone;
      }
      // 4.执行函数,将得到一个新对象
      var clone = cloneObj(obj);
      // 5.更改 obj 属性值
      obj.age.a[1] = "666";
      // 6.输出
      console.log(clone);

结果没问题:
image.png

当然,也可用Array.isArray(obj)方法用于判断一个对象是否为数组。如果对象是数组返回 true,否则返回 false。

function cloneObj(obj) {
        // 判断 obj 是否为数组
        if (Array.isArray(obj)) {
          var clone = [];
        } else {
          var clone = {};
        }
        for (let i in obj) {
          // 如果为对象则递归更进一层去拷贝
          if (typeof obj[i] == "object" && obj[i] != null) {
            clone[i] = cloneObj(obj[i]);
          } else {
            clone[i] = obj[i];
          }
        }
        return clone;
      }

四.总结:

以上就是深浅拷贝的大致内容啦。因为对象是引用类型,所以直接赋值对象给新变量,那么新变量指向的内存和原对象是一样的。所以我们通过浅拷贝和深拷贝实现开辟自己的内存空间。而浅拷贝只拷贝一层,深拷贝拷贝全部。如果,文章有什么错误的,恳请大佬指出。

点击关注,第一时间了解华为云新鲜技术~


华为云开发者联盟
1.4k 声望1.8k 粉丝

生于云,长于云,让开发者成为决定性力量