面试经典问题:聊聊js中的深浅拷贝

199 阅读8分钟

前言

在面试的时候,如果面试官问对你说:请谈谈你对深拷贝和浅拷贝的理解,你会怎么回答这个问题呢?可能有很多小伙伴都不太理解深拷贝和浅拷贝的含义和区别,那么今天就和各位小伙伴分享一下我对二者的理解。

拷贝

  • 只针对引用类型
  • 基于原对象,拷贝得到一个新对象

其中拷贝分为浅拷贝和深拷贝,我们接下来分开细谈,带大家更深刻认识拷贝

浅拷贝

我先打个比方,浅拷贝就像把你拉到太阳底下,你的影子应该是和你形状长得一样的,而你动了你的影子也会跟着动,它是受你影响的,这个影子就像是你的复制体,你又能影响它,这个就是浅拷贝。接下来我们来逐步了解浅拷贝。

浅拷贝:新对象会受原对象影响

浅拷贝机制

只拷贝了对象的第一层,如果是基本类型就复制值,而里面的子对象拷贝的是引用地址

Object.create()

我们之前聊到过是不是所有对象都有隐式原型时,我们就遇到了这个Object.create(null)创建出来的对象没有隐式原型,今天我们又遇见它了。

let obj = {
  a: 1
}
let newObj = Object.create(obj)
obj.a = 2
console.log(newObj.a)
console.log(newObj)
console.log(newObj.__proto__)

image.png

我们看这段代码,我们用Object.create()帮我们创建一个新对象newObj ,我们能看到用newObj.a是能找到2的,但是我们直接输出newObj ,会发现他居然是空对象,这不是见鬼了吗? 此时肯定有聪明的读者想到了,在newObj里面找a,找不到就会去它的隐式原型上去找,我们输出它的隐式原型,

发现还真是能在隐式原型中找到a。所以Object.create()的原理就是 创建新对象内容为空,原来对象的内容变成新对象的隐式原型

它满足了我们对浅拷贝的定义,新对象会受到原对象的影响。但是它本身并不是原对象的复制体,它的内部没有原对象的方法和属性,此时它就处在一个模棱两可的地位,你认为它是它就是,你认为它不是它就不是,但是在面试被问起浅拷贝的时候如果能想起这个方法当然是个加分项。

Object.assign({}, obj)

Object.assign(a,b)方法能将对象b的内容拼接到对象a上并返回一个新对象,如果第一个参数的内容为空对象,那就是相当于把b复制了一份,现在我们来看看它是不是浅拷贝。

let obj = {
  a: 1,
  b: {
    n: 2
  }
}
let obj3 = Object.assign({}, obj)//将后面对象内容拼接到第一个里面
obj.a = 10
obj.b.n = 20
console.log(obj3)

image.png 我们可以看到输出了a为1,欸???怎么我改了原对象a的值为10,在新对象a中没有改变呢?我们别着急,继续往后看,我们修改的原对象中b中的n值,在新对象中被修改了,也就是新对象受到了原对象的影响,并且属性和方法和对象一样,因此用Object.assign({},obj)是浅拷贝。

[].concat(arr)

let arr3 = arr.concat(arr2) 我们首先要知道的是数组的concat方法是放在构造函数的原型上面的,不然它创造出来的实例对象arr就无法直接使用了,然后上面这段代码会将arr2拼接到arr后面,数组的拼接不改变原数组,返回一个新数组arr3

let arr = [1, 2, { n: 3 }]
// let arr2 = [3, 4]
// let arr3 = arr.concat(arr2)//把arr2拼接到arr后面、 concat放在原型里面了
// console.log(arr, arr2, arr3)//数组的拼接不改变原数组,返回一个新数组
let newArr = [].concat(arr)
arr[0] = 10
arr[2].n = 30
console.log(newArr)

image.png

我们看到了同样修改原数组中的基本类型arr[0]=10并没有成功,而修改arr[2].n=30却成功了,说明[].concat(arr)是浅拷贝。

[...arr]

现在大家看到三个点应该不陌生了把,在这里是剩余的数组元素的意思,我们没有敲定前面元素,因此这里...arr的意思就是把arr数组中全部的元素单独拿出来(这是es6的新用法),然后用中括号包裹起来,这个过程也是浅拷贝

image.png

slice()

slice()不会修改原数组 splice()会修改原数组

slice()可以接收两个参数,分别是开始切除的第一个元素的下标,结束切除的最后一个元素的下标,然后返回一个新数组,不给它传参数,他就会复制一份原数组,这个过程也是浅拷贝 image.png

arr.toReversed().reverse()

let arr = ['a', 'b', 'c', 'd', 'e']
console.log(arr.reverse()); //反转数组,会修改原数组
console.log(arr.toReversed().reverse());

