淺拷貝與深拷貝(三)代碼實現深拷貝

代碼實現深拷貝

  經過對淺拷貝的簡單瞭解,咱們就到了如何實現深拷貝的問題上,常見的循環遍歷,只是遍歷了一層數據,明顯解決不了這個問題,不過咱們可以看出,深拷貝的問題不就在於怎麼解決無限層級拷貝問題嗎,這種數據類型似乎在哪見過,對沒錯,可以用遞歸解決!

  咱們先對淺拷貝代碼進行改造,用了遞歸之後就實現了一個深拷貝。

const deepClone = (source) => {
    const target = {};
    for (const i in source) {
      if (source.hasOwnProperty(i)
        && target[i] === 'object') {
        target[i] = deepClone(source[i]); // 注意這裏
      } else {
        target[i] = source[i];
      }
    }
    return target;
  };

但是這份代碼還有一些細節需要修改,如:

  • 沒有對參數進行校驗,如果傳入進來的不是對象或者數組,我們直接返回即可。

  • 通過 typeof 判斷是否對象的邏輯不夠嚴謹。 typeof null

對此可進行改進,實現一份完整的深拷貝代碼。

1. 手寫深拷貝

// 定義檢測數據類型的功能函數
const checkedType = (target) => {
  // console.log(Object.prototype.toString.call(target))
    console.log(Object.prototype.toString.call(target));
    return Object.prototype.toString.call(target).slice(8, -1);
  }
  
  // 實現深度克隆對象或者數組
  const deepClone = (target) => {
    // 判斷拷貝的數據類型
    // 初始化變量 result 成爲最終數據
    let result, targetType = checkedType(target);
    if (targetType === 'Object') {
      result = {};
    } else if (targetType === 'Array') {
      result = [];
    } else {
      return target;
    }
  
    // 遍歷目標數據
    for (let i in target) {
      // 獲取遍歷數據結構的每一項值
      let value = target[i];
      // 判斷目標結構裏的每一項值是否存在對象或者數組
      if (checkedType(value) === 'Object' || checkedType(value) === 'Array') {
        // 如果對象或者數組中還嵌套了對象或者數組,那麼繼續遍歷
        result[i] = deepClone(value);
      } else {
        result[i] = value;
      }
    }
  
    // 返回最終值
    return result;
  }
  
  const obj1 = [
    1,
    'Hello!',
    { name: '張' },
    [
      {
        name: '李',
      }
    ],
  ]
  const obj2 = deepClone(obj1);
  obj2[0] = 2;
  obj2[1] = 'Hi!';
  obj2[2].name = '小張';
  obj2[3][0].name = '小李';
  
  console.log(obj1);
  //  [ 1, 'Hello!', { name: '張' }, [ { name: '李' } ] ]
  console.log(obj2);
  //  [ 2, 'Hi!', { name: '小張' }, [ { name: '小李' } ] ]
  

經過深拷貝後,我們可以看到,再修改引用數據類型裏的值時,原對象毫不收到影響,說明這份代碼還是挺成功的。下面是代碼的實現思路:

  • Object.prototype.toString.call():穩健地判斷 JavaScript 數據類型方式,可以符合預期的判斷基本數據類型 StringUndefined 等,也可以判斷 ArrayObject 這些引用數據類型。

  • 然後,我們通過方法 targetType() 中的 Object.prototype.toString.call(),判斷傳入的數據類型屬於那種,從而改變 result 的值爲 {}、[] 或者直接返回傳入的值(return target)

  • 最後,我們再通過 for...in 判斷 target 的所有元素,如果屬於 {} 或者 [],那麼就遞歸再進行 clone() 操作;

  • 如果是基本數據類型,則直接傳遞到數組中……從而在最後返回一個深拷貝的數據。

但是如果經過嚴格的測試,這份代碼也是行不通的,如果拷貝對象是個循環空對象,那麼咱們將把自己繞進去,寫了個死循環。而對廣度和深度進行測試後,數據大的話,也是行不通的,十分容易爆棧。

這就是手寫深拷貝待解決的兩個大問題:

  • 死循環
  • 爆棧

