淺拷貝和深拷貝(較爲完整的探索)

開篇點題:javascript

  • 本文將用遞歸學習的方式探索關於《淺拷貝和深拷貝》中涉及的知識點,但願小夥伴們在腦海中深拷貝這些知識點,在面試或者工做中可以靈活使用。
const deepCloneKnowledgePoints = {
  title: '淺拷貝和深拷貝',
  chapterOne: {
    title: '章節一',
    point: [
      '淺拷貝和深拷貝初探索',
      '基本數據類型和引用數據類型',
    ],
  },
  chapterTwo: {
    title: '章節二',
    point: [
      '手寫淺拷貝',
      'Object.assign()',
      'Array.prototype.concat()',
      'Array.prototype.slice()',
      '...obj 展開運算符',
    ],
    extend: [
      'for...in',
      'for...of',
      'for...in 和 for...of 的區別',
      'hasOwnProperty',
    ],
  },
  chapterThree: {
    title: '章節三',
    point: [
      '手寫深拷貝',
      'JSON.parse(JSON.stringify())',
      '函數庫 Lodash',
      '框架 jQuery',
    ],
    extend: [
      'typeof',
      'instanceof',
      'constructor',
      {
        point: 'Object.prototype.toString.call()',
        extend: [
          'Function.prototype.apply()',
          'Function.prototype.bind()',
          'Function.prototype.call()',
          'apply()、bind() 以及 call() 的區別',
        ],
      },
      {
        point: 'JSON.parse(JSON.stringify())',
        extend: [
          'JSON.parse()',
          'JSON.stringify()',
        ]
      }
    ],
  },
};
複製代碼

一 目錄

不折騰的前端,和鹹魚有什麼區別php

目錄
一 目錄
二 前言
三 淺拷貝和深拷貝初探索
四 淺拷貝
4.1 手寫淺拷貝
4.2 Object.assign()
4.3 Array.prototype.concat()
4.4 Array.prototype.slice()
4.5 ...obj 展開運算符
五 深拷貝
5.1 手寫深拷貝
5.2 JSON.parse(JSON.stringify())
5.3 函數庫 Lodash
5.4 框架 jQuery
六 總結

二 前言

返回目錄css

在某次寫業務代碼中,忽然來了個場景:html

  • 後端小夥伴須要我將 接口返回的源數據 和 頁面修改後新數據 各發一份給它。

代碼表現爲:前端

// 初始數據
const initData = {
  // ...
};

// finalData 數據來源於 initData
// 可是 finalData 的修改不能影響到 initData
const finalData = _.deepClone(initData);

// 返回最終形式 result 給後端
// finalData 和 initData 是兩份不一樣的數據
const result = {
  initData: initData,
  finalData: finalData,
};
return res;
複製代碼

那麼問題來了,若是採用 const finalData = initData,可不能夠直接修改 finalData 的數據呢?它會不會影響到 initDatajava

我們能夠小試一下:node

const initData = { obj: '123' };
const finalData = initData;
finalData.obj = '456';
console.log(initData);
console.log(finalData);
// 猜猜上面兩個 console.log 返回啥?
複製代碼

若是不能夠,那麼咱們須要怎樣操做,才能返回新舊對比數據給後端?jquery

三 淺拷貝和深拷貝初探索

返回目錄git

如今,咱們開始揭曉上面答案。github

在這以前,先看看 基本數據類型和引用數據類型

數據分爲基本數據類型和引用數據類型。

  • 基本數據類型:String、Number、Boolean、Null、Undefined、Symbol。基本數據類型是直接存儲在棧中的數據。
  • 引用數據類型:Array、Object。引用數據類型存儲的是該對象在棧中引用,真實的數據存儲在內存中。

爲了形象瞭解,我們看下面代碼:

// 基本數據類型
let str1 = '123';
str2 = str1;
str2 = '456';
console.log(str1); // '123'
console.log(str2); // '456'

