“深拷贝” 与 “浅拷贝”
基础概念
JavaScript的数据类型
-
Javascript有五种基本数据类型和两种引用类型
-
基本类型(栈内存) Undefined,Null,Boolean,Number和String
-
引用数据类型(堆内存)
- Object
- Array
-
Undefined和Null的区别
- Undefined类型只有一个值,就是undefined,已声明未赋值的变量输出的结果
- Null类型也只有一个值,也就是null, 一个不存在的对象的结果
深浅拷贝使用对象
- 主要针对复杂数据类型(Object,Array)的复制问题。 浅拷贝与深拷贝都可以实现在已有对象上再生出一份的作用。但是对象的实例是存储在堆内存中然后通过一个引用值去操作对象,由此拷贝的时候就存在两种情况了-
浅拷贝和深拷贝的区别
- 拷贝引用和拷贝实例的区别
- 浅拷贝(shallow copy)-
- 只复制指向某个对象的指针,而不复制对象本身,新旧对象共享一块内存;
- 深拷贝(deep copy)-复制并创建一个一摸一样的对象,不共享内存,修改新对象,旧对象保持不变。
浅拷贝的实现
浅拷贝的意思就是只复制引用,而未复制真正的值,有时候我们只是想备份数组,但是只是简单让它赋给一个变量,改变其中一个,另外一个就紧跟着改变。
对象的浅拷贝-
var obj = { name:'Hanna Ding', age: 22}var obj2 = obj;obj2['c'] = 5;console.log(obj); //Object {name: "Hanna Ding", age: 22, c: 5}console.log(obj2); //Object {name: "Hanna Ding", age: 22, c: 5}数组的浅拷贝-
var arr = [1, 2, 3, '4'];
var arr2 = arr;arr2[1] = "test";console.log(arr); // [1, "test", 3, "4"]console.log(arr2); // [1, "test", 3, "4"]
arr[0]="fisrt"console.log(arr); // ["fisrt", "test", 3, "4"]console.log(arr2); // ["fisrt", "test", 3, "4"]- 利用 = 赋值操作符实现了一个浅拷贝
- 随着 arr2 和 arr 改变,arr 和 arr2 也随着发生了变化
深拷贝的实现
数组的深拷贝
使用slice()和concat()方法来解决上面的问题
slice()
var arr = ['a', 'b', 'c'];var arrCopy = arr.slice(0);arrCopy[0] = 'test'console.log(arr); // ["a", "b", "c"]console.log(arrCopy); // ["test", "b", "c"]concat()
var arr = ['a', 'b', 'c'];var arrCopy = arr.concat();arrCopy[0] = 'test'console.log(arr); // ["a", "b", "c"]console.log(arrCopy); // ["test", "b", "c"]局限性
slice() 和 concat()拷贝的局限性
var arr1 = [{"name":"Roubin"},{"name":"RouSe"}];//原数组var arr2 = [].concat(arr1);//拷贝数组arr1[1].name="Tom";console.log(arr1);//[{"name":"Roubin"},{"name":"Tom"}]console.log(arr2);//[{"name":"Roubin"},{"name":"Tom"}]结论-使用.concat()和浅拷贝的结果一样 那slice()会出现什么结果
var arr1 = [{"name":"weifeng"},{"name":"boy"}];//原数组var arr2 = arr1.slice(0);//拷贝数组arr1[1].name="girl";console.log(arr1);// [{"name":"weifeng"},{"name":"girl"}]console.log(arr2);//[{"name":"weifeng"},{"name":"girl"}结论-使用.slice()和浅复制的结果一样
var a1=[["1","2","3"],"2","3"];var a2=a1.slice(0);a1[0][0]=0; //改变a1第一个元素中的第一个元素console.log(a1); //[["0","2","3"],"2","3"]console.log(a2); //[["0","2","3"],"2","3"]- 由于数组的内部属性值是引用对象(Object,Array),slice和concat对对象数组的拷贝,整个拷贝还是浅拷贝,拷贝之后数组各个值的指针还是指向相同的存储地址.
- 因此,slice和concat这两个方法,仅适用于对不包含引用对象的一维数组的深拷贝
arrayObj.slice(start, [end]) 该方法返回一个 Array 对象,其中包含了 arrayObj 的指定部分。不会改变原数组 arrayObj.concat() 方法用于连接两个或多个数组。该方法不会改变现有的数组,而仅仅会返回被连接数组的一个副本。 其实也就是下面实现的方式,但还是用上面的方法来实现比较简单高效些
function deepCopy(arr1, arr2) { for (var i = 0; i < arr1.length; ++i) { arr2[i] = arr1[i]; }}ES6扩展运算符实现数组的深拷贝
var arr = [1,2,3,4,5]var [ ...arr2 ] = arrarr[2] = 5console.log(arr) //[1,2,5,4,5]console.log(arr2) //[1,2,3,4,5]对象的深拷贝
-
对象的深拷贝实现原理- 定义一个新的对象,遍历源对象的属性并赋给新对象的属性
-
两种方案-
- 利用递归来实现每一层都重新创建对象并赋值
- 利用 JSON 对象中的 parse 和 stringify
ES6扩展运算符实现对象的深拷贝
var obj = { name: 'FungLeo', sex: 'man', old: '18'}var { ...obj2 } = objobj.old = '22'console.log(obj) ///{ name: 'FungLeo', sex: 'man', old: '22'}console.log(obj2) ///{ name: 'FungLeo', sex: 'man', old: '18'}var obj = { name:'xiao ming', age: 22}
var obj2 = new Object();obj2.name = obj.name;obj2.age = obj.age
obj.name = 'xiaoDing';console.log(obj); //Object {name: "xiaoDing", age: 22}console.log(obj2); //Object {name: "xiao ming", age: 22}obj2是在堆中开辟的一个新内存块,将obj1的属性赋值给obj2时,obj2是同直接访问对应的内存地址。
- 递归的方法
- 递归的思想就很简单了,就是对每一层的数据都实现一次 创建对象->对象赋值的操作。
function deepClone(source){ const targetObj = source.constructor === Array ? [] : {}; // 判断复制的目标是数组还是对象 for(let keys in source){ // 遍历目标 if(source.hasOwnProperty(keys)){ if(source[keys] && typeof source[keys] === 'object'){ // 如果值是对象,就递归一下 targetObj[keys] = source[keys].constructor === Array ? [] : {}; targetObj[keys] = deepClone(source[keys]); }else{ // 如果不是,就直接赋值 targetObj[keys] = source[keys]; } } } return targetObj;}
var obj = { name: 'Hanna', age: 22}var objCopy = deepClone(obj)obj.name = 'ding';console.log(obj);//Object {name: "ding", age: 22}console.log(objCopy);//Object {name: "Hanna", age: 22}-
对象与Json相互转换 JSON.stringify/parse的方法
-
JSON.stringify()-是将一个 JavaScript 值转成一个 JSON 字符串。
-
JSON.parse():是将一个 JSON 字符串转成一哥JavaScript 值或对象。 JavaScript 值和 JSON 字符串的相互转换。
function deepClone(origin){ var clone={}; try{ clone= JSON.parse(JSON.stringify(origin)); } catch(e){
} return clone;
}未封装和封装的进行比较-
const originArray = [1,2,3,4,5];const cloneArray = JSON.parse(JSON.stringify(originArray));console.log(cloneArray === originArray); // falseconst originObj = {a:'a',b:'b',c:[1,2,3],d:{dd:'dd'}};const cloneObj = JSON.parse(JSON.stringify(originObj));console.log(cloneObj === originObj); // false
cloneObj.a = 'aa';cloneObj.c = [1,1,1];cloneObj.d.dd = 'tt';
console.log(cloneObj);console.log(originObj);/****************封装层**************/function deepClone(origin){ var clone={}; try{ clone= JSON.parse(JSON.stringify(origin)); } catch(e){
} return clone;
}const originObj = {a:'a',b:'b',c:[1,2,3],d:{dd:'dd'}};const cloneObj = deepClone(originObj);console.log(cloneObj === originObj); // false //改变值cloneObj.a = 'aa';cloneObj.c = [4,5,6];cloneObj.d.dd = 'tt';
console.log(cloneObj); // {a:'aa',b:'b',c:[1,1,1],d:{dd:'tt'}};console.log(originObj);// {a:'a',b:'b',c:[1,2,3],d:{dd:'dd'}};- 虽然上面的深拷贝很方便(请使用封装函数进行项目开发以便于维护),但是,只适合一些简单的情景(Number, String, Boolean, Array, Object),扁平对象,那些能够被 json 直接表示的数据结构。function对象,RegExp对象是无法通过这种方式深拷贝。
JSON.stringify()在深浅拷贝中的坑
处理undefined、Function和Symbol值
- undefined、Function和Symbol值不是有效的JSON值,用JSON.stringify()转换对象时,如果对象中有以上值时会被省略,或者被更改为null。
例如-
const obj1 = { foo: function() {}, bar: undefined, baz: Symbol('example')};const obj2 = { arr: [function(){}]};const jsonString = JSON.stringify(obj1);console.log('obj1转换后的值为:',jsonString);console.log('obj2转换后的值为:',JSON.stringify(obj2));// 输出: '{}'// 输出: {"arr":[null]}布尔、数字和字符串对象
- 布尔、数字和字符串对象在字符串化过程中会被转换为它们对应的原始值。
const boolObj = new Boolean(true);const jsonString = JSON.stringify(boolObj);console.log(jsonString); // 输出: 'true'忽略Symbol键的属性
- Symbol键属性在字符串化过程中完全被忽略,即使使用替换函数也是如此。这意味着与Symbol键关联的任何数据都将在生成的JSON字符串中被排除。
const obj1 = { [Symbol('example')]: 'value'};const obj2 = { [Symbol('example')]: [function(){}] };const jsonString = JSON.stringify(obj1);console.log(jsonString);console.log(JSON.stringify(obj2)); // 均输出: '{}'处理无穷大(Infinity)、NaN和Null值
- Infinity、NaN 和 null 值在字符串化过程中都被视为 null。
const obj = { value: Infinity, error: NaN, nothing: null };const jsonString = JSON.stringify(obj);console.log(jsonString);// 输出: '{"value":null,"error":null,"nothing":null}'Date对象被视为字符串
- Date实例通过实现toJSON()函数来返回一个字符串(与date.toISOString()相同),因此在字符串化过程中被视为字符串。
const dateObj = new Date();const jsonString = JSON.stringify(dateObj);console.log(jsonString);// 输出-"2024-01-31T09:42:00.179Z"循环引用异常
如果 JSON.stringify() 遇到具有循环引用的对象,它会抛出一个错误。循环引用发生在一个对象在循环中引用自身的情况下。
const circularObj = { self: null };circularObj.self = circularObj;JSON.stringify(circularObj);// Uncaught TypeError: Converting circular structure to JSONBigInt转换错误
- 使用JSON.stringify()转换BigInt类型的值时引发错误。
const bigIntValue = BigInt(42);JSON.stringify(bigIntValue); // Uncaught TypeError: Do not know how to serialize a BigInt总结
- 对象中有时间类型的时候,序列化之后会变成字符串类型。
- 对象中有undefined和Function类型数据的时候,序列化之后会直接丢失。
- 对象中有NaN、Infinity和-Infinity的时候,序列化之后会显示null。
- 对象循环引用的时候,会直接报错。