淺拷貝與深拷貝(JavaScript)

一、預(yù)備知識(shí)
  • ECMAScript變量包含兩種不同數(shù)據(jù)類型的值:基本數(shù)據(jù)類型引用數(shù)據(jù)類型。
    基本數(shù)據(jù)類型:名值存儲(chǔ)在棧內(nèi)存中;
    引用數(shù)據(jù)類型:名存在棧內(nèi)存中,值存在于堆內(nèi)存中,但是棧內(nèi)存會(huì)提供一個(gè)引用的地址指向堆內(nèi)存中的值。
  • 目前基本數(shù)據(jù)類型有:Boolean、Null、Undefined、Number、String、Symbol,引用數(shù)據(jù)類型有:Object、Array、Function、RegExp、Date
  • 深拷貝與淺拷貝的概念只存在于引用數(shù)據(jù)類型


二、Js自帶的深拷貝方法
1、Array
  • slice()、concat、Array.from()、... 操作符:只能實(shí)現(xiàn)一維數(shù)組的深拷貝
var arr1 = [1, 2, [3, 4]], arr2 = arr1.slice();
console.log(arr1); //[1, 2, [3, 4]]
console.log(arr2); //[1, 2, [3, 4]]
arr2[0] = 2 
arr2[2][1] = 5; 
console.log(arr1); //[1, 2, [3, 5]]
console.log(arr2); //[2, 2, [3, 5]]
2、Object
  • Object.assign():只能實(shí)現(xiàn)一維對(duì)象的深拷貝
var obj1 = {x: 1, y: 2}, obj2 = Object.assign({}, obj1);
console.log(obj1) //{x: 1, y: 2}
console.log(obj2) //{x: 1, y: 2}
obj2.x = 2; //修改obj2.x
console.log(obj1) //{x: 1, y: 2}
console.log(obj2) //{x: 2, y: 2}
var obj1 = {
    x: 1, 
    y: {
        m: 1
    }
};
var obj2 = Object.assign({}, obj1);
console.log(obj1) //{x: 1, y: {m: 1}}
console.log(obj2) //{x: 1, y: {m: 1}}
obj2.y.m = 2; //修改obj2.y.m
console.log(obj1) //{x: 1, y: {m: 2}}
console.log(obj2) //{x: 2, y: {m: 2}}
  • JSON.parse(JSON.stringify(obj)):可實(shí)現(xiàn)多維對(duì)象的深拷貝,但會(huì)忽略undefined、任意的函數(shù)、symbol 值
var obj1 = {
    x: 1, 
    y: {
        m: 1
    },
    a:undefined,
    b:function(a,b){
      return a+b
    },
    c:Symbol("foo")
};
var obj2 = JSON.parse(JSON.stringify(obj1));
console.log(obj1) //{x: 1, y: {m: 1}, a: undefined, b: ?, c: Symbol(foo)}
console.log(obj2) //{x: 1, y: {m: 1}}
obj2.y.m = 2; //修改obj2.y.m
console.log(obj1) //{x: 1, y: {m: 1}, a: undefined, b: ?, c: Symbol(foo)}
console.log(obj2) //{x: 2, y: {m: 2}}

注:進(jìn)行JSON.stringify()序列化的過(guò)程中,undefined、任意的函數(shù)以及 symbol 值,在序列化過(guò)程中會(huì)被忽略(出現(xiàn)在非數(shù)組對(duì)象的屬性值中時(shí))或者被轉(zhuǎn)換成 null(出現(xiàn)在數(shù)組中時(shí))。

由上面可知,JS 提供的自有方法并不能徹底解決Array、Object的深拷貝問(wèn)題,因此我們應(yīng)該自己實(shí)現(xiàn)。


三、深拷貝函數(shù)簡(jiǎn)單寫法(遞歸實(shí)現(xiàn))
function deepClone(obj){
  let result = Array.isArray(obj)?[]:{};
  if(obj && typeof obj === "object"){
    for(let key in obj){
      if(obj.hasOwnProperty(key)){
        if(obj[key] && typeof obj[key] === "object"){
          result[key] = deepClone(obj[key]);
        }else{
          result[key] = obj[key];
        }
      }
    }
  }
  return result;
}

// 測(cè)試用
var obj1 = {
    x: {
        m: 1
    },
    y: undefined,
    z: function add(z1, z2) {
        return z1 + z2
    },
    a: Symbol("foo"),
    b: [1,2,3,4,5],
    c: null
};
var obj2 = deepClone(obj1);
obj2.x.m = 2;
obj2.b[0] = 2;
console.log(obj1);
console.log(obj2);