// 引用數據類型
let arr1 = [1, 2, 3];
arr2 = arr1;
arr2.push(4);
console.log(arr1); // [1, 2, 3, 4]
console.log(arr2); // [1, 2, 3, 4]
複製代碼

如上,因爲基本數據類型是直接存儲的,因此若是咱們對基本數據類型進行拷貝,而後修改新數據後,不會影響到原數據。

而當你對引用數據類型進行拷貝,而後修改新數據後,它就會影響到原數據。

形象舉例:

  • 我有一個大農場,裏面有房子、車子、羊羣、大草原。
  • 基本數據類型:我以前有一輛車子,我又買了一輛,這樣我就有兩輛,某一輛壞了不會影響到另外一輛。我以前有一棟房子,我又蓋了一棟,這樣我就有兩棟,某一棟被雷劈了不會影響到另外一棟。
  • 引用數據類型:我房子前面就是大草原,裏面有一些腐爛的草被羊吃了,羊由於吃了腐爛的草得病死了。草原仍是草原,可是內部的草變少了;羊羣仍是羊羣,可是內部的羊變少了。

在 JavaScript 中,房子、車子這些 單個 屬性的,就是基本數據類型,它有本身的私人領域,你能夠克隆它,可是新的它不會影響原有的它(舊車壞,新車照樣開)。

而若是是羊羣、大草原這種 集體 屬性的,就是對象數據數據,若是咱們一個一個存儲,那就太麻煩了,很差一會兒找到,因此瀏覽器會給它開闢一大塊領域,而後告訴你怎麼找到它(索引)。因此當你修改它當中的內容(羊吃草,草減小),它內部就會發生變化。

OK,瞭解 基本數據類型和引用數據類型 後,我們進一步探索賦值、淺拷貝和深拷貝:

/** * @name 賦值 */
const dataOne = {
  title: 'study',
  number: ['jsliang', 'JavaScriptLiang', 'LiangJunrong'],
};
const dataTwo = dataOne;
dataTwo.title = 'play';
dataTwo.number = ['null'];
console.log(dataOne);
// dataOne: { title: 'play', number: ['null'] }
console.log(dataTwo);
// dataTwo: { title: 'play', number: ['null'] }

/** * @name 淺拷貝 */
const dataThree = {
  title: 'study',
  number: ['jsliang', 'JavaScriptLiang', 'LiangJunrong'],
};
const dataFour = shallowClone(dataThree); // shallowClone 待實現
dataFour.title = 'play';
dataFour.number = ['null'];
console.log(datadataThreeOne);
// dataThree: { title: 'study', number: ['null'] }
console.log(dataFour);
// dataFour: { title: 'play', number: ['null'] }

/** * @name 深拷貝 */
const dataFive = {
  title: 'study',
  number: ['jsliang', 'JavaScriptLiang', 'LiangJunrong'],
};
const dataSix = deepClone(dataFive); // deepClone 待實現
dataSix.title = 'play';
dataSix.number = ['null'];
console.log(dataFive);
// dataFive: { title: 'study', number: ['jsliang', 'JavaScriptLiang', 'LiangJunrong'] }
console.log(dataSix);
// dataSix: { title: 'play', number: ['null'] }
複製代碼

如上,咱們給出結論:

  • 賦值:引用地址的拷貝。修改賦值後的數據,無論是基本數據類型仍是引用數據類型,都會影響到原數據。
  • 淺拷貝:一層拷貝。在淺拷貝中,修改基本數據類型不會影響原有數據的基本數據類型,修改引用數據類型會影響原有的數據類型。
  • 深拷貝:無限層級拷貝。在深拷貝中,修改基本數據類型和引用數據類型都不會影響原有的數據類型。

上面說法可能有問題:
一、有的大佬認爲淺拷貝是拷貝一層,即第一層數據無論是數組仍是字符串等都是互不影響的;
二、有的則認爲淺拷貝只對基本數據類型有效,對於引用數據類型會引用其地址。

這裏 jsliang 我的理解是將淺拷貝區分於賦值,若是你有更好觀點,能夠提出來~

圖表化以下所示:

