js中对象的拷贝非常重要,如果理解不深入必将写出致命的bug,本文将总结之前项目开中中用到的所有拷贝方法进行总结。
1 对象拷贝的本质原因及浅拷贝深拷贝概念
由于js创建对象进内存的机制,原始数据类型 Undefined,Null,Boolean,Number、String 五类是直接进栈保存的,因此对这五种类型的数据赋值操作是内存里直接复制结果值保存,而object类型的数据都是引用数据类型,栈内保存的是地址引用,实际值存储在堆中,而对引用类型的赋值操作实际上是将栈内的引用地址拷贝赋值,因此如果具有相同引用地址的变量修改了对象的某个属性值,则会引起其他相同引用地址对象的改变,造成和预期背离的结果。
举例说明:
//定义一个员工个人对象
let obj = {
name:"cc",
age:30,
job:{type:"porgrammer",com:"ali"},
cars:["passat","bmw"]
}
//该函数只获取员工的车辆基本信息姓名和车辆
let getPerInf = function(obj){
var newObj = obj;
if(newObj){
if(newObj.age){
delete newObj.age;
}
if(ewObj.job){
delete newObj.job;
}
}
return newObj;
}
//打印函数处理后的newobj结果和原始obj结果
console.log(getPerInf(obj));
console.log(obj);
结果:
通过截图我们发现经过函数getperInf处理删除job和age属性之后对全局定义的obj对象也同样起了作用,加入此时别的函数需要完整的obj怎么办?
所以以上就必然催生对象的拷贝问题了。
1.1浅拷贝
浅拷贝的概念是相对于深拷贝而言的,仅仅将原对象的的第一层属性进行了赋值神并生成新对象,其本质是对象的结构决定的,比如上例中我们定义的obj对象。
let obj = {
name:"cc",
age:30,
job:{type:"porgrammer",com:"ali"},
cars:["passat","bmw"]
}
obj中即有原属数据类型属性name和age同时还有引用数据类型job和cars如果用浅拷贝实现上面需求如下:
//定义一个员工个人对象
let obj = {
name:"cc",
age:30,
job:{type:"porgrammer",com:"ali"},
cars:["passat","bmw"]
}
//该函数只获取员工的车辆基本信息姓名和车辆
let getPerInf = function(obj){
var newObj = shshallowClone(obj);
if(newObj){
if(newObj.age){
delete newObj.age;
}
if(newObj.job){
delete newObj.job;
}
}
return newObj;
}
//浅拷贝es实现
let shshallowClone = function(obj){
let newObj =new Object();
if(obj){
for(key in obj){
newObj[key] = obj[key];
}
}
return newObj;
}
console.log(obj);
console.log(getPerInf(obj));
console.log(obj);
结果:
通过浅拷贝我们发现getPerInf方法达到了我们想要的数据结果同时全局的obj对象没有受到影响。
但是这样依然会有问题,假如我们在getPerInf方法中修改job对象中com值为“360”,再在全局中修改obj对象job对象中tyep值为“managet”,我们期待的结果肯定是getPerInf方法中返回的newObj对象不会受全局obj修改job.type的影响,同时全局obj对象也不受newObj中job.com
修改的影响:
//定义一个员工个人对象
let obj = {
name:"cc",
age:30,
job:{type:"porgrammer",com:"ali"},
cars:["passat","bmw"]
}
//该函数只获取员工的车辆基本信息姓名和车辆
let getPerInf = function(obj){
var newObj = shshallowClone(obj);
if(newObj){
if(newObj.age){
delete newObj.age;
}
if(newObj.job){
// delete newObj.job;
newObj.job.com = '360'
}
}
return newObj;
}
//浅拷贝es实现
let shshallowClone = function(obj){
let newObj =new Object();
if(obj){
for(key in obj){
newObj[key] = obj[key];
}
}
return newObj;
}
console.log(obj);
console.log(getPerInf(obj));
if(obj.job){
obj.job.type = 'manager';
}
console.log(obj);
结果:
很不幸两者竟然是相同的,这就说明obj.job对象的引用与obj.job对象的引用是完全相同的。
要解决上面的问题我们就必须进行深拷贝。
1.2深拷贝
深拷贝顾名思义就是将原对象的所有属性包含子属性为引用属性的对象进行复制给新对象。目的就是彻底阻断与原来对象的地址引用,实现互不影响。
//定义一个员工个人对象
let obj = {
name:"cc",
age:30,
job:{type:"porgrammer",com:"ali"},
cars:["passat","bmw"]
}
//该函数只获取员工的车辆基本信息姓名和车辆
let getPerInf = function(obj){
// var newObj = shshallowClone(obj);
let newObj = deepClone(obj)
if(newObj){
if(newObj.age){
delete newObj.age;
}
if(newObj.job){
// delete newObj.job;
newObj.job.com = '360'
}
}
return newObj;
}
//深拷贝
let deepClone = function (obj){
let newObj = (obj instanceof Array)? new Array() : new Object();
if(obj){
for(let key in obj){
if(typeof(obj[key]) === "object"){
newObj[key] = deepClone(obj[key]);
}else{
newObj[key] = obj[key];
}
}
}
return newObj;
}
console.log(obj);
console.log(getPerInf(obj));
if(obj.job){
obj.job.type = 'manager';
}
console.log(obj);
结果:
这样我们就彻底实现了对象的所有属性拷贝,obj 与 newObj 两者之间任何操作都会会不影响。
以上我们已经将对象拷贝的原因本质及浅拷贝深拷贝的区别已经详细的予以说明,接下来我们罗列一下一些常用的经典对象深浅拷贝实现方法。
2 es5原生实现对象浅深拷贝
2.1浅拷贝
//浅拷贝es实现
let shshallowClone = function(obj){
let newObj =new Object();
if(obj){
for(key in obj){
newObj[key] = obj[key];
}
}
return newObj;
}
2.2深拷贝
let deepClone = function (obj){
let newObj = (obj instanceof Array)? new Array() : new Object();
if(obj){
for(let key in obj){
if(typeof(obj[key]) === "object"){
newObj[key] = deepClone(obj[key]);
}else{
newObj[key] = obj[key];
}
}
}
return newObj;
}
2.3 jsonPase 实现
JSON.parse(JSON.stringify(xxx));
此种方法严重不推荐,会有一下问题:
1、如果obj里面有时间对象,则JSON.stringify后再JSON.parse的结果,时间将只是字符串的形式。而不是时间对象;
2、如果obj里有RegExp、Error对象,则序列化的结果将只得到空对象;
3、如果obj里有函数,undefined,则序列化的结果会把函数或 undefined丢失;
4、如果obj里有NaN、Infinity和-Infinity,则序列化的结果会变成null;
5、JSON.stringify()只能序列化对象的可枚举的自有属性,例如 如果obj中的对象是有构造函数生成的, 则使用JSON.parse(JSON.stringify(obj))深拷贝后,会丢弃对象的constructor;
2.4 es5的完美实现
我们要实现深拷贝必须考虑到js的所有数据类型处理Boolean,Number,String,Function,Array,Date,RegExp,Undefined,Null,Object
而且在不考虑symbol数据类型和对象属性循环引用的情况下:
//定义一个员工个人对象
let obj = {
name:"cc",
age:30,
job:{type:"porgrammer",com:"ali"},
cars:["passat","bmw"],
working:function(str){
console.log(`我是${this.name},我正在${str}`)
},
da:new Date(),
reg: new RegExp(),
xx:undefined
}
//该函数只获取员工的车辆基本信息姓名和车辆
let getPerInf = function(obj){
// var newObj = shshallowClone(obj);
// let newObj = deepClone(obj);
let newObj = deepCuClone(obj);
if(newObj){
if(newObj.age){
delete newObj.age;
}
if(newObj.job){
// delete newObj.job;
newObj.job.com = '360'
}
}
return newObj;
}
//不考虑循环引用和symbol情况
function deepCuClone(data) {
const type = this.judgeType(data);
let obj;
if (type === 'array') {
obj = [];
} else if (type === 'object') {
obj = {};
} else {
// 不再具有下一层次
return data;
}
if (type === 'array') {
// eslint-disable-next-line
for (let i = 0, len = data.length; i < len; i++) {
obj.push(this.deepCuClone(data[i]));
}
} else if (type === 'object') {
// 对原型上的方法也拷贝了....
// eslint-disable-next-line
for (const key in data) {
obj[key] = this.deepCuClone(data[key]);
}
}
return obj;
}
function judgeType(obj) {
// tostring会返回对应不同的标签的构造函数
const toString = Object.prototype.toString;
const map = {
'[object Boolean]': 'boolean',
'[object Number]': 'number',
'[object String]': 'string',
'[object Function]': 'function',
'[object Array]': 'array',
'[object Date]': 'date',
'[object RegExp]': 'regExp',
'[object Undefined]': 'undefined',
'[object Null]': 'null',
'[object Object]': 'object',
};
if (obj instanceof Element) {
return 'element';
}
return map[toString.call(obj)];
}
console.log(obj);
console.log(JSON.parse(JSON.stringify(obj)));
console.log(getPerInf(obj));
console.log(obj);
obj.working("吃饭");
结果:
3 es6 实现深浅拷贝
3.1解构赋值实现
let obj = {
name:"cc",
age:30,
job:{type:"porgrammer",com:"ali"},
cars:["passat","bmw"],
working:function(str){
console.log(`我是${this.name},我正在${str}`)
},
da:new Date(),
reg: new RegExp(),
xx:undefined
};
let obj0 = {...obj};
console.log(obj);
obj0.job.com = "xiaomi";
console.log(obj0);
结果:
显然通过结构赋值实现的拷贝仅仅是浅拷贝。
3.2 asign方法实现
obj2=Object.assign({},obj);
console.log(obj);
obj2.job.com = "tengxun";
console.log(obj2);
显然asign依然是浅拷贝。
对于es的深拷贝其实就是和上面es5的方法差不多。
对于深拷贝我们重点要关注的是es6 synbol类型及循环引用的深拷贝问题。
4循环引用及symbol类型数据的深拷贝
我们直到以上所有深拷贝在处理对象循环引用的问题时都无法拷贝,而且加入对象的属性key是symbol类型的也无法拷贝。
终级解决方案
// 终极解决方案,满足循环引用和symbol
/**
* 判断是否是基本数据类型
* @param value
*/
function isPrimitive(value){
return (typeof value === 'string' ||
typeof value === 'number' ||
typeof value === 'symbol' ||
typeof value === 'boolean')
}
/**
* 判断是否是一个js对象
* @param value
*/
function isObject(value){
return Object.prototype.toString.call(value) === "[object Object]"
}
/**
* 深拷贝一个值
* @param value
*/
function cloneEnDeep(value){
// 记录被拷贝的值,避免循环引用的出现
let memo = {};
function baseClone(value){
let res;
// 如果是基本数据类型,则直接返回
if(isPrimitive(value)){
return value;
// 如果是引用数据类型,我们浅拷贝一个新值来代替原来的值
}else if(Array.isArray(value)){
res = [...value];
}else if(isObject(value)){
res = {...value};
}
// 检测我们浅拷贝的这个对象的属性值有没有是引用数据类型。如果是,则递归拷贝
//同时使用Reflect可以检测到Symbol类型的属性
Reflect.ownKeys(res).forEach(key=>{
if(typeof res[key] === "object" && res[key]!== null){
//此处我们用memo来记录已经被拷贝过的引用地址。以此来解决循环引用的问题
if(memo[res[key]]){
res[key] = memo[res[key]];
}else{
memo[res[key]] = res[key];
res[key] = baseClone(res[key])
}
}
})
return res;
}
return baseClone(value)
}
//======================测试====================
//定义一个员工个人对象
let objP = {
name:"cc",
age:30,
job:{type:"porgrammer",com:"ali"},
cars:["passat","bmw"],
working:function(str){
console.log(`我是${this.name},我正在${str}`)
},
da:new Date(),
reg: new RegExp(),
xx:undefined
}
objP.job = objP;//循环引用
console.log(obj);
console.log(getPerInf(obj));
console.log(obj);
obj.working("吃饭");
结果:
总结
通过以上深浅拷贝的详细讲解,我们重点并不是要得到一个完美的深浅拷贝方法而时通过分析来提高我们对js语言数据结构的深层次认识,这样才能便于去阅读牛逼库的源码,同时减少自己codding的bug。
经测试上面方法满足所有类型的拷贝,但是项目中依然建议使用第三方库来实现对象的拷贝:
- lodash的cloneDeep
- jq的extends方法