JS对象拷贝的原理与实现

580 阅读3分钟

##前言 对象的拷贝是面试与实际开发中经常问到与应用到的知识。javascript提供的的数据操作api基本都是对象的浅拷贝。而深拷贝方法则比较少。由于经常使用到,于是把深拷贝与浅拷贝常见的方法记录下来。

1.什么是深拷贝和浅拷贝

浅拷贝和深拷贝都是对于JS中的引用类型而言的,浅拷贝就只是复制对象的引用(堆和栈的关系,简单类型UndefinedNullBooleanNumberString是存入堆,直接引用,object`` array 则是存入桟中,只用一个指针来引用值),如果拷贝后的对象发生变化,原对象也会发生变化。只有深拷贝才是真正地对对象的拷贝。

2.浅拷贝的实现

1.直接赋值

浅拷贝对象,如果对象的属性是个引用类型,那么值复制引用,而未复制真正的值。

const originArray = [1,2,3,4,5];
const originObj = {a:'a',b:'b',c:[1,2,3],d:{dd:'dd'}};

const cloneArray = originArray;
const cloneObj = originObj;

console.log(cloneArray); // [1,2,3,4,5]
console.log(originObj); // {a:'a',b:'b',c:Array[3],d:{dd:'dd'}}
 
cloneArray.push(6);
cloneObj.a = {aa:'aa'};
 
console.log(cloneArray); // [1,2,3,4,5,6]
console.log(originArray); // [1,2,3,4,5,6]
 
console.log(cloneObj); // {a:{aa:'aa'},b:'b',c:Array[3],d:{dd:'dd'}}
console.log(originArray); // {a:{aa:'aa'},b:'b',c:Array[3],d:{dd:'dd'}}

2.使用ES6+语法实现

const originArray = [1,2,3,4,5];
const originObj = {a:'a',b:'b',c:[1,2,3],d:{dd:'dd'}};

const cloneArray1=Object.assign([], originArray)   //[1,2,3,4,5]
const cloneObj1=Object.assign({}, originObj)    //{a:'a',b:'b',c:[1,2,3],d:{dd:'dd'}}

const cloneArray2=[...originArray]    ////[1,2,3,4,5]
const cloneObj2={...originObj}    //{a:'a',b:'b',c:[1,2,3],d:{dd:'dd'}}

3.深拷贝的实现

深拷贝常用的方法主要有两种:

  • 利用JSON对象的parsestringify
  • 利用递归来实现每一层都重新创建对象并赋值

1.利用JSON来实现

 JSON.stringify 是将一个 JavaScript 值转成一个 JSON 字符串。 JSON.parse 是将一个 JSON 字符串转成一个 JavaScript 值或对象。

const originArray = [1,2,3,4,5];
const cloneArray = JSON.parse(JSON.stringify(originArray));
console.log(cloneArray === originArray); // false

const originObj = {a:'a',b:'b',c:[1,2,3],d:{dd:'dd'}};
const cloneObj = JSON.parse(JSON.stringify(originObj));
console.log(cloneObj === originObj); // false

cloneObj.a = 'aa';
cloneObj.c = [1,1,1];
cloneObj.d.dd = 'doubled';

console.log(cloneObj); // {a:'aa',b:'b',c:[1,1,1],d:{dd:'doubled'}};
console.log(originObj); // {a:'a',b:'b',c:[1,2,3],d:{dd:'dd'}};

原来深克隆就这么简单?!! 其实不然,利用这种方法进行深克隆,存在着很多的隐患。

  • 函数无法序列化,属性值为函数的在转化后丢失。
  • 日期对象转换到 JSON 对象之后无法反解析为 原对象类型,解析后的值仍然是 JSON 格式的字符串。
  • 正则 RegExp 对象 RegExp 对象序列化后为一个普通的 javascript 对象,同样不符合预期。
  • undefined 序列化之后直接被过滤掉,丢失拷贝的属性。
  • NaN 序列化之后为 null,同样不符合预期结果。
const originObj = {
 name:'axuebin',
 sayHello:function(){
   console.log('Hello World');
	 }
   }
console.log(originObj); // {name: "axuebin", sayHello: ƒ}
const cloneObj = JSON.parse(JSON.stringify(originObj));
console.log(cloneObj); // {name: "axuebin"}

因此,如果对象里面如果存在上面三种属性,就不可以使用这种方法进行深拷贝了。

2.利用递归实现

递归拷贝顾名思义就是对每一层数据都实现一次创建对象->对象赋值的操作。

---直接上代码---
function deepClone(source){
    const target=source.constructor===Array?[]:{}   //判断拷贝的对象是数组还是对象
    for(let keys in source){
      if(source.hasOwnProperty(keys)){
        if(source[keys]&&typeof source[keys]==='object'){  // 如果值是对象,就递归一下
          targetObj[keys] = source[keys].constructor === Array ? [] : {};
          targetObj[keys] = deepClone(source[keys]);
        }else{ //如果不是,就直接赋值
          targetObj[keys]=source[keys]
        }
      }
    }
    return targetObj
}

函数验证

const originObj = {a:'a',b:'b',c:[1,2,3],d:{dd:'dd'}};
const cloneObj = deepClone(originObj);
console.log(cloneObj === originObj); // false

cloneObj.a = 'aa';
cloneObj.c = [1,1,1];
cloneObj.d.dd = 'doubled';

console.log(cloneObj); // {a:'aa',b:'b',c:[1,1,1],d:{dd:'doubled'}};
console.log(originObj); // {a:'a',b:'b',c:[1,2,3],d:{dd:'dd'}};

再来试试带有函数的对象:

const originObj = {
  name:'axuebin',
  sayHello:function(){
    console.log('Hello World');
  }
 }
 console.log(originObj); // {name: "axuebin", sayHello: ƒ}
 const cloneObj = deepClone(originObj);
 console.log(cloneObj); // {name: "axuebin", sayHello: ƒ}

参考资料

MDN-object.assign()

对象深拷贝