五分钟尝玩之 js深浅拷贝
前言
对于很多小伙伴来说,在实际开发业务中,会遇到各种各样的数据结构以及不同数据之间的转化,拷贝等,如果平时没有留意到,遇到错误不一定能很快的找到原因,从而会延误我们的开发周期。
我之前也是对类似这种的没有一个具体完整的知识体系,遇到问题就再去查资料,很零散,终于这次下定决心一定得把深浅拷贝搞明白,希望这篇文章对大家有帮助。
什么是浅拷贝,什么是深拷贝
js的堆和栈
我们都知道,在js中,数据存储的空间分 “栈存储(stack)”和 “堆存储(heap)”,也就是,基本数据类型存储在 “栈”中,引用类型则存储在“堆”中。
栈(stack)会自动分配内存空间,会自动释放。堆(heap)动态分配的内存,大小不定也不会自动释放。
对于“堆存储(heap)”来说:从一个向另一个变量复制引用类型的值,复制的其实是指针,因此两个变量最终指向同一个对象。即复制的是栈中的地址而不是堆中的对象。
从一个变量复向另一个变量复制基本类型的值,会创建这个值的副本。
浅拷贝和深拷贝的概念
浅拷贝:对象a只是复制指向对象b的指针,而不复制对象本身。新旧对象它们还是共享同一内存。
深拷贝:如果是对象n深拷贝对象m的话,那么它们不再引用同一个存放在栈中的内存地址了,也就是它们老死不相往来了,互不影响。
浅拷贝的实现
1.赋值操作
js中最基础也是最简单的浅拷贝方式
let a = { age: 18, sex: "男" };
let b = a; console.log(b);//{ age: 18, sex: "男" }
a.age = 20;
console.log("a:", a, "b:", b); //a: {age: 20, sex: "男"} b: {age: 20, sex: "男"}
这种情况,改变b的值,会直接影响到a对象的值,反之也一样
2.Object.assign(target, ...sources)
Object.assign() 方法用于将所有可枚举属性的值从一个或多个源对象分配到目标对象。它将返回目标对象。
参数: target 目标对象 sources 源对象
返回值:目标对象target
const target = {};
const source = { a: 4, b: 5,c:{d:6}};
const returnedTarget = Object.assign(target, source);
console.log(target);//{ a: 4, b: 5,c:{d:6}}
console.log(returnedTarget );//{ a: 4, b: 5,c:{d:6}}
const target = {};
const source = { a: 4, b: 5, c: { d: 6 } };
const returnedTarget = Object.assign(target, source);
console.log(returnedTarget); //{ a: 4, b: 5,c:{d:20}}
source.c.d = 20; console.log(source);//{ a: 4, b: 5,c:{d:20}}
console.log(target);//{ a: 4, b: 5,c:{d:20}}
但是使用 object.assign 方法有几点需要注意:
- 可以处理一层的拷贝
- 它不会拷贝对象的继承属性;
- 它不会拷贝对象的不可枚举的属性;
- 可以拷贝 Symbol 类型的属性;
- Object.assign 不会在那些source对象值为 null 或 undefined 的时候抛出错误。
3.扩展运算符方式
利用 JS 的扩展运算符,在构造对象的同时完成浅拷贝的功能
/* 对象的拷贝 */
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 arr = [1, 2, 3];
let newArr = [...arr]; //跟arr.slice()是一样的效果
扩展运算符 和 object.assign 有同样的缺陷,也就是实现的浅拷贝的功能差不多,但是如果属性都是基本类型的值,使用扩展运算符进行浅拷贝会更加方便
4.concat 拷贝数组
concat() 方法用于合并两个或多个数组。此方法不会更改现有数组,而是返回一个新数组。
const array1 = ['a', 'b', 'c'];
const array2 = ['d', 'e', 'f'];
const array3 = array1.concat(array2);
console.log(array3);
const array1 = ["a", "b", "c", { g: 10 }];
const array2 = ["d", "e", "f"]; const array3 = array1.concat(array2);
console.log(array3);// ["a", "b", "c", { g: 20 }] array1[3].g = 20;
console.log(array1);//["a", "b", "c", { g: 20 }]
console.log(array2);//["d", "e", "f"]
console.log(array3);//["a", "b", "c", { g: 20 }]
concat 只能用于数组的浅拷贝,使用场景比较局限,并且也缺陷也跟object.assign一样
5.slice 拷贝数组
slice 方法也比较有局限性,因为它仅仅针对数组类型。slice 方法会返回一个新的数组对象,这一对象由该方法的前两个参数来决定原数组截取的开始和结束时间,是不会影响和改变原始数组的。
var arr1 = ["1","2","3"];
var arr2 = arr1.slice(0);
arr2[1] = "9";
console.log("arr1 :" + arr1 );console.log("arr2:" + arr2 );
6.手写一个浅拷贝
function shallowClone(target) {
if (typeof target === "object" && target !== null) {
const cloneTarget = Array.isArray(target) ? [] : {};
for (const key in target) {
if (target.hasOwnProperty(key)) {
cloneTarget[key] = target[key];
}
}
return cloneTarget;
} else {
return target;
}
}
检测:
let m = { a: 10, b: 20, c: { d: 30 } };
let n = shallowClone(m);
console.log(n);
深拷贝的实现
1.利用JSON对象的parse和stringfy
JSON.stringfy() 是目前开发过程中最简单的深拷贝方法,其实就是把一个对象序列化成为 JSON 的字符串,并将对象里面的内容转换成字符串,最后再用 JSON.parse() 的方法将JSON 字符串生成一个新的对象
let obj1 = { a:1, b:[1,2,3] }
let str = JSON.stringify(obj1);
let obj2 = JSON.parse(str);
console.log(obj2); //{a:1,b:[1,2,3]}
obj1.a = 2;
obj1.b.push(4);
console.log(obj1); //{a:2,b:[1,2,3,4]}
console.log(obj2); //{a:1,b:[1,2,3]}
缺点1:
拷贝的对象的值中如果有函数、undefined、symbol 这几种类型,经过 JSON.stringify 序列化之后的字符串中这个键值对会消失;
对象中含有 NaN、Infinity 以及 -Infinity,JSON 序列化的结果会变成 null
const test = {
name: "a", undefined, NaN,
date: function hehe() {
console.log("fff");
},
};
const copyed = JSON.parse(JSON.stringify(test));
test.name = "test"; console.log("test:", test);
console.log("copyed:", copyed);
缺点2:
如果对象里有RegExp、Error对象,则序列化的结果将只得到空对象;
const test = {
name: "a",
Error,
date: new RegExp("\w+"),
};
const copyed = JSON.parse(JSON.stringify(test));
test.name = "test"; console.log("test:", test);
console.log("copyed:", copyed);
缺点3:
如果对象里面有Date 引用类型,则JSON.stringify后再JSON.parse的结果,时间将只是字符串的形式,而不是时间对象;
var test = {
date: [new Date(1536627600000), new Date(1540047600000)],
};
let copyed = JSON.parse(JSON.stringify(test));
console.log("test:", test);
console.log("copyed:", copyed);
2.手写递归来实现
function deepClone(obj) {
let cloneObj = {};
for (let key in obj) { //遍历
if (typeof obj[key] === "object") {
cloneObj[key] = deepClone(obj[key]); //是对象就再次调用该函数递归
} else {
cloneObj[key] = obj[key]; //基本类型的话直接复制值
}
}
return cloneObj
}
let obj2 = deepClone(obj1);
obj1.a.b = 2;console.log(obj2); // {a:{b:1}}
缺点:
-
这种方法只是针对普通的引用类型的值做递归复制,而对于 Array、Date、RegExp、Error、Function 这样的引用类型并不能正确地拷贝;
-
对象的属性里面成环,即循环引用没有解决。
2. 改进版:手写递归来实现
// checktype函数来 检查对象类型
function checktype(obj) {
return Object.prototype.toString.call(obj).slice(8, -1).toLocaleLowerCase()
}
function deepCopy(target, hash = new WeakMap()) {
if (target.constructor === Date) return new Date(target); // 日期对象直接返回一个新的日期对象 if (target.constructor === RegExp) return new RegExp(target); //正则对象直接返回一个新的正则对象 //hash 作为一个检查器,避免对象深拷贝中出现环引用,导致爆栈。
let type = checktype(target)
let result = null
if (type == 'object') {
result = {}
} else if (type == 'array') {
result = []
} else {
return target
}
if (hash.has(target)) {
//检查是有存在相同的对象在之前拷贝过,有则返回之前拷贝后存于hash中的对象
return hash.get(target)
}
//备份存在hash中,result目前是空对象、数组。后面会对属性进行追加,这里存的值是对象的栈
hash.set(target, result);
for (let i in target) {
if (checktype(target[i]) == "object" || checktype(target[i]) == "array") {
result[i] = deepCopy(target[i], hash); //属性值是对象,进行递归深拷贝
} else {
result[i] = target[i]; //其他类型直接拷贝
}
}
return result
}