js對象的深度克隆

在聊JavaScript(以下簡稱js)深度克隆之前,我們先來了解一下js中對象的組成。
在 js 中一切實例皆是對象,具體分爲 原始類型 和 合成類型 :
原始類型 對象指的是 Undefined 、 Null 、Boolean 、Number 和 String ,按值傳遞。
合成類型 對象指的是 array 、 object 以及 function ,按址傳遞,傳遞的時候是內存中的地址。

克隆或者拷貝分爲2種: 淺度克隆 、 深度克隆 。
淺度克隆 :基本類型爲值傳遞,對象仍爲引用傳遞。
深度克隆 :所有元素或屬性均完全克隆,並於原引用類型完全獨立,即,在後面修改對象的屬性的時候,原對象不會被修改。

又或許你剛聽說“深度克隆”這個詞,簡單來說,就是說有個變量a,a的值是個對象(包括基本數據類型),現在你要創建一個變量b,使得它擁有跟a一樣的方法和屬性等等。但是a和b之間不能相互影響,即a的值的改變不影響b值的變化。直接賦值可好?

複製代碼

var a = 1;
var b = a;
a = 10;
console.log(b);            // 1
 
var a = 'hello';
var b = a;
a = 'world';
console.log(b);            // hello
 
var a = true;
var b = a;
a = false;
console.log(b);            // true

複製代碼

實踐證明某些 JavaScript 的原始數據類型,如果要克隆直接賦值即可。
關於 function 的深度複製:查閱了一些資料, function 的深度複製似乎和原始數據類型的深度複製一樣。

複製代碼

var a = function () {
    console.log(1);
};
var b = a;
a = function () {
    console.log(2);
};
b(); 

複製代碼

本來我也是這麼認爲的,直到文章下出現了評論。思考後我覺得 function 和普通的對象一樣,只是我們在平常應用中習慣了整體的重新賦值,導致它在深度複製中的表現和原始類型一致:

複製代碼

var a = function () {
    console.log(1);
};
a.tmp = 10;
var b = a;
a.tmp = 20;
console.log(b.tmp);        // 20

複製代碼

於是乎對於 function 類型的深度克隆,直接賦值似乎並不應該是一種最好的方法(儘管實際應用中足矣)。

但是對象呢?

var a = [0,1,2,3];
var b = a;
a.push(4);
console.log(b);            // [0, 1, 2, 3, 4]

顯然與預期不符,爲什麼會這樣?因爲原始數據類型儲存的是對象的實際數據,而對象類型存儲的是對象的引用地址。上面的例子呢也就是說a和b對象引用了同一個地址,無論改變a還是改變b,其實根本操作是一樣的,都是對那塊空間地址中的值的改變。

於是我們知道了,對於基本的對象來說,不能只能用 “ = ” 賦值,思索後寫下如下代碼:

複製代碼

// 判斷arr是否爲一個數組,返回一個bool值
function isArray (arr) {
    return Object.prototype.toString.call(arr) === '[object Array]';  
}
// 深度克隆
function deepClone (obj) {  
    if(typeof obj !== "object" && typeof obj !== 'function') {
        return obj;        //原始類型直接返回
    }
    var o = isArray(obj) ? [] : {}; 
    for(i in obj) {  
        if(obj.hasOwnProperty(i)){ 
            o[i] = typeof obj[i] === "object" ? deepClone(obj[i]) : obj[i]; 
        } 
    } 
    return o;
}

複製代碼

注意代碼中判斷數組的時候用的不是 obj instanceof Array ,這是因爲該方法存在一些小問題,詳情見http://www.nowamagic.net/librarys/veda/detail/1250

用一些代碼來測試下:

複製代碼

// 測試用例:
var srcObj = {
    a: 1,
    b: {
        b1: ["hello", "hi"],
        b2: "JavaScript"
    }
};
var abObj = srcObj;
var tarObj = cloneObject(srcObj);

srcObj.a = 2;
srcObj.b.b1[0] = "Hello";

console.log(abObj.a);
console.log(abObj.b.b1[0]);

console.log(tarObj.a);      // 1
console.log(tarObj.b.b1[0]);    // "hello"

複製代碼

 

對於上面的方法再進行測試下,如下:

這個沒有區分具體的對象,在此問下大家js的對象有哪些呢?相信一般人答不出來4個
[object Object][object Array][object Null][object RegExp][object Date][object HTMLXXElement][object Map],[object Set],... 等等一系列

檢測類型使用 Object.prototype.toString.call(xxx) 和 typeof

