深拷贝与浅拷贝
复习:
- 基本数据类型:基本数据类型指的是简单数据段存放在栈中,按值访问。
- 引用数据类型:它存放在堆中,它在栈中存放了指针,该指针指向堆中它的数据;
深拷贝与浅拷贝的区别:
- 浅拷贝只能拷贝一层对象,如果有对象的嵌套,那么浅拷贝在该嵌套前表现得跟赋值一样。即在拷贝对象里,遇见基本类型,拷贝基本类型的值,遇到引用类型,拷贝的又是内存地址。
- 深拷贝解决了浅拷贝只能拷贝一层对象的问题,它将一个对象从内存拷贝出来放进另一个新区域存放新对象。
直接赋值
//基本数据类型
let a = 1;
let b = a;
b = 2;
console.log(a, b);
//引用数据类型
let arr = [0,1,2];
let brr = arr;
brr[0] = 1;
console.log(arr, brr);
控制台:
1 2
[1,1,2] [1,1,2]
显然改变brr的时候,它赋的是arr在栈中的指针(地址),所以它跟arr是同一个引用,它们都是指向同一块堆内存。
浅拷贝
Object.assign()
用于将所有可枚举属性的值从一个或多个源对象分配到目标对象。它将返回目标对象。
语法:
Object.assign(target, ...sources)
示例:
const obj = {name: "dzz", where: "juejin"};
const obj2 = Object.assign({}, obj, {name: "yly"});
console.log(obj, obj2);
控制台:
{name: "dzz", where: "juejin"} {name: "yly", where: "juejin"}
concat浅拷贝数组
用于合并两个或多个数组。此方法不会更改现有数组,而是返回一个新数组。
const arr = [0,1,2];
const arr2 = arr.concat();
arr2[0] = 1;
console.log(arr, arr2);
slice浅拷贝
返回一个新数组,由begin和end决定的原数组的浅拷贝
const arr = [0,1,2];
const arr2 = arr.slice();
arr2[0] = 1;
console.log(arr, arr2);
...展开运算符
let arr = [0,1,2];
let arr2 = [...arr];
arr2[0] = 1;
console.log(arr, arr2);
自身实现
function shallowCopy(target) {
//1. 保证传入的target为引用类型
if(typeof target !== "object" || target === null) {
return target;
}
//2. 判断传入的target是对象还是数组,在创建新的copyTarget
const copyTarget = Array.isArray(target) ? [] : {};
for(let item in target) {
//3.是自身的而不是原型链上的
if(target.hasOwnProperty(item)) {
copyTarget[item] = target[item];
}
}
return copyTarget;
}
拓展for...in和for...of
for...in:以任意顺序遍历一个对象的除Symbol以外的可枚举属性
语法:
for (variable in object)
variable:在每次迭代时,variable会被赋值为不同的属性名
object:非Symbol类型的可枚举属性被迭代的对象
最好不要用于Array
for...of:在可迭代对象(包括Array,Map,Set,String,arguments对象等等)上创建一个迭代循环,调用自定义迭代钩子,并为每个不同属性的值执行语句
语法:
for (variable of iterable)
variable:在每次迭代中,将不同属性的值分配给变量。
iterable:被迭代枚举其属性的对象。
测试:
let arr = [1,2,3];
for(let item of arr) {
console.log(item); //123
}
for(let item in arr) {
console.log(item); //012
}
深拷贝
JSON.parse()和JSON.stringfy()
json.stringfy() :将一个 JavaScript 对象或值转换为 JSON 字符串,如果指定了一个 replacer 函数,则可以选择性地替换值,或者指定的 replacer 是数组,则可选择性地仅包含数组指定的属性。
语法:developer.mozilla.org/zh-CN/docs/…
JSON.stringify(value[, replacer [, space]])
value: 将要序列化成 一个 JSON 字符串的值。
replacer: 如果该参数是一个函数,则在序列化过程中,被序列化的值的每个属性都会经过该函数的转换和处理;如果该参数是一个数组,则只有包含在这个数组中的属性名才会被序列化到最终的 JSON 字符串中;如果该参数为 null 或者未提供,则对象所有的属性都会被序列化。
space: 美化输出,指定空白字符串
注意:
- undefined,任意函数以及Symbol值在序列化过程中会被忽略。但是函数,undefined被单独转换时会返回undefined。
- 对象包含引用对象执行此方法,会抛出错误
const obj = {val: "juejin"};
obj.target = obj;
console.log(JSON.stringfy(obj)); //报错
控制台:JSON.stringfy is not a function
- Date 日期调用了 toJSON() 将其转换为了 string 字符串,会被当做字符串处理。
JSON.parse() :解析JSON字符串
语法:developer.mozilla.org/zh-CN/docs/…
JSON.parse(text[, reviver])
text:要被解析成 JavaScript 值的字符串
reviver:用来修改解析生成的原始值,调用时机在 parse 函数返回之前
小结:使用JSON.parse(JSON.stringfy())来深拷贝,不能将函数,undefined,Symbol,Data等进行正确的处理。
const obj = {
name: "dzz",
age: 12,
regexp: /\d/,
tfn: function() {},
tunde: undefined,
tnull: null,
tdate: new Date(),
syl: Symbol("juejin"),
}
const obj2 = JSON.parse(JSON.stringify(obj));
console.log("obj", obj);
console.log("obj2:", obj2);
控制台:obj {age: 12,name: "dzz",regexp: /\d/,syl: Symbol(juejin),tdate: Tue Aug 31 2021 10:22:18 GMT+0800 (中国标准时间) {},tfn: ƒ () ,tnull: null,tunde: undefined}
obj2{age: 12,name: "dzz",regexp: {},tdate: "2021-08-31T02:22:18.907Z",tnull: null}
由结果知道:正则变成了空对象;函数,undefined,Symbol直接被忽视,日期被当成字符串处理。
这就是它的缺陷(还有刚刚的循环引用)
自身实现
先开始想可以利用浅拷贝拷贝当前一层在递归下去(浅拷贝的自身实现)
简单实现:
function deepCopy(target) {
//1. 保证传入的target为引用类型,基本数据类型被返回
if(typeof target !== "object" || target === null) {
return target;
}
//2. 判断传入的target是对象还是数组,在创建新的copyTarget
const copyTarget = Array.isArray(target) ? [] : {};
for(let item in target) {
//3.是自身的而不是原型链上的
if(target.hasOwnProperty(item)) {
//4.将获取当前一层的值进行递归,引用类型被继续往下拷贝,基本数据类型直接被返回
copyTarget[item] = deepCopy(target[item]);
}
}
return copyTarget;
}
//----------------------------测试
const obj = {
name: "dzz",
child: {
name: "xxx"
},
tun: undefined,
tnull: null,
a: Symbol("juejin"),
};
const obj2 = shallowCopy(obj);
const obj3 = deepCopy(obj);
obj.child.name = "xx2";
console.log(obj2); //name被修改成xx2
console.log(obj3); //没有被修改
思考需要解决的问题:循环引用
let a = Symbol("juejin");
const obj = {
name: "dzz",
child: {
name: "xxx"
},
tun: undefined,
tnull: null,
a: 1,
};
//循环引用
obj.target = obj;
const obj2 = shallowCopy(obj);
const obj3 = deepCopy(obj);
obj.child.name = "xx2";
console.log(obj2); //name被修改成xx2
console.log(obj3); //没有被修改
控制台:报错Maximum call stack size exceeded
我们可以将对象的拷贝对象保存到哈希表中,如果当前对象已被拷贝过,那么直接返回哈希表已被拷贝过的
//这里使用WeakMap而不是Map是想让它随时得以回收
function deepCopy(target, myMap = new WeakMap()) {
//1. 保证传入的target为引用类型
if (typeof target !== "object" || target === null) {
return target;
}
//6. 判断当前对象是否被拷贝过
if(myMap.get(target)) {
return myMap.get(target);
}
//2. 判断传入的target是对象还是数组,在创建新的copyTarget
const copyTarget = Array.isArray(target) ? [] : {};
//3. 将它的拷贝的对象保存到myMap,也就是哈希表
myMap.set(target, copyTarget);
for (let item in target) {
//4. 是自身的而不是原型链上的
if (target.hasOwnProperty(item)) {
//5.进行下一次递归,不是引用类型会直接返回
copyTarget[item] = deepCopy(target[item], myMap);
}
}
return copyTarget;
}
//---------------------------测试
const obj = {
name: "dzz",
child: {
name: "xxx"
},
tun: undefined,
tnull: null,
};
//循环引用
obj.target = obj;
const obj3 = deepCopy(obj);
obj.child.name = "xx2";
obj.target.name = "xx3";
console.log(obj3);
WeakMap与Map的区别:
- WeakMap对象是一组键/值对的集合,其中的键是弱引用的。其键必须是对象,而值可以是任意的。Map任何值都可以作为一个键或一个值
- WeakMap持有的每个键对象的“弱引用”,这使得在没有其他引用存在时垃圾回收能正确进行。它用于映射的 key 只有在其没有被回收时才是有效的,即WeakMap不能被遍历,
继续思考:将对象里面的Symbol值和正则表达式也拷贝下来
简单实现版:
function deepCopy(target, myMap = new WeakMap()) {
//1. 保证传入的target为引用类型
if (typeof target !== "object" || target === null) {
return target;
}
//6. 判断当前对象是否被拷贝过
if(myMap.get(target)) {
return myMap.get(target);
}
//7. 判断是否Date或者RegExp,这里面还有很多判断没有写出比如String,Number等
if (target instanceof Date) return new Date(target);
if (target instanceof RegExp) return new RegExp(target);
//2. 判断传入的target是对象还是数组,在创建新的copyTarget
const copyTarget = Array.isArray(target) ? [] : {};
//3. 将它的拷贝的对象保存到myMap,也就是哈希表
myMap.set(target, copyTarget);
for (let item in target) {
//4. 是自身的而不是原型链上的
if (target.hasOwnProperty(item)) {
//5.进行下一次递归,不是引用类型会直接返回
copyTarget[item] = deepCopy(target[item], myMap);
}
}
return copyTarget;
}
测试:
const obj = {
name: "dzz",
child: {
name: "xxx"
},
tun: undefined,
tnull: null,
a: Symbol("a"),
b: Symbol.for("b"),
fn: function () { },
tDate: new Date(),
treg: /\d/,
};
console.log(JSON.parse(JSON.stringify(obj)));
const obj2 = deepCopy(obj);
console.log(obj2);
结语
深拷贝和浅拷贝还有很多需要学习的地方,共勉~!