和原數據是否指向同一對象 原數據爲基本數據類型 原數據包含子對象
賦值 改變【會】使原數據一同改變 改變【會】使原數據一同改變
淺拷貝 改變【不會】使原數據一同改變 改變【會】使原數據一同改變
深拷貝 改變【不會】使原數據一同改變 改變【不會】使原數據一同改變

四 淺拷貝

返回目錄

4.1 手寫淺拷貝

返回目錄

在探討經過工具進行淺拷貝以前,咱們嘗試 「手寫」 一份淺拷貝:

const arr1 = [1, 2, ['jsliang', 'JavaScriptLiang'], 4];

const shallowClone = (arr) => {
  const dst = [];
  for (let prop in arr) {
    if (arr.hasOwnProperty(prop)) {
        dst[prop] = arr[prop];
    }
  }
  return dst;
}

const arr2 = shallowClone(arr1);
arr2[2].push('LiangJunrong');
arr2[3] = 5;

console.log(arr1);
// [ 1, 2, [ 'jsliang', 'JavaScriptLiang', 'LiangJunrong' ], 4 ]
console.log(arr2);
// [ 1, 2, [ 'jsliang', 'JavaScriptLiang', 'LiangJunrong' ], 5 ]
複製代碼

能夠看到,這裏咱們修改引用數據類型裏面的引用數據類型的時候,仍是會影響到原數據,可是若是咱們修改裏面的基本數據類型的時候,就不會影響到原數據了。

那麼,在更深刻講解探討以前,咱們先聊聊爲了更好講解上面代碼思路,我作了啥:

  1. 查找 for...in,發現屬於 JavaScript 語句和聲明,因此給本身文檔庫起了個目錄 語句和聲明
  2. 發現 for...infor...of 類似,因而分別查了 for...infor...of,順帶對着兩個進行了對比 for...in 和 for...of 對比
  3. 發現 for...of 還能夠涉及到繼承和原型鏈,因而順帶將以前面試寫的文章弄過來:繼承和原型鏈
  4. 查找 hasOwnProperty,發現我還沒建 Object 分類,因而新建了目錄:Object,並寫好了 hasOwnProperty

閒扯完畢,咱們仍是講解下上面代碼爲啥能淺拷貝數據:

  • for...in:遍歷 Object 對象 arr1,將可枚舉值列舉出來。
  • hasOwnProperty():檢查該枚舉值是否屬於該對象 arr1,若是是繼承過來的就去掉,若是是自身的則進行拷貝。

這樣,咱們就成功實現了淺拷貝,而且瞭解了其中涉及的知識點~

下面咱們再介紹 3 種便捷形式,實現快速淺拷貝。

4.2 Object.assign()

返回目錄

Object.assign() 方法能夠把任意多個的源對象自身的可枚舉屬性拷貝給目標對象,而後返回目標對象。

可是須要注意的是,Object.assgin() 進行的是淺拷貝,拷貝的是對象的屬性的引用,而不是對象自己。

const obj1 = {
  username: 'LiangJunrong',
  skill: {
    play: ['basketball', 'computer game'],
    read: 'book',
  },
  girlfriend: ['1 號備胎', '2 號備胎', '3 號備胎'],
};
const obj2 = Object.assign({}, obj1);
obj2.username = 'jsliang'; // 修改基本類型
obj2.skill.read = 'computer book'; // 修改二層基本類型
obj2.skill.play = ['footboll']; // 修改二層引用類型
obj2.girlfriend = ['以前的都是瞎吹的!'];
console.log(obj1);
// { username: 'LiangJunrong',
// skill: { play: [ 'footboll' ], read: 'computer book' },
// girlfriend: [ '1 號備胎', '2 號備胎', '3 號備胎' ] }
console.log(obj2);
// { username: 'jsliang',
// skill: { play: [ 'footboll' ], read: 'computer book' },
// girlfriend: [ '以前的都是瞎吹的!' ] }
複製代碼

