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

正文开始...

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

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

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

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

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

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

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

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

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

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

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

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

值拷贝

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

用下面代码示例上图

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' } }

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

我们看下图理解下

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

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

深拷贝

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

我们举个栗子

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 ]
}

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

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

深拷贝与浅拷贝的区别

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

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

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

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

总结

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

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

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

  • 本文示例code exampleopen in new window

扫二维码,关注公众号
专注前端技术,分享web技术
加作者微信
扫二维码 备注 【加群】
教你聊天恋爱,助力脱单
微信扫小程序二维码,助你寻他/她
专注前端技术,分享Web技术
微信扫小程序二维码,一起学习,一起进步
前端面试大全
海量前端面试经典题,助力前端面试