在对数组和对象处理过程中,涉及到变更数据的操作是十分敏感的。为避免出现问题,一般是在不修改原始数据基础上,通过复制一个备份并编辑它以实现数据的变更。本文介绍的是 JavaScript 的深复制和浅复制机制。

应用情景

数组和对象的内容需要变更时,考虑到数据可能被引用,一般情况下不对原始数据进行操作。

  let arr = [1,2,3,4,5,6];
  const num = (n)=> Math.pow(n,n);
  console.log(num(arr[2])); // 输出 27
  arr.splice(2,3); // 移除 2-4 位置内容
  console.log(num(arr[2])); // 输出 46656

本例当中原始数组发生了删节,这一改变直接影响了 num(arr[2]) 函数的执行结果。那么如果在执行 splice() 方法前将 arr 数组复制一个,并对复制数据做对应处理,是否可以避免对函数执行的影响呢?

  let arr = [1,2,3,4,5,6];
  let num  = (n)=> Math.pow(n,n);
  console.log(num(arr[2])); // 输出 27 
  /* 截至当前,与上例一致 */
  let arrCopy = arr; 
  arrCopy.splice(2,3); // 移除 2-4 位置内容
  console.log(num(arrCopy[2])); // 输出 46656
  console.log(num(arr[2])); // 输出 46656

修改后的例子,对原始数据做了复制操作,对复制后的数据进行删改。即便如此,我们还是注意到,复制内容的删改直接影响到了原始数据的内容,通过这样的复制并不能实现我们预期的设计目的。

概念

浅复制:当对象被复制时,包含子对象的属性被直接复制,由于 JavaScript 将对象存储为存储地址,直接被复制的属性将同原始数据的属性指向同一个内容。浅复制没有改变数据的存储位置,仅仅是将数据在内存中的引用复制了一份。

深复制:当对象被复制时,除了将原有属性全部复制外,还对包含子对象的属性进行递归复制操作。将原始数据完整的复制并存储在新的存储位置。深复制操作得到的目标内容的依赖链将不涉及到原始数据的引用。

代码实现

实现数组或对象的浅复制方法有很多,在 JavaScript 中常用的“引用赋值”操作就是浅复制的一种,除此之外还可以手动实现。

  let obj = {a:1,b:{c:2,d:3}};

  /* 
  × 浅复制函数
  × @param src
  × @returns dst
  */
  function shallowCopy(src){
    let dst = {};
    for (const key in src) {
      if (src.hasOwnProperty(key)) {
        dst[key] = src[key];
      }
    }
    return dst;
  }

  let copy = shallowCopy(obj);
  console.log(obj.b.d); // 输出 3
  copy.b.d = 10;
  console.log(obj.b.d); // 输出 10

浅复制由于将对象属性的引用直接复制,复制后的内容发生改变时,将影响原始数据的内容。

深复制操作,最常用的快捷方法是将数组或对象转为 Json 格式字符串后重新还原,这一操作虽然快捷简便但是不能处理函数,还会影响到复制数据的原型链结构。除转 Json 格式之外还可以手动实现递归函数复制对象及其含子对象的属性。

  let obj = {a:1,b:{c:2,d:3}};  

  /* 
  × 深复制函数
  × @param src
  × @returns dst
  */
  function deepCopy(src){
    let dst = src.constructor === Array? []: {};
    if (typeof src !== 'object') return;
    if (JSON){
      dst = JSON.parse( JSON.stringify(src));
    }else {
      for (const key in src) {
        if (src.hasOwnProperty(key)) {
          dst[key] = typeof src[key] === 'object'? deepCopy(src[key]): src[key];
        }
      }
    }
    return dst;
  }

  let copy = deepCopy(obj)
  console.log(obj.b.c); // 输出 2
  copy.b.c = 10;
  console.log(obj.b.c); // 输出 2
  console.log(copy.b.c); // 输出 10

本例函数可以处理数组和对象的深复制操作,当支持 Json 格式互转时使用对应方法,否则使用手动递归复制。

函数中请求对象的 constructor 属性判断其具体格式。

其他

目前已有的大量 JavaScript 类库使用浅复制的数据操作,这将带来很多未知的问题。

深复制方式可以很好的实现操作时的数据分离,由于其递归设计,深复制方式一般存在性能问题,建议慎重使用。