我們分析下上面對象中哪些是引用類型需要特殊處理呢?相信大家都不陌生了。[object Object] 和 [object Array]

好!詳細大家思路有了,咋們用遞歸來實現一把吧!

 

複製代碼

const deepClone = function(obj) {
  // 先檢測是不是數組和Object
  // let isMap = Object.prototype.toString.call(obj) === '[object Map];
  // let isSet = Object.prototype.toString.call(obj) === '[object Set];
  // let isArr = Object.prototype.toString.call(obj) === '[object Array]';
  let isArr = Array.isArray(obj);
  let isJson = Object.prototype.toString.call(obj) === '[object Object]';
  if (isArr) {
    // 克隆數組
    let newObj = [];
    for (let i = 0; i < obj.length; i++) {
      newObj[i] = deepClone(obj[i]);
    }
    return newObj;
  } else if (isJson) {
    // 克隆Object
    let newObj = {};
    for (let i in obj) {
      newObj[i] = deepClone(obj[i]);
    }
    return newObj;
  }
  // 不是引用類型直接返回
  return obj;
};

Object.prototype.deepClone = function() {
  return deepClone(this);
};

注:先不考慮Map Set Arguments [object XXArrayBuffer] 對象了原理都是一樣

複製代碼

各種情況分析完了才說算是真克隆
我們在控制檯看下

    • 注意先要把方法在控制檯輸進去,在調試

 

是不是解決了? 在此並沒有結束。 專注的夥伴們相信發現了對象中包含了個 deepClone 方法,具體細節我們在此就不多說了,我們給 Object 添加了個 Object.prototype.deepClone方法導致了每個對象都有了此方法。

原則上我們不允許在原型鏈上添加方法的,因爲在循環中 for inObject.entriesObject.valuesObject.keys等方法會出現自定義的方法。

相信熟悉 Object 文檔的夥伴人已經知道解決方案了,

Object.defineProperty 這個方法給大家帶來了福音 具體參考 Object 文檔。我們使用一個enumerable (不可枚舉)屬性就可以解決了。

在原來基礎上添加以下代碼即可。

Object.defineProperty(Object.prototype, 'deepClone', {enumerable: false});

再看控制檯

同樣上面方法中也是無法克隆一個不可枚舉的屬性。

完整代碼如下:

複製代碼

const deepClone = function(obj) {
  // 先檢測是不是數組和Object
  // let isArr = Object.prototype.toString.call(obj) === '[object Array]';
  let isArr = Array.isArray(obj);
  let isJson = Object.prototype.toString.call(obj) === '[object Object]';
  if (isArr) {
    // 克隆數組
    let newObj = [];
    for (let i = 0; i < obj.length; i++) {
      newObj[i] = deepClone(obj[i]);
    }
    return newObj;
  } else if (isJson) {
    // 克隆Object
    let newObj = {};
    for (let i in obj) {
      newObj[i] = deepClone(obj[i]);
    }
    return newObj;
  }
  // 不是引用類型直接返回
  return obj;
};

Object.prototype.deepClone = function() {
  return deepClone(this);
};
Object.defineProperty(Object.prototype, 'deepClone', {enumerable: false});

複製代碼

注: 爲了兼容低版本瀏覽器需要藉助 babel-polyfill;

 附: 其他深拷貝方式選擇:https://blog.csdn.net/ios99999/article/details/77646594

一維數據結構的深拷貝方法建議使用:Object.assign();

二維數據結構及以上的深拷貝方法建議使用:JSON.parse(JSON.stringify());

特別複雜的數據結構的深拷貝方法建議使用:Loadsh.cloneDeep();

 

JSON.parse(JSON.stringify(obj))是最簡單粗暴的深拷貝,能夠處理JSON格式的所有數據類型,但是對於正則表達式類型、函數類型等無法進行深拷貝,而且會直接丟失相應的值,還有就是它會拋棄對象的constructor。也就是深拷貝之後,不管這個對象原來的構造函數是什麼,在深拷貝之後都會變成Object。同時如果對象中存在循環引用的情況也無法正確處理:

複製代碼

var obj = { a: {a: "hello"}, b: 33 };
var newObj = JSON.parse(JSON.stringify(obj));
newObj.b = "hello world";
console.log(obj);    //  { a: "hello", b: 33 };
console.log(newObj);    //  { a: "hello world", b: 33};
console.log(obj==newObj);  //  false
console.log(obj===newObj);  //  false

複製代碼

 

參考鏈接:

https://segmentfault.com/a/1190000014336441

https://segmentfault.com/a/1190000014336441

 

轉載自:https://www.cnblogs.com/momo798/p/9235128.html

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