js - 深浅拷贝

1,198 阅读4分钟

现象

js真浅拷贝指的是对引用数据类型而言。

基本类型:Number,String,Boolean,Null,Undefined ,Symbol
引用类型:Object,Array,Date,RegExp,Function

引用类型地址存放在栈中,对象实际存放在堆中。具体可以查看文章js函数的参数都是按值传递怎么理解?

层级

  • 浅拷贝 只会将对象的各个属性进行依次复制,并不会进行递归复制,也就是说只会赋值目标对象的第一层属性。
  • 深拷贝不同于浅拷贝,它不只拷贝目标对象的第一层属性,而是递归拷贝目标对象的所有属性。

是否为属性开辟新的堆

  • 浅拷贝是创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址 ,所以如果其中一个对象改变了这个地址,就会影响到另一个对象。
  • 深拷贝是将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象,且修改新对象不会影响原对象。

赋值,浅拷贝和深拷贝的区别

  • 当我们把一个对象赋值给一个新的变量时,赋的其实是该对象的在栈中的地址,而不是堆中的数据。也就是两个对象指向的是同一个存储空间,无论哪个对象发生改变,其实都是改变的存储空间的内容,因此,两个对象是联动的。
  • 浅拷贝:重新在堆中创建内存,拷贝前后对象的基本数据类型互不影响,但拷贝前后对象的引用类型因共享同一块内存,会相互影响。
  • 深拷贝:从堆内存中开辟一个新的区域存放新对象,对对象中的子对象进行递归拷贝,拷贝前后的两个对象互不影响。

var objOrigin = {
	name: '耳东蜗牛',
  information: {
  	age: 27,
    base: 'shanghai'
  }
}

// 赋值操作
var objAssignment = objOrigin;

// 浅拷贝
var objShallowCopy = Object.assign({}, objOrigin)

// 深拷贝
var objDeepCopy = JSON.parse(JSON.stringify(objOrigin))

内存查看

image.png
image.png
image.png
image.png
image.png

图示

image.png


浅拷贝

方式

数组

Array.concat

let array = [1, 2, {name: 'rodchen'}];
let arrayShallow = array.concat([]);

array[2].name = '耳东蜗牛';
console.log(arrayShallow);

Array.slice

let array = [1, 2, {name: 'rodchen'}];
let arrayShallow = array.slice(1);

array[2].name = '耳东蜗牛';
console.log(arrayShallow);

ES6延展符

let array = [1, 2, {name: 'rodchen'}];
let arrayShallow = [...array];

array[2].name = '耳东蜗牛';
console.log(arrayShallow);

Array.map | Array.filter

let array = [1, 2, {name: 'rodchen'}];
let arrayMapShallow = array.map(item => item);

let arrayFilterShallow = array.filter(item => true);

array[2].name = '耳东蜗牛';
console.log(arrayMapShallow);
console.log(arrayFilterShallow);

对象

Object.assign

let obj = {
	age: 27,
  information: {
  	name: 'rodchen'
  }
}

let shallowObj = Object.assign({}, obj)
obj.information.name = '耳东蜗牛'
console.log(shallowObj)

对象延展符

let obj = {
	age: 27,
  information: {
  	name: 'rodchen'
  }
}

let shallowObj = {...obj}
obj.information.name = '耳东蜗牛'
console.log(shallowObj)

手动实现

function cloneShallow(source) {
  // 判断source是否为对象
  if (!(source instanceof Object)) {
    return source;
  }

  var target = source instanceof Array ? [] : {};

  for (var key in source) {
    if (Object.prototype.hasOwnProperty.call(source, key)) {
      target[key] = source[key];
    }
  }

  return target;
}



深拷贝

方式

JSON parse和stringify方法

  • undefined、任意的函数、正则表达式类型以及 symbol 值,在序列化过程中会被忽略(出现在非数组对象的属性值中时)或者被转换成 null(出现在数组中时);
  • 它会抛弃对象的constructor。也就是深拷贝之后,不管这个对象原来的构造函数是什么,在深拷贝之后都会变成Object;
  • 如果对象中存在循环引用的情况无法正确处理。

