溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

javascript深拷貝、淺拷貝和循環(huán)引用的詳細(xì)講解

發(fā)布時間:2021-09-02 12:36:31 來源:億速云 閱讀:136 作者:chen 欄目:web開發(fā)

本篇內(nèi)容主要講解“javascript深拷貝、淺拷貝和循環(huán)引用的詳細(xì)講解”,感興趣的朋友不妨來看看。本文介紹的方法操作簡單快捷,實用性強(qiáng)。下面就讓小編來帶大家學(xué)習(xí)“javascript深拷貝、淺拷貝和循環(huán)引用的詳細(xì)講解”吧!

一、為什么有深拷貝和淺拷貝?

這個要從js中的數(shù)據(jù)類型說起,js中數(shù)據(jù)類型分為基本數(shù)據(jù)類型和引用數(shù)據(jù)類型。

基本類型值指的是那些保存在棧內(nèi)存中的簡單數(shù)據(jù)段,即這種值是完全保存在內(nèi)存中的一個位置。包含Number,String,Boolean,Null,Undefined ,Symbol。

引用類型值指的是那些保存在堆內(nèi)存中的對象,所以引用類型的值保存的是一個指針,這個指針指向存儲在堆中的一個對象。除了上面的 6 種基本數(shù)據(jù)類型外,剩下的就是引用類型了,統(tǒng)稱為 Object 類型。細(xì)分的話,有:Object 類型、Array 類型、Date 類型、RegExp 類型、Function 類型 等。

正因為引用類型的這種機(jī)制, 當(dāng)我們從一個變量向另一個變量復(fù)制引用類型的值時,實際上是將這個引用類型在棧內(nèi)存中的引用地址復(fù)制了一份給新的變量,其實就是一個指針。因此當(dāng)操作結(jié)束后,這兩個變量實際上指向的是同一個在堆內(nèi)存中的對象,改變其中任意一個對象,另一個對象也會跟著改變。

javascript深拷貝、淺拷貝和循環(huán)引用的詳細(xì)講解

因此深拷貝和淺拷貝只發(fā)生在引用類型中。簡單來說他們的區(qū)別在于:

1. 層次

  • 淺拷貝 只會將對象的各個屬性進(jìn)行依次復(fù)制,并不會進(jìn)行遞歸復(fù)制,也就是說只會賦值目標(biāo)對象的第一層屬性。

  • 深拷貝不同于淺拷貝,它不只拷貝目標(biāo)對象的第一層屬性,而是遞歸拷貝目標(biāo)對象的所有屬性。


2. 是否開辟新的棧

  • 淺拷貝 對于目標(biāo)對象第一層為基本數(shù)據(jù)類型的數(shù)據(jù),就是直接賦值,即「傳值」;而對于目標(biāo)對象第一層為引用數(shù)據(jù)類型的數(shù)據(jù),就是直接賦存于棧內(nèi)存中的堆內(nèi)存地址,即「傳址」,并沒有開辟新的棧,也就是復(fù)制的結(jié)果是兩個對象指向同一個地址,修改其中一個對象的屬性,則另一個對象的屬性也會改變,

  • 深拷貝 而深復(fù)制則是開辟新的棧,兩個對象對應(yīng)兩個不同的地址,修改一個對象的屬性,不會改變另一個對象的屬性。

二、淺拷貝

以下是實現(xiàn)淺拷貝的幾種實現(xiàn)方式:

1.Array.concat()

  const arr = [1,2,3,4,[5,6]];
  const copy = arr.concat(); \\ 利用concat()創(chuàng)建arr的副本
  
  \\改變基本類型值,不會改變原數(shù)組
  copy[0] = 2; 
  arr; //[1,2,3,4,[5,6]];

  \\改變數(shù)組中的引用類型值,原數(shù)組也會跟著改變
  copy[4][1] = 7;
  arr; //[1,2,3,4,[5,7]];

能實現(xiàn)類似效果的還有slice()和Array.from()等,大家可以自己嘗試一下~

2.Object.assign()

const obj1 = {x: 1, y: 2};
const obj2 = Object.assign({}, obj1);

obj2.x = 2; \\修改obj2.x,改變對象中的基本類型值
console.log(obj1) //{x: 1, y: 2} //原對象未改變
console.log(obj2) //{x: 2, y: 2}
const obj1 = {
  x: 1, 
  y: {
    m: 1
  }
};
const obj2 = Object.assign({}, obj1);

obj2.y.m = 2; \\修改obj2.y.m,改變對象中的引用類型值
console.log(obj1) //{x: 1, y: {m: 2}} 原對象也被改變
console.log(obj2) //{x: 2, y: {m: 2}}

三、深拷貝

1.JSON.parse()和JSON.stringify()

const obj1 = {
  x: 1, 
  y: {
    m: 1
  }
};
const obj2 = JSON.parse(JSON.stringify(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: 1}} 原對象未改變
console.log(obj2) //{x: 2, y: {m: 2}}

這種方法使用較為簡單,可以滿足基本日常的深拷貝需求,而且能夠處理JSON格式能表示的所有數(shù)據(jù)類型,但是有以下幾個缺點:

  • undefined、任意的函數(shù)、正則表達(dá)式類型以及 symbol 值,在序列化過程中會被忽略(出現(xiàn)在非數(shù)組對象的屬性值中時)或者被轉(zhuǎn)換成 null(出現(xiàn)在數(shù)組中時);

  • 它會拋棄對象的constructor。也就是深拷貝之后,不管這個對象原來的構(gòu)造函數(shù)是什么,在深拷貝之后都會變成Object;

  • 如果對象中存在循環(huán)引用的情況無法正確處理。

2.遞歸

function deepCopy1(obj) {
  // 創(chuàng)建一個新對象
  let result = {}
  let keys = Object.keys(obj),
    key = null,
    temp = null;

  for (let i = 0; i < keys.length; i++) {
    key = keys[i];  
    temp = obj[key];
    // 如果字段的值也是一個對象則遞歸操作
    if (temp && typeof temp === 'object') {
      result[key] = deepCopy(temp);
    } else {
    // 否則直接賦值給新對象
      result[key] = temp;
    }
  }
  return result;
}

const obj1 = {
  x: {
    m: 1
  },
  y: undefined,
  z: function add(z1, z2) {
    return z1 + z2
  },
  a: Symbol("foo")
};

const obj2 = deepCopy1(obj1);
obj2.x.m = 2;

console.log(obj1); //{x: {m: 1}, y: undefined, z: ?, a: Symbol(foo)}
console.log(obj2); //{x: {m: 2}, y: undefined, z: ?, a: Symbol(foo)}

四、循環(huán)引用

看似遞歸已經(jīng)完全解決我們的問題了,然而還有一種情況我們沒考慮到,那就是循環(huán)引用

1.父級引用

這里的父級引用指的是,當(dāng)對象的某個屬性,正是這個對象本身,此時我們?nèi)绻M(jìn)行深拷貝,可能會在子元素->父對象->子元素...這個循環(huán)中一直進(jìn)行,導(dǎo)致棧溢出。比如下面這個例子:

 const obj1 = {
  x: 1, 
  y: 2
};
obj1.z = obj1;

const obj2 = deepCopy1(obj1); \\棧溢出

解決辦法是:只需要判斷一個對象的字段是否引用了這個對象或這個對象的任意父級即可,可以修改上面的deepCopy函數(shù):

function deepCopy2(obj, parent=null) {
  //創(chuàng)建一個新對象
  let result = {};
  let keys = Object.keys(obj),
     key = null,
     temp = null,
     _parent = parent;
  //該字段有父級則需要追溯該字段的父級
  while(_parent) {
    //如果該字段引用了它的父級,則為循環(huán)引用
    if(_parent.originParent === obj) {
      //循環(huán)引用返回同級的新對象
      return _parent.currentParent;
    }
    _parent = _parent.parent
  }
  for(let i=0,len=keys.length;i<len;i++) {
    key = keys[i]
    temp = obj[key]
    // 如果字段的值也是一個新對象
    if(temp && typeof temp === 'object') {
      result[key] = deepCopy(temp, {
        //遞歸執(zhí)行深拷貝,將同級的待拷貝對象與新對象傳遞給parent,方便追溯循環(huán)引用
        originParent: obj,
        currentParent: result,
        parent: parent
      });
    } else {
      result[key] = temp;
    }
  }
  return result;
}

const obj1 = {
  x:1
}
obj1.z = obj1;

const obj2 = deepCopy2(obj1);

2. 同級引用

假設(shè)對象obj有a,b,c三個子對象,其中子對象c中有個屬性d引用了對象obj下面的子對象a。

const obj= {
  a: {
    name: 'a'
  },
  b: {
    name: 'b'
  },
  c: {

  }
};
c.d.e = obj.a;

此時c.d.e和obj.a 是相等的,因為它們引用的是同一個對象

console.log(c.d.e === obj.a); //true

如果我們調(diào)用上面的deepCopy2函數(shù)

const copy = deepCopy2(obj);
console.log(copy.a); // 輸出: {name: "a"}
console.log(copy.d.e);// 輸出: {name: "a"}
console.log(copy.a === copy.d.e); // 輸出: false

以上表現(xiàn)我們就可以看出,雖然opy.a 和copy.d.e在字面意義上是相等的,但二者并不是引用的同一個對象,這點上來看對象copy和原對象obj還是有差異的。

這種情況是因為obj.a并不在obj.d.e的父級對象鏈上,所以deepCopy2函數(shù)就無法檢測到obj.d.e對obj.a也是一種引用關(guān)系,所以deepCopy2函數(shù)就將obj.a深拷貝的結(jié)果賦值給了copy.d.e。

解決方案:父級的引用是一種引用,非父級的引用也是一種引用,那么只要記錄下對象A中的所有對象,并與新創(chuàng)建的對象一一對應(yīng)即可。

function deepCopy3(obj) {
  // hash表,記錄所有的對象的引用關(guān)系
  let map = new WeakMap();
  function dp(obj) {
    let result = null;
    let keys = Object.keys(obj);
    let key = null,
      temp = null,
      existobj = null;

    existobj = map.get(obj);
    //如果這個對象已經(jīng)被記錄則直接返回
    if(existobj) {
      return existobj;
    }

    result = {}
    map.set(obj, result);

    for(let i =0,len=keys.length;i<len;i++) {
      key = keys[i];
      temp = obj[key];
      if(temp && typeof temp === 'object') {
        result[key] = dp(temp);
      }else {
        result[key] = temp;
      }
    }
    return result;
  }
  return dp(obj);
}

const obj= {
  a: {
    name: 'a'
  },
  b: {
    name: 'b'
  },
  c: {

  }
};
c.d.e = obj.a;

const copy = deepCopy3(obj);

到此,相信大家對“javascript深拷貝、淺拷貝和循環(huán)引用的詳細(xì)講解”有了更深的了解,不妨來實際操作一番吧!這里是億速云網(wǎng)站,更多相關(guān)內(nèi)容可以進(jìn)入相關(guān)頻道進(jìn)行查詢,關(guān)注我們,繼續(xù)學(xué)習(xí)!

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI