JavaScript对象的复制

JavaScript对象的复制

假定一个对象为如下

var obj1 = {
  a : 1,
  b : 'abc',
  c : true,
  d : [1,2,4],
  e : undefined,
  f : null,
  g : NaN,
  h : function(){},
  get i(){
      return this.a;
  },
  set i(value){
    this.a = value;
  },
  j : {}
};
Object.defineProperty( obj1, 'd', {
  writable : false,
  configurable : false,
  enumerable   : false,    
} );

整体思路

针对一个对象复制,思路即为遍历这个对象,然后将这个对象的属性和值拷贝给返回的对象上。

版本1 浅复制

因为js中对象的赋值是引用关系,所以如果我们之间以下面这种方式遍历复制一个对象的话


function shallowCopy( obj ){
const copyObj = {};
for( let key in obj ){
copyObj[key] = obj[key];
}
return copyObj
}

我们能够发现,这种复制是一种浅复制,两个对象之前是一种引用关系,改变其中一个,则会影响另一个对象。

深度复制

版本2.0 JSON方法复制对象

若一个对象是一个浅对象,即其值只含有JSON所包含的数据类型。有一种hack的写法。如下


function jsonCopy( obj ){
return JSON.parse( JSON.stringify( obj ) );
}

但是这种情况有不足的地方,首要的一条便是他会省略值为undefined和函数的属性,并且将值为NaN的属性的值转换为null.

版本2.1 普通的深度复制


function deepClone1( copySource ){
//使复制的对象copyObj和被复制对象有同样的原型
const copyObj = Object.create( copySource.constructor.prototype );
//遍历
function travel( dest, source ){
let keyType = null, value = null;
for( let key in source ){ //可枚举属性
if( source.hasOwnProperty(key) ){ //可枚举的自有属性
value = source[key];
//属性值的类型
keyType = Object.prototype.toString.call( value ).slice( 8, -1 );
if( keyType === ‘Array’ ){
dest[key] = [];
}
else if( keyType === ‘Object’ ){
dest[key] = Object.create( value.constructor.prototype );
}
else{
dest[key] = value;
continue;
}
//递归调用
travel( dest[key], source[key] )
}
}
}
travel( copyObj, copySource );
return copyObj;
}

这种方法看着比第一个好像要好点,但是写的不够优雅用到了(for…in)和对象的hasOwnProperty()方法

版本2.2 稍微优雅一点的深度复制


function deepClone2( copySource ){
const copyObj = Object.create( copySource.constructor.prototype );
function travel( dest, source ){
let keyType = null, value = null;
//Object.keys()返回的是一个包含对象自有可枚举属性的数组
Object.keys( source ).forEach( (key)=>{
value = source[key];
keyType = Object.prototype.toString.call( value ).slice( 8, -1 );
if( keyType === ‘Array’ ) dest[key] = [];
else if( keyType === ‘Object’ ) dest[key] = Object.create( value.constructor.prototype );
else return dest[key] = value;
travel( dest[key], source[key] );
} );
}
travel( copyObj, copySource );
return copyObj;
}

这种方法看着也好看了一点,不过好的也有限。我们可以发现,它对于getter和setter函数是无能为力的,并且不能复制对象属性的属性描述符。并且我们发现它不能复制一个对象的自有不可枚举的属性。复制后的对象如下

版本2.3 更加健壮的深度复制


function deepClone3( copySource ){
const copyObj = Object.create( copySource.constructor.prototype );
function travel( dest, source ){
let keyType = null, value = null, descript = null;
//返回对象的所有自有属性
Object.getOwnPropertyNames( source ).forEach( (key)=>{
value = source[key];
keyType = Object.prototype.toString.call( value ).slice( 8, -1 );
//获得该对象上该属性的属性描述符
descript = Object.getOwnPropertyDescriptor( source, key );
Object.defineProperty( dest, key, descript );
if( keyType === ‘Array’ )
dest[key] = [];
else if( keyType === ‘Object’ ){
dest[key] = Object.create( value.constructor.prototype );
}
else
return;
travel( dest[key], source[key] );
} );
}
travel( copyObj, copySource );
return copyObj;
}

这个时候我们发现对于一个js中的一个对象,我们的这种方法好像完全没有问题了。但是……..

如果一个对象是一个环,也就是循环引用了的话,即对于对象obj1增加一条语句
obj1.j.call = obj1.j;

看结果


可以看出爆栈了…………….

so,我们应该针对循环引用的对象做一些对策。

我们知道在ES6中增加了Set和Map这两种数据类型,在ES6之前,我们不能将一个对象作为对象的属性key,对象只能作为value在对象中。但是利用Set和Map我们可以将对象作为key。对于对象循环引用的问题,我们可以用一个Set保存遍历的对象的记录,如果一个对象已经被遍历过,我们就用Set记录。如果一个对象已经被遍历过,说明发生了循环引用,这时候我们就不需要继续递归的深度遍历了。

版本2.4 支持对象循环引用的深度复制


function deepClone4( copySource ){
const copyObj = Object.create( copySource.constructor.prototype );
const visited = new WeakSet();
visited.add( copySource );
function travel( dest, source ){
let keyType = null, value = null, descript = null;
Object.getOwnPropertyNames( source ).forEach( (key)=>{
value = source[key];
keyType = Object.prototype.toString.call( value ).slice( 8, -1 );
descript = Object.getOwnPropertyDescriptor( source, key );
Object.defineProperty( dest, key, descript );
if( keyType === ‘Array’ ) dest[key] = [];
else if( keyType === ‘Object’ ){
if( visited.has(value) ){
return;
}
dest[key] = Object.create( value.constructor.prototype );
visited.add(value);
}
else return;
travel( dest[key], source[key] );
} );
}
travel( copyObj, copySource );
return copyObj;
}

这回我们发现我们写的最终的版本的JS对象深度复制解决了对象的深度遍历,对象属性的属性描述符,针对getter和setter函数也没有问题,同时也支持对象循环引用。OK,暂时就到这个版本了…….

-->