零、前言
1 浅拷贝定义
2 浅拷贝例子以及相关实现
1 深拷贝定义
2 深拷贝例子以及相关实现
一、浅拷贝定义
1 浅拷贝定义(存在于第一层且是基本类型值的,直接赋值 即传值,但 为引用类型的话 是直接 复制堆地址,即 “传址”!!!)
浅拷贝只会将对象的各个属性进行依次复制,并不会进行递归复制,也就是说只会赋值目标对象的第一层属性。
对于目标对象第一层为基本数据类型的数据,就是直接赋值,即「传值」; 而对于目标对象第一层为引用数据类型的数据,就是直接赋存于栈内存中的堆内存地址,即「传址」。
二、浅拷贝例子以及相关实现
1 最简单的一种浅拷贝 (let obj2 = obj1;) 。直接赋值!!!
2 Object.assign(第一层的是“深拷贝”,深层的是“浅拷贝”)
语法:Object.assign(target, ...sources)
let obj1 = {a:1, b: {c: 1}};
let obj2 = {};
Object.assign(obj2, obj1);
obj2.a = 'a新值'
obj1.b.c = 'c新值';
console.log(obj1, obj2);
运行结果结果如下(a不变,但是里面的 c 变了!!!)
2 扩展运算符 ... 实现的浅拷,如 let cloneObj = { ...obj }【跟 Object.assign() 类似,“浅层深复制,深层浅复制”】
语法:let cloneObj = { ...obj };
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}}
三、深拷贝定义(核心:递归拷贝 目标对象的所有属性 !!!)
1 深拷贝不同于浅拷贝,它不只拷贝目标对象的第一层属性,而是递归拷贝目标对象的所有属性。
一般来说,在JavaScript中考虑复合类型的深层复制的时候,往往就是指对于 Date
、Object
与 Array
这三个复合类型的处理。我们能想到的最常用的方法就是先创建一个空的新对象,然后递归遍历旧对象,直到发现基础类型的子节点才赋予到新对象对应的位置。
不过这种方法会存在一个问题,就是 JavaScript 中存在着神奇的原型机制,并且这个原型会在遍历的时候出现,然后需要考虑原型应不应该被赋予给新对象。那么在遍历的过程中,我们可以考虑使用 hasOwnProperty
方法来判断是否过滤掉那些继承自原型链上的属性。
四、深拷贝例子以及相关实现
1 一个最简单的深拷贝(核心:手动“赋值完所有的属性”)
let obj1 = {
a: {
b: 1
},
c: 1
};
let obj2 = {};
obj2.a = {}
obj2.c = obj1.c
obj2.a.b = obj1.a.b;
console.log(obj1); //{a:{b:1},c:1};
console.log(obj2); //{a:{b:1},c:1};
obj1.a.b = 2;
console.log(obj1); //{a:{b:2},c:1};
console.log(obj2); //{a:{b:1},c:1};
2 JSON.stringify | parse【核心:JSON.stringify 序列化 与 反序列化 parse !!】
JSON.stringify()是目前前端开发过程中最常用的深拷贝方式,原理是把一个对象序列化成为一个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]}
注意:(JSON去深拷贝需要注意的点。很多属性值可能变成了 null )
- 拷贝的对象的值中如果有函数,undefined,symbol则经过JSON.stringify()序列化后的JSON字符串中这个键值对会消失
- 无法拷贝不可枚举的属性,无法拷贝对象的原型链
- 拷贝Date引用类型会变成字符串
- 拷贝RegExp引用类型会变成空对象
- 对象中含有NaN、Infinity和-Infinity,则序列化的结果会变成null
- 无法拷贝对象的循环应用(即obj[key] = obj)
3 递归的深拷贝自我实现(手撕)
function deepClone(obj) {
let new_obj = {};
for(let key in obj){
if(obj[key] instanceof Object){ // 当前遍历的属性值 是对象时
new_obj[key] = deepClone(obj[key]);
}else { // 当前遍历的属性值 是基本类型值时
new_obj[key] = obj[key];
}
}
return new_obj;
}
let obj1 = {
a:{
b:1
}
};
let obj2 = deepClone(obj1);
obj1.a.b = 2;
console.log(obj2); //{a:{b:1}}
注意:(这个仍然存在较多的问题!!!)
- 首先这个deepClone函数并不能复制不可枚举的属性以及Symbol类型
- 这里只是针对Object引用类型的值做的循环迭代,而对于Array,Date,RegExp,Error,Function引用类型无法正确拷贝
- 对象成环,即循环引用 (例如:obj1.a = obj)
4 采用第三方库( jquery 和 lodash )
1 jquery 有提供一个$.extend可以用来做 Deep Copy。
var $ = require('jquery');
var obj1 = {
a: 1,
b: { f: { g: 1 } },
c: [1, 2, 3]
};
var obj2 = $.extend(true, {}, obj1);
console.log(obj1.b.f === obj2.b.f);
// false
2 另外一个很热门的函数库lodash,也有提供_.cloneDeep用来做 Deep Copy。
var _ = require('lodash');
var obj1 = {
a: 1,
b: { f: { g: 1 } },
c: [1, 2, 3]
};
var obj2 = _.cloneDeep(obj1);
console.log(obj1.b.f === obj2.b.f);
// false
五、总结
-
封装的deepClone方法虽然能实现对ECMAScript原生引用类型的拷贝,但是对于对象来说范围太广了,仍有很多无法准确拷贝的(比如DOM节点),但是在日常开发中一般并不需要拷贝很多特殊的引用类型,深拷贝对象使用JSON.stringify依然是最方便的方法之一(当然也需要了解JSON.stringify的缺点)
-
实现一个完整的深拷贝是非常复杂的,需要考虑到很多边界情况,这里我也只是对部分的原生的构造函数进行了深拷贝,对于特殊的引用类型有拷贝需求的话,建议还是借助第三方完整的库
-
对于深入研究深拷贝的原理有助于理解JavaScript引用类型的特点,以及遇到相关特殊的问题也能迎刃而解,对于提高JavaScript的基础还是很有帮助的~~~