在聊JavaScript(如下簡稱js)深度克隆以前,咱們先來了解一下js中對象的組成。
在 js
中一切實例皆是對象,具體分爲 原始類型 和 合成類型 :
原始類型 對象指的是 Undefined
、 Null
、Boolean
、Number
和 String
,按值傳遞。
合成類型 對象指的是 array
、 object
以及 function
,按址傳遞,傳遞的時候是內存中的地址。ios
克隆或者拷貝分爲2種: 淺度克隆 、 深度克隆 。
淺度克隆 :基本類型爲值傳遞,對象仍爲引用傳遞。
深度克隆 :全部元素或屬性均徹底克隆,並於原引用類型徹底獨立,即,在後面修改對象的屬性的時候,原對象不會被修改。正則表達式
又或許你剛據說「深度克隆」這個詞,簡單來講,就是說有個變量a,a的值是個對象(包括基本數據類型),如今你要建立一個變量b,使得它擁有跟a同樣的方法和屬性等等。可是a和b之間不能相互影響,即a的值的改變不影響b值的變化。直接賦值可好?segmentfault
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
類型的深度克隆,直接賦值彷佛並不該該是一種最好的方法(儘管實際應用中足矣)。babel
可是對象呢?數據結構
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/1250this
用一些代碼來測試下:
// 測試用例: 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); };
注:先不考慮 對象了原理都是同樣MapSetArguments[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
參考連接: