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

前端戰五渣發表於2019-04-03

直接進入正題

JavaScript資料型別

5種簡單資料型別(也稱為基本資料型別):Undefined、Null、Boolean、Number和String;

1種複雜資料型別:Object;

基本資料型別(5種簡單資料型別):直接儲存在棧(stack)中的資料

引用型別(複雜資料型別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是引用了同一個記憶體中的物件,所以當一個更改以後,另一個也會跟著改變。


插播深克隆的形象圖片,我自己畫的,大概意思理解一下

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


深克隆

我們要實現的深克隆,是一個完全克隆出一個全新的物件在記憶體中

不完美深克隆——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入門》
還有還有??????

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

閱讀量上萬了,開心~

使用

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:這篇沒有加入動漫元素???


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

相關文章