但上面的深拷貝方法遇到循環(huán)引用,會(huì)陷入一個(gè)循環(huán)的遞歸過(guò)程,從而導(dǎo)致爆棧。如:

var obj1 = {
    x: 1, 
    y: 2
};
obj1.z = obj1;
var obj2 = deepClone(obj1);

因此需要改進(jìn)。


四、深拷貝函數(shù)改進(jìn)(防止循環(huán)遞歸)

解決因循環(huán)遞歸而暴棧的問(wèn)題,只需要判斷一個(gè)對(duì)象的字段是否引用了這個(gè)對(duì)象或這個(gè)對(duì)象的任意父級(jí)即可。

function deepClone(obj, parent = null){ // 改進(jìn)(1)
  let result = Array.isArray(obj)?[]:{};
  let _parent = parent;  // 改進(jìn)(2)
  while(_parent){ // 改進(jìn)(3)
    if(_parent.originalParent === obj){
      return _parent.currentParent;
    }
    _parent = _parent.parent;
  }
  if(obj && typeof obj === "object"){
    for(let key in obj){
      if(obj.hasOwnProperty(key)){
        if(obj[key] && typeof obj[key] === "object"){
          result[key] = deepClone(obj[key],{ // 改進(jìn)(4)
            originalParent: obj,
            currentParent: result,
            parent: parent
          });
        }else{
          result[key] = obj[key];
        }
      }
    }
  }
  return result;
}

// 調(diào)試用
var obj1 = {
    x: 1, 
    y: 2
};
obj1.z = obj1;
var obj2 = deepClone(obj1);
console.log(obj1); //太長(zhǎng)了去瀏覽器試一下吧~ 
console.log(obj2); //太長(zhǎng)了去瀏覽器試一下吧~ 


五、深拷貝函數(shù)最終版(支持基本數(shù)據(jù)類型、原型鏈、RegExp、Date類型)
function deepClone(obj, parent = null){ 
  let result; // 最后的返回結(jié)果

  let _parent = parent; // 防止循環(huán)引用
  while(_parent){
    if(_parent.originalParent === obj){
      return _parent.currentParent;
    }
    _parent = _parent.parent;
  }
  
  if(obj && typeof obj === "object"){ // 返回引用數(shù)據(jù)類型(null已被判斷條件排除))
    if(obj instanceof RegExp){ // RegExp類型
      result = new RegExp(obj.source, obj.flags)
    }else if(obj instanceof Date){ // Date類型
      result = new Date(obj.getTime());
    }else{
      if(obj instanceof Array){ // Array類型
        result = []
      }else{ // Object類型,繼承原型鏈
        let proto = Object.getPrototypeOf(obj);
        result = Object.create(proto);
      }
      for(let key in obj){ // Array類型 與 Object類型 的深拷貝
        if(obj.hasOwnProperty(key)){
          if(obj[key] && typeof obj[key] === "object"){
            result[key] = deepClone(obj[key],{ 
              originalParent: obj,
              currentParent: result,
              parent: parent
            });
          }else{
            result[key] = obj[key];
          }
        }
      }
    }
  }else{ // 返回基本數(shù)據(jù)類型與Function類型,因?yàn)镕unction不需要深拷貝
    return obj
  }
  return result;
}

// 調(diào)試用
function construct(){
    this.a = 1,
    this.b = {
        x:2,
        y:3,
        z:[4,5,[6]]
    },
    this.c = [7,8,[9,10]],
    this.d = new Date(),
    this.e = /abc/ig,
    this.f = function(a,b){
        return a+b
    },
    this.g = null,
    this.h = undefined,
    this.i = "hello",
    this.j = Symbol("foo")
}
construct.prototype.str = "I'm prototype"
var obj1 = new construct()
obj1.k = obj1
obj2 = deepClone(obj1)
obj2.b.x = 999
obj2.c[0] = 666
console.log(obj1)
console.log(obj2)
console.log(obj1.str)
console.log(obj2.str)

注:Function類型的深拷貝:

  • bind():使用fn.bind()可將函數(shù)進(jìn)行深拷貝,但因?yàn)閠his指針指向問(wèn)題而不能使用;
  • eval(fn.toString()):只支持箭頭函數(shù),普通函數(shù)function fn(){}則不適用;
  • new Function(arg1,arg2,...,function_body):需將參數(shù)與函數(shù)體提取出來(lái);
    PS:一般也不需要深拷貝Function。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容