js中的深拷贝和浅拷贝

48次阅读

共计 4513 个字符,预计需要花费 12 分钟才能阅读完成。

前言

本文主要总结一下 js 的基本数据类型与堆栈存储的联系,什么是浅拷贝、什么是深拷贝、有什么应用实例,以及如何自己实现深拷贝和怎么浅拷贝。
若有不对之处欢迎指正~ 希望能与大家一起学习,共同进步~

js 数据类型与堆栈存储的联系

基本类型和引用类型与存储方式

基本类型:undefined,null,Boolean,String,Number,Symbol

引用类型:Object,Array,Date,Function,RegExp

  • 基本类型 采用 栈存储:栈存储在内存中占固定大小。

  • 引用类型 采用 堆存储:变量标识符与变量在堆内存的存储地址(引用)保存在栈中,当解释器寻找该引用值时,会首先检索其在栈中的地址,取得地址后从堆中获得实体。

  • 闭包 的变量也是使用堆存储:因为 闭包 中的变量不会随着外层的函数从调用栈中销毁,所以将其实体保存在了堆内存中,是内层函数仍能访问外层已销毁的函数中的变量。

当我们讨论起深浅复制时,讨论的对象都是引用类型。

赋值操作

基本类型复制

var a = 1;
var b = a;
a = 3;
console.log(a);//3
console.log(b);//1

在栈内存中的数据发生数据变化的时候,系统会自动为新的变量分配一个新的之值在栈内存中,两个变量相互独立,互不影响的。

引用类型复制

var obj1 = {a:123,b:321}
var obj2 = obj1;
obj2.b = 111
console.log(obj1.b);//111

引用类型的复制,同样为新的变量 obj2 分配栈内存空间;但是变量对应的具体值是保存在堆内存中,栈中只是一个地址指针,所以当 obj2.b 改变时,obj1.b保存的地址

浅拷贝

即新的对象复制已有对象中的非对象属性的值和对象属性的引用。

实例:

Object.assign

var obj1 = {a:123,b:321,c:{name:'jay',song:'说好不哭'}}
var obj2 = {};
Object.assign(obj2,obj1)
obj2.b = 111
console.log(obj1.b);//321
obj2.c.song = '说了再见'
console.log(obj1.c)//{name: "jay", song: "说了再见"}

从上例子可以看出,Object.assign是一个浅拷贝,对于传入的第二个参数对象,当某个属性为对象时只会拷贝一份相同的内存地址。

注意事项:

  • 只会拷贝源对象的自身属性(不拷贝继承属性)
  • 不会拷贝对象不可枚举属性
  • 由于 undefinednull无法转为对象,所以不能作为 Object.assign 第一个参数(作为第二个参数相当于没有)

Array.prototype.slice

Object.assign 类似

var a = [1, 3, 5, { x: 1} ];
var b = Array.prototype.slice.call(a);
b[0] = 2;
console.log(a); // [1, 3, 5, { x: 1} ];
console.log(b); // [2, 3, 5, { x: 1} ];
// 浅拷贝后,数组 a[0]并不会随着 b[0]改变而改变,说明 a 和 b 在栈内存中引用地址并不相同。b[3].x = 2;
console.log(a); // [1, 3, 5, { x: 2} ];
console.log(b); // [2, 3, 5, { x: 2} ];
// 浅拷贝后,数组中对象的属性会根据修改而改变,说明浅拷贝的时候拷贝的已存在对象的对象的属性引用。

从上例子可以看出,Array.prototype.slice是一个浅拷贝,对于那些类型为对象的项,拷贝的是该项对应对象的内存地址(属性引用)。

Array.prototype.concat

let array = [{a: 1}, {b: 2}];
let array1 = [{c: 3},{d: 4},3];
let array2=array.concat(array1);
array1[2]=4;
console.log(array2);// [{ a: 1}, {b: 2}, {c: 3}, {d: 4},3 ]
console.log(array1);// [{ c: 3}, {d: 4},4 ]
array1[0].c=123;
console.log(array2);// [{ a: 1}, {b: 2}, {c: 123}, {d: 4},3 ]
console.log(array1);// [{ c: 123}, {d: 4},4 ]

从上例子可以看出,Array.prototype.concat是一个浅拷贝,对于那些类型为对象的项,拷贝的是该项对应对象的内存地址(属性引用)。

… 扩展运算符

  • 语法

var cloneObj = {...obj};