能夠看到的是,Object.assign() 對於第一層的數據來講,是深拷貝,對於第二層及以上的數據來講,是淺拷貝。

4.3 Array.prototype.concat()

返回目錄

concat() 是數組的一個內置方法,用戶合併兩個或者多個數組。

這個方法不會改變現有數組,而是返回一個新數組。

詳細內容能夠看 jsliang 的文章或者看 MDN 的文章:

在這裏,咱們經過 concat() 來淺拷貝一個數組:

const arr1 = [
  1,
  {
    username: 'jsliang',
  },
];

let arr2 = arr1.concat();
arr2[0] = 2;
arr2[1].username = 'LiangJunrong';
console.log(arr1);
// [ 1, { username: 'LiangJunrong' } ]
console.log(arr2);
// [ 2, { username: 'LiangJunrong' } ]
複製代碼

看到這裏,小夥伴們應該明白,經過 concat() 進行的淺拷貝,能夠修改裏面的基本數據類型而不影響原值,可是修改裏面的引用數據類型,就會影響到原有值了。

4.4 Array.prototype.slice()

返回目錄

slice() 也是數組的一個內置方法,該方法會返回一個新的對象。

slice() 不會改變原數組。

詳細能夠看下 jsliangconcat 或者直接看 MDN 的文獻:

const arr1 = [
  1,
  {
    username: 'jsliang',
  },
];

let arr2 = arr1.slice();
arr2[0] = 2;
arr2[1].username = 'LiangJunrong';
console.log(arr1);
// [ 1, { username: 'LiangJunrong' } ]
console.log(arr2);
// [ 2, { username: 'LiangJunrong' } ]
複製代碼

能夠看到的是,它和前面的 concat() 表現的淺拷貝如出一轍,若是小夥伴但願研究更深層次的內容,能夠看下 Array.prototype.concat()Array.prototype.slice() 的源碼具體實現。

4.5 ...obj 展開運算符

返回目錄

展開運算符是 ES6 中新提出來的一種運算符。

在拷貝數組、對象以及拼接數組等方面均可以使用。

這邊咱們也能夠嘗試下使用 const obj2 = {...obj1} 的形式進行淺拷貝。

/** * @name ...obj拷貝數組 */
const arr1 = [1, 2, 3];
const arr2 = [...arr1]; // like arr.slice()
arr2.push(4); 

console.log(arr1); // [ 1, 2, 3 ]
console.log(arr2); // [ 1, 2, 3, 4 ]

/** * @name ...obj拷貝對象 */
const obj1 = {
  name: 'jsliang',
  arr1: ['1', '2', '3'],
  obj: {
    name: 'JavaScriptLiang',
    arr2: ['4', '5', '6'],
  },
};
const obj2 = {...obj1};
obj2.name = 'LiangJunrong';
obj2.arr1 = ['null'];
obj2.obj.name = 'jsliang';
obj2.obj.arr2 = ['null'];

console.log(obj1);
// { name: 'jsliang',
// arr1: [ '1', '2', '3' ],
// obj: { name: 'jsliang', arr2: [ 'null' ] } }
console.log(obj2);
// { name: 'LiangJunrong',
// arr1: [ 'null' ],
// obj: { name: 'jsliang', arr2: [ 'null' ] } }
複製代碼

五 深拷貝

返回目錄

5.1 手寫深拷貝

返回目錄

---小節 1---

那麼,手寫深拷貝的話,要怎麼實現呢?

咱們嘗試稍微修改下前面的淺拷貝代碼:

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;
};
複製代碼

固然,這份代碼是存在問題的:

  1. 沒有對參數進行校驗,若是傳入進來的不是對象或者數組,咱們直接返回便可。
  2. 經過 typeof 判斷是否對象的邏輯不夠嚴謹。

---小節 2---

既然存在問題,那麼咱們就須要嘗試克服:

// 定義檢測數據類型的功能函數
const checkedType = (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: 'jsliang1' },
  [
    {
      name: 'LiangJunrong1',
    }
  ],
]
const obj2 = deepClone(obj1);
obj2[0] = 2;
obj2[1] = 'Hi!';
obj2[2].name = 'jsliang2';
obj2[3][0].name = 'LiangJunrong2';

