前端戰五渣學JavaScript——深克隆(深拷貝)

直接進入正題javascript

JavaScript數據類型

5種簡單數據類型(也稱爲基本數據類型):Undefined、Null、Boolean、Number和String;前端

1種複雜數據類型:Object;java

基本數據類型(5種簡單數據類型):直接存儲在棧(stack)中的數據web

引用類型(複雜數據類型Object):存儲的是該對象在棧中引用,真實的數據存放在堆內存裏面試

淺克隆

基礎數據類型

(我我的以爲。。。基礎數據類型沒有什麼深克隆淺克隆之分,暫且目錄先這麼分吧)數組

由於數據類型的特性,在賦值的時候,基本數據類型和引用類型是不同的⬇️數據結構

// 基本數據類型
var a = '我是變量a的值';
var b = a;

console.log(a); // 我是變量a的值
console.log(b); // 我是變量a的值

b = '我是變量b的值';

console.log(a); // 我是變量a的值
console.log(b); // 我是變量b的值
複製代碼

上面代碼咱們聲明瞭變量a我是變量a的值,而後聲明變量b,並把變量a賦值給變量b,輸出,得出變量ab輸出的值是同樣的
而後咱們單獨更改了變量b的值,再輸出的時候發現變量ab輸出的值不同了,能夠證實他們兩個的值是單獨存在的,互相沒有聯繫,就算var b = a,也只是新增了一個變量b和值。模塊化

複雜數據類型(引用類型)

// 引用類型
var obj1 = {
  a: 'a',
  b: 'b'
}
var obj2 = obj1;

console.log(obj1); // { a: 'a', b: 'b' }
console.log(obj2); // { a: 'a', b: 'b' }

obj2.b = 'bb';

console.log(obj1); // { a: 'a', b: 'bb' }
console.log(obj2); // { a: 'a', b: 'bb' }
複製代碼

在上面的代碼中能發現,在引用類型直接的賦值,聲明變量obj1爲一個對象,而後讓講變量obj1賦值給obj2,這時候輸出的變量obj1和變量obj2的值是同樣的
當咱們更改了obj2.b的值之後,輸出結果發現obj1.b的值也跟着發生了變化
這就是咱們要說的,引用類型的賦值只是給了一個對內存中對象引用的一個指針,因此變量obj1和變量obj2是引用了同一個內存中的對象,因此當一個更改之後,另外一個也會跟着改變。函數


插播深克隆的形象圖片,我本身畫的,大概意思理解一下工具


深克隆

咱們要實現的深克隆,是一個徹底克隆出一個全新的對象在內存中

不完美深克隆——Object.assign()

Object.assign()方法用於將全部可枚舉屬性的值從一個或多個源對象複製到目標對象。它將返回目標對象。————————《MDN web docs》

Object.assign(target, ...sources)

這是MDN文檔中對Object.assign()這個方法的說明,其實就是這個方法能夠穿入兩個參數,第一個參數是目標函數,第二個參數是源對象,而後會把源對象的可枚舉到的屬性複製到目標函數中,而後返回目標對象,也就是更改了目標對象。舉個例子,先來了解一下這個方法怎麼用⬇️

// 聲明目標對象和源對象
const target = { a: 1, b: 2 };
const source = { b: 4, c: 5 };
// 將源對象上可枚舉的屬性複製到目標對象上,有相同鍵值的覆蓋
const returnedTarget = Object.assign(target, source);

console.log(source); // { b: 4, c: 5 }

console.log(target); // { a: 1, b: 4, c: 5 }

console.log(returnedTarget); // { a: 1, b: 4, c: 5 }
複製代碼

就目前的形勢,真的是把源對象的屬性複製到目標對象上了,貌似是能夠實現深克隆的,那咱們再來看下面的例子⬇️

// 聲明目標對象和源對象
const target = {};
const source = {
  a: 1,
  b: {
    ba: 'ba',
    bb: 'bb'
  },
  c: function () {
    console.log('c')
  }
};
// 將源對象上可枚舉的屬性複製到目標對象上,有相同鍵值的覆蓋
const returnedTarget = Object.assign(target, source);

console.log(target); // { a: 1, b: { ba: 'ba', bb: 'bb' }, c: [Function: c] }
console.log(source); // { a: 1, b: { ba: 'ba', bb: 'bb' }, c: [Function: c] }
console.log(returnedTarget); // { a: 1, b: { ba: 'ba', bb: 'bb' }, c: [Function: c] }

target.b.ba = 'ba2';

console.log(target); // { a: 1, b: { ba: 'ba2', bb: 'bb' }, c: [Function: c] }
console.log(source); // { a: 1, b: { ba: 'ba2', bb: 'bb' }, c: [Function: c] }
console.log(returnedTarget); // { a: 1, b: { ba: 'ba2', bb: 'bb' }, c: [Function: c] }
複製代碼

上面的代碼想表達的意思就是,當咱們不涉及到第二層或者更深層的包含複雜數據類型,可見Object.assign()方法是基本可行的,可是若是對象中包含另外一層Object或者Array這樣的引用類型,他們仍是保存的指針,而不是真的複製出一個新的Object或者Array
既然Object.assign()不是很完美,那咱們換個方法————序列化

不完美深克隆——JSON.stringify()JSON.parse()