解決方法,博主也是瞭解的比較少,能簡單理解到:

  • 死循環的解決是用哈希表進行循環檢測,我們設置一個數組或者哈希表存儲已拷貝過的對象,當檢測到當前對象已存在於哈希表中時,取出該值並返回即可。
  • 爆棧的問題可用棧解決

這裏就不過多解釋了,有興趣的可去大佬文章深挖。

面試題之如何實現一個深拷貝
深拷貝的終極探索

2. JSON. parse (JSON. stringify())

  • JSON. stringify():將對象轉成JSON 字符串。
  • JSON. parse():將字符串解析成對象。

const arr1 = [
    1,
    {
      username: 'zhangsan',
    },
  ];
  
  let arr2 = JSON.parse(JSON.stringify(arr1));
  arr2[0] = 2;
  arr2[1].username = 'li';
  console.log(arr1);
  // [ 1, { username: 'zhangsan' } ]
  console.log(arr2);
  // [ 2, { username: 'li' } ]
  

  通過JSON. parse (JSON. stringify())JavaScript 對象轉序列化(轉換成JSON字符串),再將其還原成JavaScript對象,一去一 來我們就產生了一個新的對象,而且對象會開闢新的棧,從而實現深拷貝。

  此方法雖然簡單,但是卻有很多侷限性。

  • 1、不能存放函數或者 Undefined,否則會丟失函數或者 Undefined
  • 2、不要存放時間對象,否則會變成字符串形式;
  • 3、不能存放 RegExpError 對象,否則會變成空對象;
  • 4、不能存放 NaNInfinity-Infinity,否則會變成 null

3.函數庫Lodash

Lodash作爲一個JavaScript 函數庫/工具庫,它裏面有非常好用的封裝好的功能,大家可以去試試,這裏我們查看下它的cloneDeep() 方法,該方法會遞歸拷貝value

首先需要npm先下載lodash

var lodash = require('lodash');

const obj1 = [
  1,
  'Hello!',
  { name: 'html' },
  [
    {
      name: 'css',
    }
  ],
]
const obj2 = lodash.cloneDeep(obj1);
obj2[0] = 2;
obj2[1] = 'Hi!';
obj2[2].name = 'lodash';
obj2[3][0].name = 'js';

console.log(obj1);
// [ 1, 'Hello!', { name: 'html' }, [ { name: 'css' } ] ]
console.log(obj2);
// [ 2, 'Hi!', { name: 'lodash' }, [ { name: 'js' } ] ]

4.框架jQuery中extend() 方法

const obj1 = [
        1,
        'Hello!',
        { name: 'html' },
        [
          {
            name: 'css',
          }
        ],
      ]
      const obj2 = {};
      /**
       * @name jQuery深拷貝
       * @description $.extend(deep, target, object1, object2...)
       * @param {Boolean} deep 可選 true 或者 false,默認是 false,所以一般如果需要填寫,最好是 true。
       * @param {Object} target 需要存放的位置
       * @param {Object} object 可以有 n 個原數據
       */
      $.extend(true, obj2, obj1);
      obj2[0] = 2;
      obj2[1] = 'Hi!';
      obj2[2].name = 'js';
      obj2[3][0].name = 'jq';

      console.log(obj1);
      console.log(obj2);

總結

之前瀏覽博客的時候,總是看到別人關於深拷貝與淺拷貝的文章,也知道面試這是常問的知識點,但是一直沒怎麼看。而在前不久,在小組輪到我講課了,思來想去講寫什麼,最後決定講這個,經過一星期的學習,真的學習到了不少,碰到任何不懂的問題,自己立馬去搜了搜,也連聯想了許多相關的知識點,也算是把這一難點學會了,雖然平時可能用不用到,但用不用的到並不是關鍵,要的是學會知識😁,一個深拷貝卻蘊含了那麼多的知識點,誰能想的到,平時可能也不會怎麼注意吧。


完整文章見:

自建博客地址:ahuiyo的博客 - 教你手擼深拷貝與淺拷貝

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章