在聊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 in
, Object.entries
, Object.values
, Object.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
參考鏈接: