JS物件複製:深複製和淺複製

華為雲開發者聯盟發表於2021-08-18
摘要:物件複製,簡而言之就是將物件再複製一份,但是,複製的方法不同將會得到不同的結果。

本文分享自華為雲社群《js物件深淺複製,來,試試看!》,作者: 北極光之夜。。

一.速識概念:

物件複製,簡而言之就是將物件再複製一份,但是,複製的方法不同將會得到不同的結果。比如直接給新變數賦值為一個物件:

  // 1.建一個物件
  var obj = {
    name: "北極光之夜。",
    like: "aurora",
  };
  // 2. 直接將物件賦值給變數 clone
  var clone = obj;
  // 3.修改obj的like屬性
  obj.like = "wind";
  // 4.輸出 clone 物件
  console.log(clone);

從輸出結果可以看到,我明明改變的是 obj 物件的屬性,但是 clone 物件的屬性也改變了。這是因為,當建立 obj 物件時,它在堆記憶體中開闢了一塊空間儲存物件的內容。而當 clone 直接賦值為 obj 時,clone 並不會再重新開闢一塊堆記憶體,而是 obj 跟 clone 說我把我這記憶體空間儲存的物件的地址給你,這個地址存在棧記憶體中,你透過棧記憶體的地址找到堆記憶體裡物件的內容,我們們共用就完事了。所以說, obj 和 clone 指向的都是同一塊內容,不管誰改了物件的內容,別人再訪問都是改過之後的了。
image.png

所以這不是我們想要的,我不想共用,我想要屬於自己的一片天地,我命由我不由你,所以這就需要淺複製和深複製了。

簡單補充: 像一些基本資料型別的變數(Number Boolean String undefined null)被賦值時會直接在棧記憶體中開闢出了一個新的儲存區域用來儲存新的變數,不會如物件那樣只是把引用給別人。

二.淺複製原理與常用方法:

簡單來說淺複製就是只複製一層。什麼意思呢 ?比如我有一個物件 obj :

 var obj = {
        name: "北極光之夜。",
        like: "aurora",
      };

我要把它複製給變數 b ,原理就是我再重新開闢一塊記憶體,然後我直接看 obj 裡有什麼屬性和值就直接複製一份,比如透過如下方式實現:

     // 1.建一個物件
      var obj = {
        name: "北極光之夜。",
        like: "aurora",
      };
      // 2. 封裝一個函式,實現傳入一個物件返回一個複製後的新物件
      function cloneObj(obj) {
        let clone = {};
        // 3.用 for  in 遍歷obj的屬性
        for (let i in obj) {
          clone[i] = obj[i];
        }
        return clone;
      }
      // 4.執行函式,將得到一個新物件
      var clone = cloneObj(obj);
      // 5.更改 obj 屬性值
      obj.like = "wind";
      // 6.輸出
      console.log(clone);

結果:
image.png

可以看到,就是新建一個空物件,還是迴圈直接賦值給它,這時改變 obj 的like屬性值 ,新建的那個物件也不受影響了。但是,如果 obj 是下面這種形式的呢:

 var obj = {
        name: "北極光之夜。",
        like: "aurora",
        num: {
          a: "1",
          b: "2",
        },
      };

此時再用上面那種方法就不行了,如果obj只改變像 name 這種屬性還沒問題,但是當 obj 改變得是像 num 這種引用型別(物件、陣列都是引用型別)的資料時,複製的物件還是能被影響,因為淺複製只能複製一層,如果複製的物件裡還有子物件的話,那子物件複製其是也只是得到一個地址指向而已。這透過上面程式碼也能看出,就一層迴圈而已。想要真的達到我命由我不由天的話得用深複製,真正的刨根問底。深複製見第三大點。下面介紹下淺複製常用的方法,當物件只有一層的時候還是用淺複製好。

淺複製常用的方法:

1.第一種是主要利用 for in 遍歷原物件的屬性。

// 封裝一個函式,實現傳入一個物件返回一個複製後的新物件
 function cloneObj(obj) {
    let clone = {};
    // 用 for  in 遍歷obj的屬性
    for (let i in obj) {
      clone[i] = obj[i];
    }
    return clone;
  }

2.可以用Object.keys()方法:

Object.keys() 方法會返回一個由一個給定物件的自身可列舉屬性組成的陣列。

function cloneObj(obj) {
    let clone = {};
    for (let i of Object.keys(obj)) {
      clone[i] = obj[i];
    }
    return clone;
  }

3.可以用Object.entries()方法:

Object.entries()方法返回一個給定物件自身可列舉屬性的鍵值對陣列。

  function cloneObj(obj) {
    let clone = {};
    for (let [key, value] of Object.entries(obj)) {
      clone[key] = value;
    }
    return clone;
  }

4.可用Object.getOwnPropertyNames()配合forEach迴圈:

Object.getOwnPropertyNames()返回一個由它的屬性構成的陣列。

 function cloneObj(obj) {
    let clone = {};
    Object.getOwnPropertyNames(obj).forEach(function (item) {
      clone[item] = obj[item];
    });
    return clone;
  }

