细说浅拷贝与深拷贝

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

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


正文开始...


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


  • 为什么会有浅拷贝与深拷贝
  • 浅拷贝是什么,深拷贝又是什么
  • 浅拷贝与深拷贝有何区别
  • 写一个例子佐证以上所有的观点


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


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


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


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


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

0a4811f2f4bff6a583394d6a4295d44e.png


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


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


值拷贝


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

f824a40904c98c02d54beecad920689d.png

用下面代码示例上图

var userName = 'Maic';
var age = 18;
var userInfo = {
  name: 'Maic',
  age: 18,
  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', age: 10, fav: { play1: 'swim', play2: 'basket ball' } } 
------
Tom 20 { name: 'jake', age: 10, fav: { 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', age: 18, fav: { play1: 'swim', play2: 'basket ball' } }
------ 
Tom 20 { name: 'jake', age: 10, fav: { play1: 'swim', play2: 'basket ball' } }

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


我们看下图理解下

98089b369971e5a7f4c18873d909f241.png

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


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


深拷贝


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


我们举个栗子

var userInfo = {
  name: 'Maic',
  age: 18,
  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',
  age: 18,
  fav: { play1: 'ping pang', play2: 'basket ball' }
} 
----------
{
  name: 'Maic',
  age: 18,
  fav: { play1: 'hello', play2: 'basket ball' }
}

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


  • 方案2


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

var userInfo = {
  name: 'Maic',
  age: 18,
  fav: {
    play1: 'ping pang',
    play2: 'basket ball'
  },
  fav2: [
    {
      a: 1,
      b: 2
    },
    {
      a: 3,
      b: 4
    },
  ]
}
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',
  age: 18,
  fav: { play1: 'ping pang', play2: 'basket ball' },
  fav2: [ { a: '666', b: 2 }, { a: 3, b: 4 } ]
} 
-------
{
  name: 'Maic',
  age: 18,
  fav: { play1: '111', play2: 'basket ball' },
  fav2: [ { a: '666', b: 2 }, { a: 3, b: 4 } ]
}

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

...
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',
  age: 18,
  fav: { play1: 'ping pang', play2: 'basket ball' },
  fav2: [ { a: 1, b: 2 }, { a: 3, b: 4 } ],
  fav3: [ 1, 2, 3 ]
} 
-------
{
  name: 'Maic',
  age: 18,
  fav: { play1: '111', play2: 'basket ball' },
  fav2: [ { a: '666', b: 2 }, { a: 3, b: 4 } ],
  fav3: [ 1, 2, 3 ]
}

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

6faba1437a1c977b0769978ac259f42f.png


深拷贝与浅拷贝的区别


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


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


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


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


总结


  • 值拷贝(直接赋值操作),主要区分基础数据类型与引用数据类型,如果是基础数据类型,那么新值修改不会影响原有的值,但是如果引用数据类型,那么新修改的值会影响原有数据类型
  • 浅拷贝,如果拷贝的对象内部属性是引用数据类型,那么像es6中的对象扩展符或者Object.assign都是浅拷贝操作,新拷贝的基础数据类型修改不会影响原有值,但是如果拷贝的是引用数据类型,那么新拷贝的值与原有值是同一份引用,新值修改会影响原有的值
  • 深拷贝,一句话,新拷贝的对象修改值不会影响原有值
  • 本文示例code example[1]


相关文章
|
8月前
|
存储 人工智能 前端开发
深拷贝浅拷贝的区别?如何实现一个深拷贝?
深拷贝浅拷贝的区别?如何实现一个深拷贝?
115 0
|
3月前
|
存储 前端开发 JavaScript
浅拷贝和深拷贝的区别?
本文首发于微信公众号“前端徐徐”,介绍了JavaScript中浅拷贝和深拷贝的概念及其实现方法。文章首先解释了数据类型的基础,包括原始值和对象的区别,然后详细介绍了浅拷贝和深拷贝的定义、底层逻辑以及常见的实现方式,如 `Object.assign`、扩展运算符、`JSON.stringify` 和手动实现等。最后,通过对比浅拷贝和深拷贝的区别,帮助读者更好地理解和应用这两种拷贝方式。
132 0
浅拷贝和深拷贝的区别?
|
4月前
|
存储 JSON 前端开发
栈在前端中的应用,顺便再了解下深拷贝和浅拷贝!
该文章探讨了栈在前端开发中的应用,并深入讲解了JavaScript中深拷贝与浅拷贝的区别及其实现方法。
栈在前端中的应用,顺便再了解下深拷贝和浅拷贝!
|
7月前
|
安全 Java
深拷贝和浅拷贝的区别
深拷贝和浅拷贝的区别
面试官:深拷贝与浅拷贝有啥区别?
面试官:深拷贝与浅拷贝有啥区别?
|
8月前
|
存储 Java Apache
【面试问题】深拷贝和浅拷贝的区别?
【1月更文挑战第27天】【面试问题】深拷贝和浅拷贝的区别?
|
JavaScript
一文弄懂浅拷贝和深拷贝
一文弄懂浅拷贝和深拷贝
56 0
|
存储 JavaScript 前端开发
深拷贝浅拷贝有什么区别?怎么实现深拷贝?
深拷贝浅拷贝有什么区别?怎么实现深拷贝?
106 0
|
JSON 数据格式
深拷贝和浅拷贝、及实现方式
深拷贝和浅拷贝、及实现方式
106 0
C++学习笔记_06 深拷贝和浅拷贝 2021-04-19
C++学习笔记_06 深拷贝和浅拷贝 2021-04-19

热门文章

最新文章