let obj = {a:1,b:{c:1}}
let obj2 = {...obj};
obj.a=2;
console.log(obj); //{a:2,b:{c:1}}
console.log(obj2); //{a:1,b:{c:1}}

obj.b.c = 2;
console.log(obj); //{a:2,b:{c:2}}
console.log(obj2); //{a:1,b:{c:2}}
let array1 = [{c: 3},{d: 4},3];
let array3 = [...array1]
array3[2] = 4
array3[1].c =2
console.log(array1)//[{c: 2},{d: 4},3];
console.log(array3)//[{c: 2},{d: 4},4];

从上例子可以看出,扩展运算符也是浅拷贝,对于那些类型为对象的项,拷贝的是该项对应对象的内存地址(属性引用)。

实现

实现原理:新的对象复制已有对象中非对象属性的值和对象属性的 引用, 也就是说对象属性并不复制到内存。(其实很简单,利用对象间赋值操作进一步应用到对象的属性上就 ok)

function cloneShallow(source) {var target = {};
    for (var key in source) {
      // 遍历一个 source 对象自有的、继承的、可枚举的、非 Symbol 的属性。对于每个不同的属性,语句都会被执行。if (Object.prototype.hasOwnProperty.call(source, key)) {
          //hasOwnProperty 会忽略掉那些从原型链上继承到的属性和自身属性。target[key] = source[key];
        }
    }
    return target;
}

深拷贝

即新的对象拷贝已有对象时会从堆内存中开辟另一个新的区域来存放。而不是拷贝其属性引用;修改新对象不会影响源对象。

实例

JSON.parse(JSON.stringify()))

let array1 = [1,3,{username: 'cxkk'}]
let array2 = JSON.parse(JSON.stringify(array1))
array2[0] =2;
array2[2].username = 'jay'
console.log(array1)//[1,3,{username: 'cxkk'}]
console.log(array2)//[2,3,{username: 'jay'}]

注意事项:

  • 拷贝对象的属性中出现的 函数 undefinedsymbol 经过 JSON.stringify 序列化后会消失
  • 无法拷贝不可枚举属性及对象的原型链
  • 拷贝 date 类型会变成字符串,拷贝 RegEXp 类型会变成空对象
  • 对象中含有 NaNInfinity-Infinity,则序列化的结果会变成null
  • 无法拷贝对象的循环引用(即obj[key] = obj)

实现:

利用递归简单实现

// 判断对象且不为空
function isObject(source){return typeof source ==='object' && source !=null}
// 递归实现深拷贝
function deepClone(source){if(!isObject(source)){throw new Error('error arguments', 'shallowClone');
    return source;// 非对象;则返回自身值
  }
  else{var target = Object.prototype.toString.call(source) === "[object Array]" ? []: {};
    for(var key in source){if(Object.prototype.hasOwnProperty.call(source,key)){if(isObject(source[key])){target[key] = Object.prototype.toString.call(source[key]) === "[object Array]" ? []: {};
          target[key] = deepClone(source[key]);// 递归复制值为对象的属性
        }
        else{target [key] = source[key]
        }
      }
    }
  }
  return target;
}
var obj1 = {
  str: '123',
  arr: [1, 2, 3],
  obj: {name: 'jay'},
  func: function(){return 1;}
};
var obj2 = deepClone(obj1);
obj2.str = '321';
obj2.arr[2] = 4;
console.log(obj2 === obj1); //false
console.log(obj2.obj === obj1.obj); //false
console.log(obj2.func === obj1.func); //true
obj2.func = function(){return 2}
console.log(obj2.func === obj1.func); //false

第三方深拷贝库

lodash提供的_.cloneDeep

var _ = require('lodash');
var obj1 = {
  str: '123',
  arr: [1, 2, 3],
  obj: {name: 'jay'},
  func: function(){return 1;}
};
var obj2 = _.cloneDeep(obj1);
console.log(obj2 === obj1); //false
console.log(obj2.obj === obj1.obj); //false
console.log(obj2.func === obj1.func); //true

jquery 提供的$.extend

var $ = require('jquery');
var obj1 = {
  str: '123',
  arr: [1, 2, 3],
  obj: {name: 'jay'},
  func: function(){return 1;}
};
var obj2 = $.extend(true, {}, obj1);// 第一个参数传入 true 表示深拷贝
console.log(obj2 === obj1); //false
console.log(obj2.obj === obj1.obj); //false
console.log(obj2.func === obj1.func); //true

参考文章:搞不懂 JS 中赋值·浅拷贝·深拷贝的请看这里

正文完
 0