手动实现

初始版本


function cloneDeep(source) {
  // 判断source是否为对象
  if (!(source instanceof Object)) {
    return source;
  }

  var target = source instanceof Array ? [] : {};

  for (var key in source) {
    if (Object.prototype.hasOwnProperty.call(source, key)) {
      target[key] = source instanceof Object ? cloneDeep(source[key]) : source[key];
    }
  }

  return target;
}
let array = [1, 2, {name: 'rodchen'}];
let arrayShallow = cloneDeep(array);

array[2].name = '耳东蜗牛';
console.log(arrayShallow);  // Array [1, 2, Object { name: "rodchen" }]

let obj = {
  age: 27,
  information: {
  	name: 'rodchen'
  }
}

let shallowObj = cloneDeep(obj)
obj.information.name = '耳东蜗牛'
console.log(shallowObj) // Object { age: 27, information: Object { name: "rodchen" } }

循环引用

下面的例子当中,我们使用循环应用,执行上面的方法,汇报栈溢出。

let array = [1, 2];
array[2] = array
let arrayShallow = cloneDeep(array);  // Error: Maximum call stack size exceeded

array[2].name = '耳东蜗牛';
console.log(arrayShallow);

let obj = {
  age: 27,
}

obj.information = obj

let shallowObj = cloneDeep(obj)  // Error: Maximum call stack size exceeded
obj.information.name = '耳东蜗牛'
console.log(shallowObj)
function cloneDeep(source, hash = new WeakMap()) {
  // 判断source是否为对象
  if (!(source instanceof Object)) {
    return source;
  }
  
  if (hash.get(source)) return hash.get(source);
  
  var target = source instanceof Array ? [] : {};
  hash.set(source, target);

  for (var key in source) {
    if (Object.prototype.hasOwnProperty.call(source, key)) {
      target[key] = source instanceof Object ? cloneDeep(source[key], hash) : source[key];
    }
  }

  return target;
}

这里的weakMap可以通过ES5数组代替。


适配方法,日期,正则

function cloneDeep(source, hash = new WeakMap()) {
  // 判断source是否为对象
  if (!(source instanceof Object)) {
    return source;
  }
  
  if (source instanceof Function) return new Function("return " + source.toString())();
  if (source instanceof Date) return new Date(source);
  if (source instanceof RegExp) return new RegExp(source);
  
  if (hash.get(source)) return hash.get(source);
  
  var target = source instanceof Array ? [] : {};
  hash.set(source, target);

  for (var key in source) {
    if (Object.prototype.hasOwnProperty.call(source, key)) {
      target[key] = source instanceof Object ? cloneDeep(source[key], hash) : source[key];
    }
  }

  return target;
}

第三方

  • Underscore —— _.clone():
这个方法实际上是一种浅复制 (shallow-copy)
  • jQuery —— .clone()/.clone() / .extend():
在 jQuery 中也有这么一个叫 $.clone()的方法,可是它并不是用于一般的 JS 对象的深复制,而是用于 DOM 对象。
与 Underscore 类似,我们也是可以通过 $.extend() 方法来完成深复制。
值得庆幸的是,我们在 jQuery 中可以通过添加一个参数来实现递归extend。
调用$.extend(true, {}, ...)就可以实现深复制。
  • lodash —— _.clone() / _.cloneDeep():
在lodash中关于复制的方法有两个,分别是_.clone()和_.cloneDeep()。
其中_.clone(obj, true)等价于_.cloneDeep(obj)
// npm install --save @jsmini/clone
import { clone, cloneJSON, cloneLoop, cloneForce } from '@jsmini/clone';



参照

segmentfault.com/a/119000001…
segmentfault.com/a/119000001…
jerryzou.com/posts/dive-…
juejin.cn/post/684490…