文章目录
“深拷贝” 与 “浅拷贝”
基础概念
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 ] = arr
arr[2] = 5
console.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 } = obj
obj.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); // false
const 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 JSON
BigInt转换错误
- 使用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。
- 对象循环引用的时候,会直接报错。