前端面试之浅拷贝和深拷贝?

Author Avatar
EmptinessBoy 5月 26, 2021
  • 在其它设备中阅读本文章

这是昨天在 Boss 上随手刷到的一个看起来非常简单的面试题,也就是 JS 的深浅拷贝,(翻译成人话的化就是如何将 JS 的对象完整的复制一份?

听起来十分简单是不是?

为啥不直接定义一个变量,然后让它等于原来的对象不就完事了?

哈哈,如果你也是这么想的,那恭喜你也掉坑里去啦!那么就让我来好好说说原因吧!

开始之前

这篇文章可能用到 VUE 的生命周期函数,如果你还不知道,可以阅读这一篇文章:

👉 传送门:VUE的生命周期是啥?

为了真正搞明白 JS 深拷贝和浅拷贝的原理,我准备了如下初始模板代码:

模板代码

模板代码基于 VUE2.6 的项目构建,html 模板如下:

<template>
  <div class="home">
    <p>list:{{ list }}</p>
    <p>listcp:{{ listcp }}</p>
    <p>obj:{{ obj }}</p>
    <p>objcp:{{ objcp }}</p>
  </div>
</template>

JS的 data 部分如下,我们构建了一个数组 list 和一个对象 obj 作为我们被拷贝的原始对象。listcp 和 objcp 则用来存放我们拷贝后的对象。

data() {
  return {
    list: ["吃", "睡", "就是玩!"],
    obj: {
      name: "学长帆帆",
      wechatid: "xuezhangfanfan",
      hobbies: ["吃", "睡", "就是玩!"],
      major: {
        web: [
          {
            lang1: "html",
            lang2: "xml",
          },
          "css",
          "js",
        ],
        webframework: ["bootdtrap", "vue.js", "flutter"],
      },
    },
  };
},

这时候运行代码,应该能看到浏览器上显示了原始的对象以及为空的拷贝后对象:

image-20210525222521449

for in 遍历数组和对象

在开始正式进行拷贝之前,我们还需要了解下 ES6 中的 for …… in 循环。我们在刚才代码的基础上,加入如下两个 for in 循环,使之分别遍历 list 和 obj 的内容。

mounted() {
  for (let item in this.list) {
    console.log(item);
  }
  for (let item in this.obj) {
    console.log(item);
  }
},

运行后打开控制台显示如下:

image-20210524200119790

可以看到如果遍历的对象为 Array,则 item 值为数组的索引;如果遍历的对象为 Object ,则 item 值为对象内的 key。

浅拷贝

直接复制对象

那么我们回过来看我们刚才遇到的问题。由于 JavaScript 中对象实际都以地址进行存储,如果我们只是简单的让一个变量等于原始的对象,这一步的等号其实只复制了对象的地址。如果其中拷贝前的对象的内容发生变化,则拷贝的对象也会发送变化(其实仍然是同一个对象)。不信的话,我们用下面的代码做一下实验:

mounted() {
  this.listcp = this.list;
  this.objcp = this.obj;
},

image-20210524201002634

可以看到运行后,浏览器输出了拷贝前和拷贝后的对象。那么我们尝试修改拷贝后的对象:

mounted() {
  this.listcp = this.list;
  this.objcp = this.obj;
  this.listcp[0] = "我改变了拷贝后的数组";
  this.objcp.name = "我改变了拷贝后的对象的name";
},

image-20210524201116383

可以看到,虽然我们修改的是拷贝后对象的内容,但是拷贝前的原始对象的内容也跟着一起改变了。

一次 for 循环拷贝

这时你是不是要说了,这个问题简单呀,我们只需要用一个 for in 循环遍历一轮对象内的元素,然后复制到新的对象中就行了呀!

话不多说,直接上代码!

mounted() {
  this.objcp = this.simpleClone(this.obj);
  this.listcp = this.simpleClone(this.list);
  this.listcp[0] = "我改变了拷贝后的数组";
  this.objcp.name = "我改变了拷贝后的对象的name";
},
methods: {
  simpleClone(obj) {
    let newObj;
    if (obj.constructor === Array) {
      newObj = [];
    } else {
      newObj = {};
    }
    for (let key in obj) {
      newObj[key] = obj[key];
    }
    console.log(newObj);
    return newObj;
  },
},

这里我写了一个简单的 for 循环遍历要拷贝对象的属性并赋值到新的对象。然后我们再尝试用刚才的方法修改拷贝后对象的某一条属性。

可以看到,现在我成功的修改了拷贝后对象的属性而没有影响到拷贝前的原始对象。

image-20210524203216666

那么是不是这样就完事大吉了呢?

其实不然,当我们尝试修改 objcp.hobbies[0]

this.objcp.hobbies[0] = "我修改了,拷贝后对象内的数组里的值";

image-20210524203647675

可以看到,拷贝前的原始数组又被修改了。

因为刚才的一次 for 循环只会将对象的各个属性进行依次复制,并不会进行递归复制,而 JavaScript 存储对象都是存地址的,所以浅复制会导致 objcp.hobbies 和 obj.hobbies 指向同一块内存地址。

深拷贝

懒人办法(使用JSON方法

由于 JS 中为我们提供了一种对象的序列化和反序列化方法 window.JSON.stringify() 和 window.JSON.parse()。使用 window.JSON.stringify() 可以快速把一个对象转换成字符串,而 window.JSON.parse() 可以快读的将字符串还原成对象,因此我们可以借用这两个简单的内置函数来拷贝一个对象。

cloneByJson(obj) {
  let newObj;
  newObj = JSON.parse(JSON.stringify(obj))
  return newObj;
},

递归实现深层拷贝

那么如果客户机不支持 windows.JSON 呢?

其实很简单,我们可以借用递归的算法,将对象的每一个属性依次拷贝过来!

mounted() {
  this.objcp = this.deepClone(this.obj);
  this.listcp = this.deepClone(this.list);
  this.listcp[0] = "我改变了拷贝后的数组";
  this.objcp.name = "我改变了拷贝后的对象的name";
  this.objcp.hobbies[0] = "我修改了,拷贝后对象内的数组里的值";
},
methods: {
  deepClone(obj) {
    let newobj = obj.constructor === Array ? [] : {};
    if (typeof obj !== "object") {
      return;
    } else {
      for (var i in obj) {
        newobj[i] = typeof obj[i] === "object" ? 
          //递归进行拷贝
          this.cloneObj(obj[i]) 
          : obj[i];
      }
    }
    return newobj;
  },
},

现在可以看到,我们修改拷贝后的对象并不会影响拷贝前的对象啦!

image-20210524204819390

最佳实践

将上面两段深拷贝的代码结合到一起,就可以得到下面最通用的深拷贝函数啦!直接 ctrl + c 就可以在项目中使用啦!

// 兼容不支持JSON的浏览器
cloneObj(obj) {
  if (typeof obj !== "object") {
    return;
  } 
  let newobj = obj.constructor === Array ? [] : {};
  if (window.JSON) {
    let str = JSON.stringify(obj); //序列化对象
    newobj = JSON.parse(str); //还原
  } else {
      for (let item in obj) {
      newobj[item] =
        typeof obj[item] === "object"
          ? this.cloneObj(obj[item])
          : obj[item];
    }
  }
  return newobj;
},

到这里,关于 JS 的深浅拷贝就和大家讲完啦!篇幅限制,演示的 VUE 项目完整代码请公众号后台回复 “代码深拷贝” 来获取。

尾图4

This blog is under a CC BY-NC-ND 4.0 Unported License
本文链接:https://coding.emptinessboy.com/2021/05/%E5%89%8D%E7%AB%AF%E9%9D%A2%E8%AF%95%E4%B9%8B%E6%B5%85%E6%8B%B7%E8%B4%9D%E5%92%8C%E6%B7%B1%E6%8B%B7%E8%B4%9D/