序列化(Serialization)意味着將對象或某種其餘類型的數據結構轉換爲可存儲格式(例如,文件或者buffer)
在JavaScript中,你能夠經過調用JSON.stringify()函數將某個值序列化爲JSON格式的字符串。
CSS值能夠經過調用CSSStyleDeclaration.getPropertyValue()函數來序列化。
————————————《MDN web docs》

咱們如今說說一個最簡單的僞深克隆(不是官方詞語),能夠達到大部分功能,可是依然會有欠缺,這就是經過JSON.stringify()JSON.parse()方法對對象進行序列化和反序列化。依然是上面的例子⬇️

// 聲明源對象
let source = {
  a: 1,
  b: {
    ba: 'ba',
    bb: 'bb'
  },
  c: function () {
    console.log('c')
  }
};
// 經過序列化而後再反序列化源對象來賦值目標對象
let target = JSON.parse(JSON.stringify(source))

console.log(source); // { a: 1, b: { ba: 'ba', bb: 'bb' }, c: [Function: c] }
console.log(target); // { a: 1, b: { ba: 'ba', bb: 'bb' } }

target.b.ba = 'ba2';

console.log(source); // { a: 1, b: { ba: 'ba', bb: 'bb' }, c: [Function: c] }
console.log(target); // { a: 1, b: { ba: 'ba2', bb: 'bb' } }
複製代碼

上面經過序列化和反序列化的方法克隆了源對象到目標對象,不只第一層屬性同樣,第二層的對象也不單單是指向同一個對象,這看似完美的方法卻有幾點缺陷

  1. 他沒法實現對函數 、RegExp等特殊對象的克隆
  2. 會拋棄對象的constructor,全部的構造函數會指向Object
  3. 對象有循環引用,會報錯

如下例子借鑑《面試官:請你實現一個深克隆》 >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

// 構造函數
function person(pname) {
  this.name = pname;
}

const Messi = new person('Messi');

// 函數
function say() {
  console.log('hi');
};

const oldObj = {
  a: say,
  b: new Array(1),
  c: new RegExp('ab+c', 'i'),
  d: Messi
};

const newObj = JSON.parse(JSON.stringify(oldObj));

// 沒法複製函數
console.log(newObj.a, oldObj.a); // undefined [Function: say]
// 稀疏數組複製錯誤
console.log(newObj.b[0], oldObj.b[0]); // null undefined
// 沒法複製正則對象
console.log(newObj.c, oldObj.c); // {} /ab+c/i
// 構造函數指向錯誤
console.log(newObj.d.constructor, oldObj.d.constructor); // [Function: Object] [Function: person]
複製代碼

咱們能夠看到在對函數、正則對象、稀疏數組等對象克隆時會發生意外,構造函數指向也會發生錯誤。

const oldObj = {};

oldObj.a = oldObj;

const newObj = JSON.parse(JSON.stringify(oldObj));
console.log(newObj.a, oldObj.a); // TypeError: Converting circular structure to JSON
複製代碼

對象的循環引用會拋出錯誤.
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

實現深克隆(代碼有問題,僅供參考思路)

看來咱們想經過已有的方法是實現不了深克隆的,因此咱們須要本身手寫方法來實現深克隆,咱們要記住的思路就是兩點

  1. 判斷數據類型,分別處理
  2. 遞歸
/** * 常量 * @type {string} */
const TYPE_OBJECT = '[object Object]';
const TYPE_ARRAY = '[object Array]';
/** * 判斷對象的類型 * @param obj 來源對象 * @returns {string} 對象類型 */
function typeToString(obj) {
  return Object.prototype.toString.call(obj)
}

/** * 深克隆對象 * @param oldObj 源對象 * @returns {Object} 返回克隆後的對象 */
function deepClone(oldObj) {
  let newObj;
  if ( oldObj === null ) {
    return null
  }
  if ( typeof oldObj !== 'object') {
    return oldObj
  }
  switch (typeToString(oldObj)) {
    case TYPE_OBJECT:
      newObj = {}
      break;
    case TYPE_ARRAY:
      newObj = [];
      break;
  }
  for (let i in oldObj) {
    newObj[i] = deepClone(oldObj[i]);
  }
  return newObj
}
複製代碼

以上是我本身手寫實現的深克隆
請勿抄襲,寫的只是個小demo,不能用在生產環境,判斷的數據類型有限,而且沒有處理對象繼承的constructor指向問題,也沒有解決循環引用的問題
看一下大概思路就好了

最佳實踐————Lodash的_.cloneDeep

Lodash是什麼?Lodash是一個一致性、模塊化、高性能的JavaScript實用工具庫,實用方法請參考個人另外一篇博客
掘金:《lodash入門》 簡書:《lodash入門》
還有還有😝😝😝😝😝😝


閱讀量上萬了,開心~

使用

import _ from 'lodash';

var objects = [{ 'a': 1 }, { 'b': 2 }];
 
var deep = _.cloneDeep(objects);
console.log(deep[0] === objects[0]);
// => false
複製代碼

使用就是這麼使用,看了眼源碼是怎麼實現,能夠說lodash在深克隆方法的實現上真是全之又全,判斷的項目測20來項,不只有數據類型的判斷,還有浮點數的判斷,多少位的浮點數的判斷,反正就是不少判斷,以及邊界考慮。

OH MY GOD,用它!!

PS:這篇沒有加入動漫元素😢😢😢


我是前端戰五渣,一個前端界的小學生。

相關文章
相關標籤/搜索