console.log(obj1);
// [
// 1,
// 'Hello!',
// { name: 'jsliang1' },
// [
// { name: 'LiangJunrong1' },
// ],
// ]

console.log(obj2);
// [
// 2,
// 'Hi!',
// { name: 'jsliang2' },
// [
// { name: 'LiangJunrong2' },
// ],
// ]
複製代碼

下面講解下這份深拷貝代碼:

首先,咱們先看檢查類型的那行代碼:Object.prototype.toString.call(target).slice(8, -1)

在說這行代碼以前,咱們先對比下檢測 JavaScript 數據類型的 4 種方式:

  • 方式一:typeof:沒法判斷 null 或者 new String() 等數據類型。
  • 方式二:instanceof:沒法判斷 'jsliang'123 等數據類型。
  • 方式三:constructor:判斷 nullundefined 會直接報錯。
  • 方式四:Object.prototype.toString.call():穩健地判斷 JavaScript 數據類型方式,能夠符合預期的判斷基本數據類型 String、Undefined 等,也能夠判斷 Array、Object 這些引用數據類型。

詳細研究能夠看 jsliang 的學習文檔:

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

最後,咱們再經過 for...in 判斷 target 的全部元素,若是屬於 {} 或者 [],那麼就遞歸再進行 clone() 操做;若是是基本數據類型,則直接傳遞到數組中……從而在最後返回一個深拷貝的數據。


---小節 3---

以上,咱們的代碼看似沒問題了是否是?假設咱們須要拷貝的數據以下:

const obj1 = {};
obj1.a = obj1;
console.log(deepClone(obj1));
// RangeError: Maximum call stack size exceeded
複製代碼

看,咱們直接研製了個死循環出來!

那麼咱們須要怎麼解決呢?有待實現!

乘我還沒找到教好的解決方案以前,小夥伴們能夠看下下面文章,思考下是否能解決這個問題:

……好的,雖然口頭說着但願小夥伴們自行翻閱資料,可是爲了防止被寄刀片,jsliang 仍是在這裏寫下本身以爲 OK 的代碼:

function isObject(obj) {
  return Object.prototype.toString.call(obj) === '[object Object]';
}

function deepClone(source, hash = new WeakMap()) {
  if (!isObject(source)) return source;
  // 新增代碼,查哈希表
  if (hash.has(source)) return hash.get(source);

  var target = Array.isArray(source) ? [] : {};
  // 新增代碼,哈希表設值
  hash.set(source, target);

  for (var key in source) {
    if (Object.prototype.hasOwnProperty.call(source, key)) {
      if (isObject(source[key])) {
        // 新增代碼,傳入哈希表
        target[key] = deepClone(source[key], hash);
      } else {
        target[key] = source[key];
      }
    }
  }
  return target;
}

/** * @name 正常深拷貝測試 */
const a = {
  name: 'jsliang',
  book: {
    title: '深拷貝學習',
    price: 'free',
  },
  a1: undefined,
  a2: null,
  a3: 123
};
const b = deepClone(a);
b.name = 'JavaScriptLiang';
b.book.title = '教你如何泡妞';
b.a3 = 456;
console.log(a);
// { name: 'jsliang',
// book: { title: '深拷貝學習', price: 'free' },
// a1: undefined,
// a2: null,
// a3: 123 }
console.log(b);
// { name: 'JavaScriptLiang',
// book: { title: '教你如何泡妞', price: 'free' },
// a1: undefined,
// a2: null,
// a3: 456 }

/** * @name 解決死循環 */
const c = {};
c.test = c;
const d = deepClone(c);
console.log(c);
// { test: [Circular] }
console.log(d);
// { test: [Circular] }
複製代碼

---小節 4---

既然搞定完死循環,我們再看看另外一個問題:

const checkedType = (target) => {
  return Object.prototype.toString.call(target).slice(8, -1);
}

