# 赋值与浅拷贝、深拷贝

首先不要把引用类型的赋值(=)归结为浅拷贝,深拷贝和浅拷贝只针对像 Object, Array 这样的复杂对象的。简单来说,浅拷贝只拷贝一层对象的属性,而深拷贝则递归拷贝了所有层级。

# js数据类型

基本类型为Number、String、Boolean、Undefined、Null,引用类型为Object。

基本类型是储存存放在栈内存中的简单数据段,数据大小确定,内存空间大小可以分配,是直接按值存放的,所以可以直接访问。

引用类型存放在堆内存中,它的变量实际是存放在栈中的一个地址(指针)指向堆中的实际内存地址。

# 区分赋值、浅拷贝与深拷贝

赋值

基本类型赋值(=)直接在栈中开辟新内存,把值赋值到新内存中,而引用类型的赋值(=)是直接复制栈内存中的地址(指针),并没有在堆内存总开辟新的内存,因此他们都指向同一个堆内存,任何操作都会影响原来的数据。

let obj2 = obj1;

浅拷贝与深拷贝

首先不要把赋值和浅拷贝和深拷贝混在一起。

浅拷贝和深拷贝都在堆内存中开辟新的内存,都进行了复制。

浅拷贝就是进行简单的复制,只会将对象的各个属性进行依次复制,不会递归复制,所以那些子属性如果再是引用类型(对象、数组、函数)的话,就只复制了他们的地址(指针),实际的内存地址没有被复制。

所以就理解为什么数组方法slice、concat、扩展运算符、Object.assign()这些都是浅拷贝了。它们都进行了简单的拷贝,没有再对深层次的数据进行拷贝。

深拷贝是对对象以及对象的所有子对象进行拷贝。

# 实现浅拷贝

1、常见的浅拷贝方法

扩展运算符(...);
Object.assign();
Array.prototype.concat();
Array.prototype.slice();
lodash(_.clone());

2、简单封装一个

function clone(target) {
  var cloneTarget = {};
  for(var key in target) {
    cloneTarget[key] = target[key]
  }
  return cloneTarget;
}

# 实现深拷贝

1、JSON.parse(JSON.stringify());

虽然用起来很方便,但是,只适合一些简单的情景(Number, String, Boolean, Array, Object),扁平对象,那些能够被 json 直接表示的数据结构。function对象,RegExp对象是无法通过这种方式深拷贝。

let obj2=JSON.parse(JSON.stringify(obj1));

2、递归赋值

实习深拷贝,其实思路和浅拷贝一样,如果是基础类型就直接赋值,如果是引用类型,就循环遍历复制每一个子元素,子元素如果再是再接着遍历复制。就是用了递归思想。

简单实现一个对象递归复制:

function deepClone(target) {
  if (typeof target === 'object') {
    var cloneTarget = {};
    for(var key in target) {
      cloneTarget[key] = deepClone(target[key]);
    }
    return cloneTarget;
  } else {
    return target; // 不是引用类型的话直接返回,省去循环
  }
}

// 另一种方式,这种方式没有考虑基本类型

function deepClone(target) {
  var cloneTarget = {};
  for(var key in target) {
    if (target[key] && typeof target[key] === 'object') {
      cloneTarget[key] = deepClone(target[key]);
    } else {
      cloneTarget[key] = target[key];
    }
  }
  return cloneTarget;
}

兼容数组:

function deepClone(target) {
  if (typeof target === 'object') {
    var cloneTarget = Array.isArray(target) ? [] : {};
    for(var key in target) {
      cloneTarget[key] = deepClone(target[key]);
    }
    return cloneTarget;
  } else {
    return target; // 不是引用类型的话直接返回,省去循环
  }
}

到这一个基本的深拷贝就已经实现了,如果想继续深入,可以往下看。




循环引用:

当一个对象的子元素的值是自身时就会被无限循环,陷入死循环。

var target = {
  field1: 1,
  field2: undefined,
  field3: {
      child: 'child'
  },
  field4: [2, 4, 8]
};
target.target = target;

解决办法:额外开辟一个空间,来保存已经拷贝过的对象,当前对象需要拷贝时,去这个空间查找有没有拷贝过这个对象,有的话直接返回,没有继续拷贝。

这个存储空间,需要可以存储key-value形式的数据,且key可以是一个引用类型,我们可以选择Map这种数据结构:

  • 检查map中有无克隆过的对象
  • 有 - 直接返回
  • 没有 - 将当前对象作为key,克隆对象作为value进行存储
  • 继续克隆
function deepClone(target, map = new Map()) {
  if (typeof target === 'object') {
    var cloneTarget = Array.isArray(target) ? [] : {};
    if (map.get(target)) return map.get(target);
    map.set(target, cloneTarget);
    for (var key in target) {
      cloneTarget[key] = deepClone(target[key], map);
    }
    return cloneTarget;
  } else {
    return target;
  }
}

优点: 使用WeakMap代替map, WeakMap是弱引用,其键必须是对象,值是任意的。我们使用map是强引用,设置的map值会一直存储在内存中,不会自动清除。所以如果对象特别大的话使用弱引用的好处就体现了,js垃圾回收机制会自动帮我们回收,而不需要我们手动清除。

function deepClone(target, map = new WeakMap()) {
    // ...
};

兼容其他数据类型:日期/正则等特殊对象

// 判断是否是对象
// function isObject(target) {
//   return (typeof target === 'object' || typeof target === 'function') && target !== null;
// }
// function deepClone(target, map = new Map()) {
//   // 先判断该引用类型是否被 拷贝过
//   if (map.get(target)) {
//     return target;
//   }
//   // 获取当前值的构造函数:获取它的类型
//   var Ctor = target.constructor;
//   // 检测当前对象target是否与 正则、日期格式对象匹配
//   if (/^(RegExp|Date)$/i.test(Ctor.name)) {
//     return new Ctor(target); // 创建一个新的特殊对象(正则类/日期类)的实例
//   }
//   if (isObject(target)) {
//     map.set(target, true); // 为循环引用的对象做标记
//     var cloneTarget = Array.isArray(target) ? [] : {};
//     for (var key in target) {
//       if (target.hasOwnProperty(key)) {
//         cloneTarget[key] = deepClone(target[key], map);
//       }
//     }
//     return cloneTarget;
//   } else {
//     return target;
//   }
// }

参考链接: javascript中的深拷贝和浅拷贝 (opens new window)

js 深拷贝 vs 浅拷贝 (opens new window)

如何写出一个惊艳面试官的深拷贝? (opens new window)

JS中判断对象是对象还是数组的方法 (opens new window)