深拷贝与浅拷贝的实际应用

960 阅读6分钟

这是我参与8月更文挑战的第10天,活动详情查看:      8月更文挑战 ​

今天在做一个可操作性的表格功能时遇见一个问题,该表格的数据源来自三种表单,我用dialog弹出层,嵌套动态组件实现的这个功能,我的设计是每个表单维护自己的表单数据,在点击提交的时候,将当前表格数据,提交到父组件的表格数据对象中。

这时候问题出现了,我在添加第二笔的时候,第二笔竟然把第一笔数据覆盖了,表格中出现了两行第二笔数据,稍作分析,察觉到可能是因为浅拷贝的原因导致的,这是我实际业务中,第一次由于浅拷贝导致业务异常的情况,所以记录一下解决过程,并且复习一下深拷贝和浅拷贝的原理和带来的问题。

浅拷贝

我们先来看一个浅拷贝的案例

let a=[0,1,2,3,4],
let b=a;
console.log(a===b);   //true
a[0]=1;
console.log(b);  //1,1,2,3,4

明明b复制了a,为啥修改数组a,数组b也跟着变了,这里我不禁陷入了沉思,那么这里,就得引入基本数据类型与引用数据类型的概念了。

基本数据类型与引用数据类型

基本数据类型:

Number、String 、Boolean、Null和Undefined。基本数据类型是按值访问的,因为可以直接操作保存在变量中的实际值。 下图演示了这种基本数据类型赋值的过程:

引用类型数据:

也就是对象类型Object type,比如:Object 、Array 、Function 、Data等。

  1. javascript的引用数据类型是保存在堆内存中的对象。
  2. 与其他语言的不同是,你不可以直接访问堆内存空间中的位置和操作堆内存空间。只能操作对象在栈内存中的引用地址。所以,引用类型数据在栈内存中保存的实际上是对象在堆内存中的引用地址。通过这个引用地址可以快速查找到保存中堆内存中的对象。

下面我们来演示这个引用数据类型赋值过程:

总结区别

1.声明变量时不同的内存分配机制也带来了不同的访问机制

  1. 原始值:存储在栈(stack)中的简单数据段,也就是说,它们的值直接存储在变量访问的位置

这是因为这些原始类型占据的空间是固定的,所以可将他们存储在较小的内存区域 – 栈中。这样存储便于迅速查寻变量的值。

  1. 引用值:存储在堆(heap)中的对象,也就是说,存储在变量处的值是一个指针(point),指向存储对象的内存地址。

这是因为:引用值的大小会改变,所以不能把它放在栈中,否则会降低变量查寻的速度。相反,放在变量的栈空间中的值是该对象存储在堆中的地址。 地址的大小是固定的,所以把它存储在栈中对变量性能无任何负面影响。

  1. 引用值:在将一个保存着对象内存地址的变量复制给另一个变量时,会把这个内存地址赋值给新变量,
  2. 在javascript中是不允许直接访问保存在堆内存中的对象的,所以在访问一个对象时,

首先得到的是这个对象在堆内存中的地址,然后再按照这个地址去获得这个对象中的值,这就是传说中的按引用访问。 5. 而原始类型的值则是可以直接访问到的。

2. 复制变量时的不同

  1. 原始值:在将一个保存着原始值的变量复制给另一个变量时,会将原始值的副本赋值给新变量,此后这两个变量是完全独立的,他们只是拥有相同的value而已。

也就是说这两个变量都指向了堆内存中的同一个对象,他们中任何一个作出的改变都会反映在另一个身上。 2. 这里要理解的一点就是,复制对象时并不会在堆内存中新生成一个一模一样的对象,只是多了一个保存指向这个对象指针的变量罢了)。多了一个指针

3. 参数传递的不同(把实参复制给形参的过程)

首先我们应该明确一点:ECMAScript中所有函数的参数都是按值来传递的。 但是为什么涉及到原始类型与引用类型的值时仍然有区别呢?还不就是因为内存分配时的差别。

  1. 原始值:只是把变量里的值传递给参数,之后参数和这个变量互不影响。
  2. 引用值:对象变量它里面的值是这个对象在堆内存中的内存地址,这一点你要时刻铭记在心!

因此它传递的值也就是这个内存地址,这也就是为什么函数内部对这个参数的修改会体现在外部的原因了,因为它们都指向同一个对象。

深拷贝

深拷贝不会拷贝引用类型的引用,而是将引用类型的值全部拷贝一份,形成一个新的引用类型,这样就不会发生引用错乱的问题,使得我们可以多次使用同样的数据,而不用担心数据之间会起冲突 那么如何实现深拷贝呢?

第一种:利用序列化

 var obj1 = {
    a: 1,
    b: 2,
    c: 3
}
var objString = JSON.stringify(obj1);
var obj2 = JSON.parse(objString);
obj2.a = 5;
console.log(obj1.a);  // 1
console.log(obj2.a); // 5

小结:可以看到没有发生引用问题,修改obj2的数据,并不会对obj1造成任何影响,但是对应会造成一些数据的转变,那是因为 使用JSON.stringify()以及JSON.parse()它是不可以拷贝 undefined , function, RegExp 等等类型的

第二种:利用Object.assign(target, source)

 var obj1 = {
    a: 1,
    b: 2,
    c: ['a','b','c']
}
var obj2 = Object.assign({}, obj1);
obj2.c[1] = 5;
console.log(obj1.c); // ["a", 5, "c"]
console.log(obj2.c); // ["a", 5, "c"]

小结:可以看到对于一层对象来说是没有任何问题的,但是如果对象的属性对应的是其它的引用类型的话,还是只拷贝了引用,修改的话还是会有问题

第三种:利用递归

/**
 * 判断是否是基本数据类型
 * @param value
 */
function isPrimitive(value) {
  return (
    typeof value === 'string' ||
    typeof value === 'number' ||
    typeof value === 'symbol' ||
    typeof value === 'boolean'
  )
}
​
/**
 * 判断是否是一个js对象
 * @param value
 */
function isObject(value) {
  return Object.prototype.toString.call(value) === '[object Object]'
}
​
/**
 * 深拷贝一个值
 * @param value
 */
export default function cloneDeep(value) {
  // 记录被拷贝的值,避免循环引用的出现
  const memo = {}
​
  function baseClone(value) {
    let res
    // 如果是基本数据类型,则直接返回
    if (isPrimitive(value)) {
      return value
      // 如果是引用数据类型,我们浅拷贝一个新值来代替原来的值
    } else if (Array.isArray(value)) {
      res = [...value]
    } else if (isObject(value)) {
      res = { ...value }
    }
​
    // 检测我们浅拷贝的这个对象的属性值有没有是引用数据类型。如果是,则递归拷贝
    Reflect.ownKeys(res).forEach(key => {
      if (typeof res[key] === 'object' && res[key] !== null) {
        // 此处我们用memo来记录已经被拷贝过的引用地址。以此来解决循环引用的问题
        if (memo[res[key]]) {
          res[key] = memo[res[key]]
        } else {
          memo[res[key]] = res[key]
          res[key] = baseClone(res[key])
        }
      }
    })
    return res
  }
​
  return baseClone(value)
}
​

小结:利用这种封装的方法,可以完美解决该问题