一、为什么有深拷贝和浅拷贝?
这个要从js中的数据类型说起,js中数据类型分为基本数据类型和引用数据类型。
基本类型值
指的是那些保存在栈内存中的简单数据段,即这种值是完全保存在内存中的一个位置。包含Number
,String
,Boolean
,Null
,Undefined
,Symbol
。
引用类型值
指的是那些保存在堆内存中的对象,所以引用类型的值保存的是一个指针,这个指针指向存储在堆中的一个对象。除了上面的 6 种基本数据类型外,剩下的就是引用类型了,统称为 Object
类型。细分的话,有:Object
类型、Array
类型、Date
类型、RegExp
类型、Function
类型 等。
正因为引用类型的这种机制, 当我们从一个变量向另一个变量复制引用类型的值时,实际上是将这个引用类型在栈内存中的引用地址复制了一份给新的变量,其实就是一个指针。因此当操作结束后,这两个变量实际上指向的是同一个在堆内存中的对象,改变其中任意一个对象,另一个对象也会跟着改变。
因此深拷贝和浅拷贝只发生在引用类型中。简单来说他们的区别在于:
1. 层次
- 浅拷贝 只会将对象的各个属性进行依次复制,并不会进行递归复制,也就是说只会赋值目标对象的第一层属性。
- 深拷贝不同于浅拷贝,它不只拷贝目标对象的第一层属性,而是
递归
拷贝目标对象的所有属性。
2. 是否开辟新的栈
- 浅拷贝 对于目标对象第一层为
基本数据类型
的数据,就是直接赋值,即「传值」;而对于目标对象第一层为引用数据类型
的数据,就是直接赋存于栈内存中的堆内存地址,即「传址」,并没有开辟新的栈
,也就是复制的结果是两个对象指向同一个地址,修改其中一个对象的属性,则另一个对象的属性也会改变, - 深拷贝 而深复制则是
开辟新的栈
,两个对象对应两个不同的地址,修改一个对象的属性,不会改变另一个对象的属性。
二、浅拷贝
以下是实现浅拷贝的几种实现方式:
1.Array.concat()
const arr = [1, 2, 3, 4, [5, 6]]; const copy = arr.concat();\利用 concat()创建 arr的副本\改变基本类型值, 不会改变原数组 copy[0] = 2; arr; //[1,2,3,4,[5,6]]; \改变数组中的引用类型值,原数组也会跟着改变 copy[4][1] = 7; arr; //[1,2,3,4,[5,7]];
能实现类似效果的还有slice()和Array.from()等,大家可以自己尝试一下~
2.Object.assign()
const obj1 = { x: 1, y: 2 }; const obj2 = Object.assign({}, obj1); obj2.x = 2;\修改 obj2.x, 改变对象中的基本类型值 console.log(obj1) //{x: 1, y: 2} //原对象未改变 console.log(obj2) //{x: 2, y: 2} const obj1 = { x: 1, y: { m: 1 } }; const obj2 = Object.assign({}, obj1); obj2.y.m = 2;\修改 obj2.y.m, 改变对象中的引用类型值 console.log(obj1) //{x: 1, y: {m: 2}} 原对象也被改变 console.log(obj2) //{x: 2, y: {m: 2}}
三、深拷贝
1.JSON.parse()和JSON.stringify()
const obj1 = { x: 1, y: { m: 1 } }; const obj2 = JSON.parse(JSON.stringify(obj1)); console.log(obj1) //{x: 1, y: {m: 1}} console.log(obj2) //{x: 1, y: {m: 1}} obj2.y.m = 2; //修改obj2.y.m console.log(obj1) //{x: 1, y: {m: 1}} 原对象未改变 console.log(obj2) //{x: 2, y: {m: 2}}
这种方法使用较为简单,可以满足基本日常的深拷贝需求,而且能够处理JSON格式能表示的所有数据类型,但是有以下几个缺点:
- undefined、任意的函数、正则表达式类型以及 symbol 值,在序列化过程中会被忽略(出现在非数组对象的属性值中时)或者被转换成 null(出现在数组中时);
- 它会抛弃对象的constructor。也就是深拷贝之后,不管这个对象原来的构造函数是什么,在深拷贝之后都会变成Object;
- 如果对象中存在循环引用的情况无法正确处理。
2.递归 + 循环引用
function deepClone(source, hash = new WeakMap()) { const type = judgeType(source); let target; if (type === "array") { target = []; } else if (type === "object") { target = {}; } else { // 基础类型直接返回,不再具有下一层次 return source; } if (hash.has(source)) return hash.get(source); // 新增代码,查哈希表 hash.set(source, target); // 新增代码,哈希表设值 if (type === "array") { // eslint-disable-next-line for (let i = 0, len = source.length; i < len; i++) { target.push(deepClone(source[i], hash)); } } else if (type === "object") { // 对原型上的方法也拷贝了.... // eslint-disable-next-line for (const key in source) { target[key] = deepClone(source[key], hash); } } return target; } function judgeType(target) { // toString会返回对应不同的标签的构造函数 const toString = Object.prototype.toString; const map = { "[object Boolean]": "boolean", "[object Number]": "number", "[object String]": "string", "[object Undefined]": "undefined", "[object Null]": "null", "[object Object]": "object", "[object Array]": "array", "[object Function]": "function", "[object Date]": "date", "[object RegExp]": "regExp", }; if (target instanceof Element) { return "element"; } // console.log(toString.call(target)); return map[toString.call(target)]; } let symbol = Symbol(); let date = new Date(); let fn = () = > { return 0; }; let reg = /^a/; let obj = { string: "string", number: 1, boolean: true, null: null, undefined: undefined, symbol: symbol, arr: [1], obj: {}, fn: fn, date: date, reg: reg, }; let obj2 = deepClone(obj); obj.string = "test"; obj.number = 6; obj.boolean = false; obj.obj.a = 6; obj.arr[0] = 6; obj.fn = () = > { return 1; }; console.log("obj: ", obj); console.log("obj2: ", obj2);