5.可用Object.defineProperty()方法:

Object.defineProperty(obj, prop, descriptor) 方法會直接在一個物件上定義一個新屬性,或者修改一個物件的現有屬性,並返回此物件。obj要定義屬性的物件。prop要定義或修改的屬性的名稱或 Symbol。descriptor要定義或修改的屬性描述符。

Object.getOwnPropertyDescriptor():返回指定物件上一個自有屬性對應的屬性描述符。
屬性描述符:JS 提供了一個內部資料結構,用來描述物件的值、控制其行為。稱為屬性描述符。

function cloneObj(obj) {
        let clone = {};
        Object.getOwnPropertyNames(obj).forEach(function (item) {
          // 獲取原本obj每個屬性修飾符
          var des = Object.getOwnPropertyDescriptor(obj, item);
          // 把屬性修飾符賦值給新物件
          Object.defineProperty(clone, item, des);
        });
        return clone;
      }

還有很多方法,就不一一列舉了

三.深複製常見方法:

深複製就不會像淺複製那樣只複製一層,而是有多少層我就複製多少層,要真正的做到全部內容都放在自己新開闢的記憶體裡。可以利用遞迴思想實現深複製。

1.可以如下實現,還是用 for in 迴圈,如果為屬性物件則遞迴:

function cloneObj(obj) {
        let clone = {};
        for (let i in obj) {
          // 如果為物件則遞迴更進一層去複製
          if (typeof obj[i] == "object" && obj[i] != null) {
            clone[i] = cloneObj(obj[i]);
          } else {
            clone[i] = obj[i];
          }
        }
        return clone;
      }

試一試看:

 // 1.建一個物件
 var obj = {
        name: "北極光之夜。",
        like: "aurora",
        age: {
          a: 1,
          b: 2,
        },
      };

      // 2. 封裝一個函式,實現傳入一個物件返回一個複製後的新物件
      function cloneObj(obj) {
        let clone = {};
        for (let i in obj) {
          // 如果為物件則遞迴更進一層去複製
          if (typeof obj[i] == "object" && obj[i] != null) {
            clone[i] = cloneObj(obj[i]);
          } else {
            clone[i] = obj[i];
          }
        }
        return clone;
      }
      // 4.執行函式,將得到一個新物件
      var clone = cloneObj(obj);
      // 5.更改 obj 屬性值
      obj.age.a = "666";
      // 6.輸出
      console.log(clone);

結果如下,複製成功,原物件改變無法使新物件也改變:
image.png

2.如果物件裡面有陣列怎麼辦,陣列也跟物件一樣是引用型別,那麼我們可以在開頭加個判斷它是物件還是陣列,陣列的話賦空陣列,一樣遍歷複製:

 function cloneObj(obj) {
        // 透過原型鏈判斷 obj 是否為陣列
        if (obj instanceof Array) {
          var clone = [];
        } else {
          var clone = {};
        }
        for (let i in obj) {
          // 如果為物件則遞迴更進一層去複製
          if (typeof obj[i] == "object" && obj[i] != null) {
            clone[i] = cloneObj(obj[i]);
          } else {
            clone[i] = obj[i];
          }
        }
        return clone;
      }

試一試看:

var obj = {
        name: "北極光之夜。",
        like: "aurora",
        age: {
          a: [1, 2, 3],
          b: 2,
        },
      };

      // 2. 封裝一個函式,實現傳入一個物件返回一個複製後的新物件
      function cloneObj(obj) {
        // 先判斷 obj 是否為陣列
        if (obj instanceof Array) {
          var clone = [];
        } else {
          var clone = {};
        }
        for (let i in obj) {
          // 如果為物件則遞迴更進一層去複製
          if (typeof obj[i] == "object" && obj[i] != null) {
            clone[i] = cloneObj(obj[i]);
          } else {
            clone[i] = obj[i];
          }
        }
        return clone;
      }
      // 4.執行函式,將得到一個新物件
      var clone = cloneObj(obj);
      // 5.更改 obj 屬性值
      obj.age.a[1] = "666";
      // 6.輸出
      console.log(clone);

結果沒問題:
image.png

當然,也可用Array.isArray(obj)方法用於判斷一個物件是否為陣列。如果物件是陣列返回 true,否則返回 false。

function cloneObj(obj) {
        // 判斷 obj 是否為陣列
        if (Array.isArray(obj)) {
          var clone = [];
        } else {
          var clone = {};
        }
        for (let i in obj) {
          // 如果為物件則遞迴更進一層去複製
          if (typeof obj[i] == "object" && obj[i] != null) {
            clone[i] = cloneObj(obj[i]);
          } else {
            clone[i] = obj[i];
          }
        }
        return clone;
      }

四.總結:

以上就是深淺複製的大致內容啦。因為物件是引用型別,所以直接賦值物件給新變數,那麼新變數指向的記憶體和原物件是一樣的。所以我們透過淺複製和深複製實現開闢自己的記憶體空間。而淺複製只複製一層,深複製複製全部。如果,文章有什麼錯誤的,懇請大佬指出。

點選關注,第一時間瞭解華為雲新鮮技術~

相關文章