const deepClone = (target) => {
  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 createData = (deep, breadth) => {
  const data = {};
  let temp = data;

  for (let i = 0; i < deep; i++) {
    temp = temp['data'] = {};
    for (let j = 0; j < breadth; j++) {
      temp[j] = j;
    }
  }

  return data;
};

console.log(createData(1, 3)); 
// 1 層深度,每層有 3 個數據 { data: { '0': 0, '1': 1, '2': 2 } }

console.log(createData(3, 0));
// 3 層深度,每層有 0 個數據 { data: { data: { data: {} } } }

console.log(deepClone(createData(1000)));
// 1000 層深度,無壓力 { data: { data: { data: [Object] } } }

console.log(deepClone(createData(10, 100000)));
// 100000 層廣度,沒問題,數據遍歷須要時間

console.log(deepClone(createData(10000)));
// 10000 層深度,直接爆棧:Maximum call stack size exceeded
複製代碼

是的,你的深拷貝爆棧了!!!

雖然業務場景中可能爆棧的機率比較少,畢竟數據層級沒那麼多,可是仍是會存在這種狀況,須要怎麼處理呢?

只想大體瞭解深拷貝可能出現問題的小夥伴能夠跳過下面內容

舉個例子,假設有數據結構:

const a = {
  a1: 1,
  a2: {
    b1: 1,
    b2: {
      c1: 1
    }
  }
};
複製代碼

若是咱們將其當成數來看:

a
  /   \
 a1   a2        
 |    / \         
 1   b1 b2     
     |   |        
     1  c1
         |
         1
複製代碼

那麼,咱們就能夠採用迭代的方法,循環遍歷這棵樹了!

  1. 首先,咱們須要藉助棧。當棧爲空就遍歷完畢,棧裏面存儲下一個須要拷貝的節點
  2. 而後,往棧裏放入種子數據,key 用來存儲哪個父元素的那一個子元素拷貝對象
  3. 最後,遍歷當前節點下的子元素,若是是對象就放到棧裏,不然直接拷貝。
const deepClone = (x) => {
  const root = {};

  // 棧
  const loopList = [
    {
      parent: root,
      key: undefined,
      data: x
    }
  ];

  while (loopList.length) {
    // 深度優先
    const node = loopList.pop();
    const parent = node.parent;
    const key = node.key;
    const data = node.data;

    // 初始化賦值目標,key 爲 undefined 則拷貝到父元素,不然拷貝到子元素
    let res = parent;
    if (typeof key !== "undefined") {
      res = parent[key] = {};
    }

    for (let k in data) {
      if (data.hasOwnProperty(k)) {
        if (typeof data[k] === "object") {
          // 下一次循環
          loopList.push({
            parent: res,
            key: k,
            data: data[k]
          });
        } else {
          res[k] = data[k];
        }
      }
    }
  }

  return root;
}
複製代碼

這時候咱們再經過 createData 進行廣度和深度校驗,會發現:

console.log(deepClone(createData(10, 100000)));
// 100000 層廣度,沒問題,數據遍歷須要時間

console.log(deepClone(createData(100000)));
// 100000 層深度,也沒問題了:{ data: { data: { data: [Object] } } }
複製代碼

這樣,咱們就解決了爆棧的問題。

這裏推薦下引用思路來源於大佬的文章:深拷貝的終極探索,而後它附帶了一個深拷貝庫:@jsmini/clone,感興趣的小夥伴能夠去看看。

5.2 JSON.parse(JSON.stringify())

返回目錄

其實利用工具,達到目的,是很是聰明的作法,下面咱們討論下 JSON.parse(JSON.stringify())

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

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

注意,該方法的侷限性:
一、不能存放函數或者 Undefined,不然會丟失函數或者 Undefined;
二、不要存放時間對象,不然會變成字符串形式;
三、不能存放 RegExp、Error 對象,不然會變成空對象;
四、不能存放 NaN、Infinity、-Infinity,不然會變成 null;
五、……更多請自行填坑,具體來講就是 JavaScript 和 JSON 存在差別,二者不兼容的就會出問題。

const arr1 = [
  1,
  {
    username: 'jsliang',
  },
];

let arr2 = JSON.parse(JSON.stringify(arr1));
arr2[0] = 2;
arr2[1].username = 'LiangJunrong';
console.log(arr1);
// [ 1, { username: 'jsliang' } ]
console.log(arr2);
// [ 2, { username: 'LiangJunrong' } ]
複製代碼

5.3 函數庫 Lodash

返回目錄

Lodash 做爲一個深受你們喜好的、優秀的 JavaScript 函數庫/工具庫,它裏面有很是好用的封裝好的功能,你們能夠去試試:

這裏咱們查看下它的 cloneDeep() 方法:

能夠看到,該方法會遞歸拷貝 value

在這裏,咱們體驗下它的 cloneDeep()

// npm i -S lodash
var _ = require('lodash');

const obj1 = [
  1,
  'Hello!',
  { name: 'jsliang1' },
  [
    {
      name: 'LiangJunrong1',
    }
  ],
]
const obj2 = _.cloneDeep(obj1);
obj2[0] = 2;
obj2[1] = 'Hi!';
obj2[2].name = 'jsliang2';
obj2[3][0].name = 'LiangJunrong2';

console.log(obj1);
// [
// 1,
// 'Hello!',
// { name: 'jsliang1' },
// [
// { name: 'LiangJunrong1' },
// ],
// ]

console.log(obj2);
// [
// 2,
// 'Hi!',
// { name: 'jsliang2' }, 
// [
// { name: 'LiangJunrong2' },
// ],
// ]
複製代碼

這裏咱們使用的是 Node 安裝其依賴包的形式,若是須要用 MDN 等,小夥伴能夠前往它官網瞅瞅。(地址在本節開頭)

5.4 框架 jQuery

返回目錄

固然,不可厚非你的公司還在用着 jQuery,可能還須要兼容 IE6/7/8,或者你使用 React,可是有些場景還使用了 jQuery,畢竟 jQuery 是個強大的框架。

下面咱們嘗試下使用 jQuery 的 extend() 進行深拷貝:

index.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
</head>
<body>
  <p>嘗試 jQuery 深拷貝</p>
  <script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.js"></script>
  <script>
    $(function() {
      const obj1 = [
        1,
        'Hello!',
        { name: 'jsliang1' },
        [
          {
            name: 'LiangJunrong1',
          }
        ],
      ]
      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 = 'jsliang2';
      obj2[3][0].name = 'LiangJunrong2';

      console.log(obj1);
      // [
      //   1,
      //   'Hello!',
      //   { name: 'jsliang1' },
      //   [
      //     { name: 'LiangJunrong1'},
      //   ],
      // ];

      console.log(obj2);
      // [
      //   2,
      //   'Hi!',
      //   { name: 'jsliang2' },
      //   [
      //     { name: 'LiangJunrong2' },
      //   ],
      // ];
    });
  </script>
</body>
</html>
複製代碼

這裏因爲 Node 直接引用包好像沒嘗試成功,因此咱經過 index.html 的形式,引用了 jQuery 的 CDN 包,從而嘗試了它的深拷貝。

推薦經過 live-server 來實時監控 HTML 文件的變化

若是須要查看 jQuery.extend() 源碼能夠觀看文章:
《深拷貝與淺拷貝的實現(一)》
《JavaScript 淺拷貝和深拷貝》

jQuery.extend 源碼

jQuery.extend = jQuery.fn.extend = function() {
  var options,
    name,
    src,
    copy,
    copyIsArray,
    clone,
    target = arguments[0] || {},
    i = 1,
    length = arguments.length,
    deep = false;

  // Handle a deep copy situation
  if (typeof target === "boolean") {
    deep = target;

    // Skip the boolean and the target
    target = arguments[i] || {};
    i++;
  }

  // Handle case when target is a string or something (possible in deep copy)
  if (typeof target !== "object" && !jQuery.isFunction(target)) {
    target = {};
  }

  // Extend jQuery itself if only one argument is passed
  if (i === length) {
    target = this;
    i--;
  }

  for (; i < length; i++) {
    // Only deal with non-null/undefined values
    if ((options = arguments[i]) != null) {
      // Extend the base object
      for (name in options) {
        src = target[name];
        copy = options[name];

        // Prevent never-ending loop
        if (target === copy) {
          continue;
        }

        // Recurse if we're merging plain objects or arrays
        if (
          deep &&
          copy &&
          (jQuery.isPlainObject(copy) || (copyIsArray = Array.isArray(copy)))
        ) {
          if (copyIsArray) {
            copyIsArray = false;
            clone = src && Array.isArray(src) ? src : [];
          } else {
            clone = src && jQuery.isPlainObject(src) ? src : {};
          }

          // Never move original objects, clone them
          target[name] = jQuery.extend(deep, clone, copy);

          // Don't bring in undefined values
        } else if (copy !== undefined) {
          target[name] = copy;
        }
      }
    }
  }
  // Return the modified object
  return target;
};
複製代碼

六 總結

返回目錄

很遺憾,咱們就這樣暫時完成了 淺拷貝和深拷貝 的第一期探索啦~

在寫這系列內容中,jsliang 猶豫過,熬夜過,想放棄過……但仍是堅持下來,過了一遍。

在查資料豐富這系列知識的過程當中,jsliang 忽略了一些知識點探索,要否則會產生更多的疑惑,最終每天通宵達旦到晚上 04:00 還在精神抖擻折騰~

咱們還未深刻的一些點有:

  1. Lodash 如何實現深拷貝
  2. jQuery 如何實現深拷貝
  3. Object.assign 原理及其實現
  4. ……

可能有些小夥伴會以爲:

  • 啊,你不先折騰完,不折不扣搞清楚,就發表出來,你不以爲羞恥嗎!

enm...怎麼說,首先在寫這篇文章的時候,jsliang 作的是廣度探索,即碰到的每個知識點都接觸瞭解一下,其實這樣寫很是累,可是進步是很是大的,由於你把一些知識點都挖掘出來了。

而後,jsliang 一開始的目標,只是想了解下手寫深拷貝,以及一些工具快速實現深拷貝,因此我的以爲本次目標已經達到甚至超標了。

最後,還有一些知識點,例如手寫一個 Object.assign() 或者瞭解 Lodash 的深拷貝源碼,其實但願進一步瞭解的小夥伴,確定會自行先探索,固然若是小夥伴但願有 「前人躺坑」,那麼能夠期待個人後續完善。

畢竟:不折騰的前端,跟鹹魚有什麼區別!

因此,若是小夥伴們想持續跟進,能夠到 jsliangGitHub 倉庫 首頁找到個人公衆號、微信、QQ 等聯繫方式。

那麼,咱們後期再會~

  • 參考文獻
  1. Lodash clone 系列
  2. 淺拷貝與深拷貝
  3. 深拷貝的終極探索
  4. 深拷貝與淺拷貝的實現(一)
  5. 深刻淺出深拷貝與淺拷貝
  6. 什麼是 js 深拷貝和淺拷貝及其實現方式
  7. JavaScript 淺拷貝和深拷貝
  8. js 深拷貝 vs 淺拷貝
  9. 深拷貝的終極探索(99%的人都不知道)
  10. 面試題之如何實現一個深拷貝

jsliang 廣告推送:
也許小夥伴想了解下雲服務器
或者小夥伴想買一臺雲服務器
或者小夥伴須要續費雲服務器
歡迎點擊 雲服務器推廣 查看!

知識共享許可協議
jsliang 的文檔庫梁峻榮 採用 知識共享 署名-非商業性使用-相同方式共享 4.0 國際 許可協議進行許可。
基於github.com/LiangJunron…上的做品創做。
本許可協議受權以外的使用權限能夠從 creativecommons.org/licenses/by… 處得到。

相關文章
相關標籤/搜索