maic

V1

2022/07/18阅读:23主题:默认主题

细说js的浅拷贝与深拷贝

js的浅拷贝深拷贝在业务中时常有用到,关于浅拷贝深拷贝的剖析文章层出不穷,本文是笔者对于深拷贝与浅拷贝的理解,一起来夯实js语言基础知识的理解吧。

正文开始...

在阅读文章之前,本文主要从以下几个方面去探讨

  • 为什么会有浅拷贝与深拷贝

  • 浅拷贝是什么,深拷贝又是什么

  • 浅拷贝与深拷贝有何区别

  • 写一个例子佐证以上所有的观点

为什么会有浅拷贝与深拷贝

我们知道在js中基础数据类型是存放在栈内存中的,而引用数据类型是存放在栈地址引用的一个堆内存中。为什么两种数据会存放方式不同?这是一个值的思考的问题?我的猜想,引用数据类型是复杂的数据结构,本质上也是存放栈地址的引用,只是这个地址指向了另外一个堆内存空间,如果他们都是放在一起的话,就不太好区分你是基础数据类型,还是引用数据类型了。

首先它们都是拷贝,一个是浅,一个是深,我们先说结论,浅拷贝是基础数据类型的拷贝,只会拷贝一层,如果遇到拷贝的数据是引用数据,那么浅拷贝的数据与原有数据是同一份引用。

而深拷贝是遇到引用数据类型会创建一个新的对象,遍历原有对象,对新对象进行动态赋值,修改新对象引用不影响原有对象的属性值

我们用一个图来解释上面两段比较长的话

基础数据类型直接存放在栈地址内存中,而引用数据类型是存放在栈内存地址的引用中,这个引用实际上指向的区域是一块堆内存空间

在了解浅拷贝深拷贝之前,我们先来了解下值拷贝

值拷贝

当我对原有基础数据类型与引用数据类型进行赋值

用下面代码示例上图

var userName = 'Maic';
var age = 18;
var userInfo = {
  name'Maic',
  age18,
  fav: {
    play1'ping pang',
    play2'basket ball'
  }
}

var cacheUserName = userName;
var cacheAge = age;
// 对象值拷贝
var cacheUserInfo = userInfo;
cacheUserName = 'Tom';
cacheAge = 20;
cacheUserInfo.name = 'jake';
cacheUserInfo.age = 10;
cacheUserInfo.fav.play1 = 'swim';

console.log(userName, age, userInfo, '------', cacheUserName, cacheAge, cacheUserInfo);

然后运行node index.js 从执行结果上来看

Maic 18 { name'jake'age10fav: { play1'swim'play2'basket ball' } } 
------
Tom 20 { name'jake'age10fav: { play1'swim'play2'basket ball' } }

因此可以得出结论

  • 基础数据类型的赋值,是值的拷贝,会重新开辟一个栈空间,新拷贝的值修改不会影响原有数据类型

  • 引用数据类型的赋值,原有引用数据与新赋值的数据指向的是同一份地址,修改引用数据的属性会影响原来的

以上是两种数据类型值的拷贝,貌似与浅拷贝还有离得有点远

浅拷贝

于是我们看下对象扩展的浅拷贝

...
// 对象浅拷贝
var cacheUserInfo = { ...userInfo }
// 与下面等价
// var cacheUserInfo = Object.assign({}, userInfo);
// 修改值拷贝后值
cacheUserName = 'Tom';
cacheAge = 20;
cacheUserInfo.name = 'jake';
cacheUserInfo.age = 10;
cacheUserInfo.fav.play1 = 'swim';

console.log(userName, age, userInfo, '------', cacheUserName, cacheAge, cacheUserInfo);

我使用了es6对象扩展对原有对象进行拷贝,那么此时结果是怎么样

Maic 18 { name'Maic'age18fav: { play1'swim'play2'basket ball' } }
------ 
Tom 20 { name'jake'age10fav: { play1'swim'play2'basket ball' } }

不知道注意到没有,在引用数据类型的第一级如果这个属性是基础数据类型,那么修改并不会影响原有的值,如果属性是引用数据类型,那么这层结构会是一个值拷贝,修改新赋值属性,会影响到原有的对象属性

我们看下图理解下

因此我们可以得出结论,浅拷贝如果遇到基础数据类型,那么修改新值,不会影响原有的值,但是如果数据是引用数据类型,那么新修改的值会影响原有的值,因为新修改的与原修改的是同一份引用。

所以拷贝只会拷贝一层,如果数据是引用数据类型,实际上会直接引用同一份数据。

深拷贝

深拷贝顾名思义,从表现层来看就是,就是为了修改新数据而不影响原有数据而产生的。

我们举个栗子

var userInfo = {
  name'Maic',
  age18,
  fav: {
    play1'ping pang',
    play2'basket ball'
  }
}

当我需要修改userInfo.fav.play1的值,而不想影响原有userInfo对象的值,那么此时你就会想到深拷贝,那怎么深拷贝呢。

  • 方案1

利用JSON.stringify(data)拷贝对象

...
const newUseInfo = JSON.parse(JSON.stringify(userInfo));
newUseInfo.fav.play1 = 'hello';
console.log(userInfo, '----', newUseInfo);