使用reverse()反转数组会改变原数组,所以我们可以使用es6新增的一个方法toReversed,它能反转原数组赋值到一个新数组上,并且不会改变原数组,因此我们可以先对数组使用toReversed再使用reverse(),就能确保在不改变原数组的情况下通过反转实现复制数组。

手写浅拷贝方法

面试官经常会提及拷贝的问题,它可能不会问你这是浅拷贝还是深拷贝这种大家都知道的知识,它可能会另辟蹊径直接让你手写一个一个浅拷贝方法,这就让你必须了解浅拷贝的机制:贝对象的第一层,如果是基本类型就复制值,而里面的子对象拷贝的是引用地址。接下来我们直接开始讲解。

let person = {
  name: 'zhangsan',
  age: 18,
  like: {
    n: 'running'
  }
}
// 实现浅拷贝
function shallow(obj) {
  let res = {}
  for (let key in obj) {// for in 遍历对象的所有属性,包括隐式属性
    //key 是不是obj显示拥有的属性
    if (obj.hasOwnProperty(key)) {// 用hasOwnProperty()判断是否是显示属性
      res[key] = obj[key]
    }
  }
  return res
}
let res = shallow(person)
person.name = 'lisi'
person.age = 19
person.like.n = 'swimming'
console.log(res)

我们要得到一个新对象就要先创建一个空对象,我们要把原对象上面的所有属性都给新对象,这时候我们可以用for in 遍历对象的所有属性,注意用forin能遍历到对象的隐式属性,我们通常只需要得到原对象显示属性,因此我们可以用hasOwnProperty()来判断此时遍历到的属性是不是该对象的显示属性,如果是的话就执行res[key] = obj[key] 使用当前的键 key,将 obj 中同名属性的值赋给新对象 res。 只有原始对象 obj 的属性值的引用地址(对于对象类型)或值(对于基本类型)被拷贝到新对象 res 中。 刚好完美符合了浅拷贝的机制,最后返回res这个数组。

image.png 看结果,改name,age基本类型改不动,改原对象中的对象n改动了


深拷贝

平时在日常见到的拷贝绝大多数都是浅拷贝,让我们现在来认识认识深拷贝,浅拷贝像影子,深拷贝就像你的照片,你在拍的时候是什么样子(当然p图的不算),只要成为照片了就定格了当时的样子,十多年过去你的样子改变,但是你的照片里的样子不会改变。

JSON.parse(JSON.stringify(obj))

let obj = {
  name: '钟总',
  age: 18,
  like: {
    n: '金铲铲'
  },
  a: true,
  b: null,
  c: undefined,
  d: Infinity,
  e: -Infinity,
  f: NaN,
  g: Symbol(1),
  i: function () { }
}

let res = JSON.parse(JSON.stringify(obj))
obj.like.n = 'cs'
console.log(res);
//深拷贝

image.png 我们让钟总去打cs没有成功,他还是爱玩他的金铲铲,我们确实是实现了深拷贝,但是我们发现了数据里面怎么有这么多null,而且我的c,g,i怎么都不见了,因此用JSON.parse(JSON.stringify(obj))实现的深拷贝有很多缺点

1.不能识别bigint

2.不能拷贝undefined symbol function 无法处理值:NaN Infinity无穷大

3.无法处理循环引用

如果我们在obj中放了一个bigint,程序就会报错,第二点我们在结果里面清晰看到,我们重点来说说第三点:

image.png 我们让obj中的c的值为like的值,然后又让like.n的值等于c的值这样不就将两头连接起来形成了一个循环,这个就叫做循环引用。这也是JSON.parse(JSON.stringify(obj))无法识别的。

structuredClone()

在大家伙一直无法真正实现深拷贝的苦不堪言中,官方终于给做了以一个能实现深拷贝的方法

const user = {
  name: '朱总',
  like: {
    n: '泡脚',
    m: '吃鸡'
  }
}
const newUser = structuredClone(user)
user.like.n = '喝茶'
console.log(newUser)

image.png 我们也是看到朱总今天的脚是非泡不可了。

手写深拷贝

let obj = {
  name: '小朱',
  age: 18,
  like: {
    a: '吃饭'
  }
}

//手敲深拷贝
function deepClone(obj) {
  let clone = obj instanceof Array ? [] : {}

  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      if (typeof obj[key] === 'object' && obj[key] !== null) {
        //if(obj[key] instanceof Object){
        clone[key] = deepClone(obj[key])
      } else {
        clone[key] = obj[key]
      }
    }
  }
  return clone
}

let newObj = deepClone(obj)
obj.like.n = '睡觉'
console.log(newObj)

image.png 这里实现深拷贝就是在浅拷贝的前提下与运用递归,需要一点递归的知识,我们就不在这里深讲了,聪明的小伙伴可能在这里已经想到了,感兴趣的小伙伴可以自行去了解了解。

看到这里了就不妨动动手点个赞吧,谢谢大家