结果:

{
  name'Maic',
  age18,
  fav: { play1'ping pang'play2'basket ball' }

----------
{
  name'Maic',
  age18,
  fav: { play1'hello'play2'basket ball' }
}

但是我们得考虑到JSON.stringify这种有种缺陷,必须是json对象,有其他比如方法这种会被自动过滤处理。而且如果json对象格式错误,就会抛出异常,所以我们看下另外一种方案。

  • 方案2

使用代理对象思想,将原有对象拷贝一份,然后再赋值

var userInfo = {
  name'Maic',
  age18,
  fav: {
    play1'ping pang',
    play2'basket ball'
  },
  fav2: [
    {
      a1,
      b2
    },
    {
      a3,
      b4
    },
  ]
}
const isType = (val) => {
  return (type) => Object.prototype.toString.call(val) === `[object ${type}]`
}
function deepMerge(target = {}{
  const ret = {};
  for (let key in target) {
    if (target.hasOwnProperty(key)) {
      if (isType(target[key])('Object')) {
        ret[key] = deepMerge(target[key])
      } else {
        ret[key] = target[key];
      }
    }
  }
  return ret;
}
const cacheObj = deepMerge(userInfo);
cacheObj.fav.play1 = '111';
cacheObj.fav2[0].a = '666';
console.log(userInfo, '-----', cacheObj);

最终结果是

{
  name'Maic',
  age18,
  fav: { play1'ping pang'play2'basket ball' },
  fav2: [ { a'666'b2 }, { a3b4 } ]

-------
{
  name'Maic',
  age18,
  fav: { play1'111'play2'basket ball' },
  fav2: [ { a'666'b2 }, { a3b4 } ]
}

但是如果数据中有数组,貌似数组的这种情况还是同一份值,那是因为直接赋值了

...
function deepMerge(target = {}) {
  const ret = {};
  for (let key in target) {
    if (target.hasOwnProperty(key)) {
      if (isType(target[key])('Object')) {
        ret[key] = deepMerge(target[key])
      } else {
        // 是因为这里直接赋值了操作
        ret[key] = target[key];
      }
    }
  }
  return ret;
}

于是需要多加一个条件,需要对数组进行判断

const isType = (val) => {
  return (type) => Object.prototype.toString.call(val) === `[object ${type}]`
}
function deepMerge(target{
  const ret = Array.isArray(target) ? [] : {};
  for (let key in target) {
    if (target.hasOwnProperty(key)) {
      if (isType(target[key])('Object')) {
        ret[key] = deepMerge(target[key])
      } else if (isType(target[key])('Array')) {
        // 判断数组,并再次递归,用数组concat方法添加该数据
        ret[key] = [].concat([...deepMerge(target[key])]);
      } else {
        ret[key] = target[key];
      }
    }
  }
  return ret;
}
const cacheObj = deepMerge(userInfo);
cacheObj.fav.play1 = '111';
cacheObj.fav2[0].a = '666';
console.log(userInfo, '----', cacheObj);

此时结果

{
  name'Maic',
  age18,
  fav: { play1'ping pang'play2'basket ball' },
  fav2: [ { a1b2 }, { a3b4 } ],
  fav3: [ 123 ]

-------
{
  name'Maic',
  age18,
  fav: { play1'111'play2'basket ball' },
  fav2: [ { a'666'b2 }, { a3b4 } ],
  fav3: [ 123 ]
}

以上用一个图来进一步理解下

真是人才,深拷贝原来是这样的

深拷贝与浅拷贝的区别

通过以上例子,我们已经知道

浅拷贝如果拷贝对象内部的数据是基础数据类型,那么直接拷贝,新对象修改值,不会影响原有的值,如果拷贝的对象是一个引用数据类型,那么会是一个值的引用,此时新拷贝对象修改其值会影响原有的值。浅拷贝只会拷贝一层,拷贝的内部引用数据类型是同一份。

深拷贝本质上就是无论原对象值是基础数据类型,还是引用数据类型,我新拷贝的对象修改对象内部的值,并不会影响原有对象的值

另外还要有一点值拷贝,也是赋值,基础数据类型赋值,新修改的数据不会影响原有的数据,但是如果是引用数据类型,那么新拷贝的值修改会影响原有数据

总结

  • 值拷贝(直接赋值操作),主要区分基础数据类型与引用数据类型,如果是基础数据类型,那么新值修改不会影响原有的值,但是如果引用数据类型,那么新修改的值会影响原有数据类型

  • 浅拷贝,如果拷贝的对象内部属性是引用数据类型,那么像es6中的对象扩展符或者Object.assign都是浅拷贝操作,新拷贝的基础数据类型修改不会影响原有值,但是如果拷贝的是引用数据类型,那么新拷贝的值与原有值是同一份引用,新值修改会影响原有的值

  • 深拷贝,一句话,新拷贝的对象修改值不会影响原有值

  • 本文示例code example[1]

参考资料

[1]

code example: https://github.com/maicFir/lessonNote/tree/master/javascript/15-深拷贝与浅拷贝

分类:

前端

标签:

JavaScript

作者介绍

maic
V1

公众号:Web技术学苑