1. 認識深拷貝和淺拷貝
javascript中一般有按值傳遞和按引用傳遞兩種復制,按值傳遞的是基本數據類型(Number,String,Boolean,Null,Undefined),一般存放於內存中的棧區,存取速度快,存放量小;按引用傳遞的是引用類型(Object,Array,Function,Symbol),一般存放與內存中的堆區,存取速度慢,存放量大,其引用指針存於棧區,並指向引用本身。
深拷貝和淺拷貝是相對於引用類型而言的:
淺拷貝: 指兩個js 對象指向同一個內存地址,其中一個改變會影響另一個;
深拷貝: 指復制后的新對象重新指向一個新的內存地址,兩個對象改變互不影響。
2. 淺拷貝
淺拷貝常用的方法如下:
1. 簡單的賦值操作:
var arr = [1,2,3]; var newarr = arr; newarr[0] = "one"; console.log(arr); // ["one", 2, 3] console.log(newarr); // ["one", 2, 3] console.log(arr==newarr); // true console.log(arr===newarr); // true
2. Object.assign()方法是ES6的新函數,
可以把任意多個的源對象自身的可枚舉屬性拷貝給目標對象,然后返回目標對象。拷貝的是對象的屬性的引用,而不是對象本身,但是也可以實現一層深拷貝:
var obj = { a: {a: "hello"}, b: 33 }; var newObj = Object.assign({}, obj); newObj.a.a = "hello world"; console.log(obj); // { a: {a: "hello world"}, b: 33 }; console.log(newObj); // { a: {a: "hello world"}, b: 33 }; console.log(obj.a.a==newObj.a.a); // true console.log(obj.a.a===newObj.a.a); // true
3.$.extend({},obj)使用遞歸思路實現了淺拷貝和深拷貝,第一個參數類型為Boolean,當為false的時候必須省略不寫則是淺拷貝,當為true的時候為深拷貝:
var obj = { a: {a: "hello"}, b: 33 }; var newObj = $.extend({}, obj); newObj.a.a = "hello world"; console.log(obj); // { a: {a: "hello world"}, b: 33 }; console.log(newObj); // { a: {a: "hello world"}, b: 33 }; console.log(obj.a.a==newObj.a.a); // true console.log(obj.a.a===newObj.a.a); // true
淺拷貝是我們經常使用的操作一些對象或數組的有效方法,具體的使用需要結合實際場景來合理的使用,還要考慮一些兼容性的問題,但是在大多實際情景下我們需要使用更多的是深拷貝,尤其是在各種MVVM框架中,引入了狀態管理,更多的體現在數據流中希望我們不改變原對象,這樣的話實現深拷貝就顯得尤為重要。
3. 深拷貝
簡單深拷貝常用的方法如下(一維的數據結構):
1.手動的賦值操作:
var obj = { a: 10, b: 20}; var newObj = { a: obj.a, b: obj.b}; newObj.b = 100; console.log(obj); // { a: 10, b: 20} console.log(newObj); // { a: 10, b: 100}; console.log(obj == newObj); // false console.log(obj === newObj); // false
2.Object.assign()方法是ES6的新函數,只能簡單的復制一層屬性到目標對象,還得考慮兼容性:
var obj = { a: {a: "hello"}, b: 33 }; var newObj = Object.assign({}, obj); newObj.b = 100; console.log(obj); // { a: "hello", b: 33 }; console.log(newObj); // { a: "hello", b: 100 }; console.log(obj==newObj); // false console.log(obj===newObj); // false
復雜深拷貝常用的方法如下(二維的數據結構及以上):
1.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
2.$.extend(true,{},obj)使用遞歸思路可以實現深拷貝,要求第一個參數必須為true:
var obj = { a: {a: "hello"}, b: 33 }; var newObj = $.extend(true, {}, obj); newObj.a.a = "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
3. lodash中的_.clone(obj, true)等價於_.cloneDeep(obj) 兩個方法,lodash花了大量的代碼來實現 ES6 引入的大量新的標准對象,並針對存在環的對象的處理也是非常出色的,因此對於深拷貝來說lodash和其他庫相比最友好:
var obj = { a: {a: "hello"}, b: 33 }; var newObj = _.cloneDeep(obj); newObj.a.a = "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
4. 自己實現一個簡單的深拷貝deepClone():
function deepClone(obj){ if(typeof obj !== "object") return; let newObj = obj instanceof Array ? [] : {}; for(let key in obj){ if(obj.hasOwnProperty(key)){ newObj[key] = typeof obj[key] === "object" ? deepClone(obj[key]) : obj[key]; } } return newObj; } let obj = {a: 11, b: function(){}, c: {d: 22}}; deepClone(obj); // {a: 11, b: f(), c: {d: 22}};
對於深拷貝來說最常用的就是這些方法,當然還有其他的一些庫,比如deepCopy等,此外數組常用contact和slice來實現深拷貝,不同的方法有其最好的適用環境,還是那句話:"離開場景談性能就是耍流氓",下面用數據具體分析一下一維數據結構和二維數據結構在不同方法下的性能對比。
4.深拷貝不同方法的性能對比
深拷貝一維數據結構用時對比:
var obj = []; for (var i = 0; i < 100; i++) { obj[i] = Math.random(); } console.time("assign"); var newObj = Object.assign({}, obj); console.timeEnd("assign"); console.time("JSON.parse(JSON.stringify())"); var newObj = JSON.parse(JSON.stringify(obj)); console.timeEnd("JSON.parse(JSON.stringify())"); console.time("$.extend"); var newObj = $.extend(true, {}, obj); console.timeEnd("$.extend"); console.time("Loadsh.cloneDeep"); var newObj = _.cloneDeep(obj); console.timeEnd("Loadsh.cloneDeep");
經過多次實驗分析發現,一維數據結構的深拷貝方法性能最佳的為Object.assign();
深拷貝二維數據結構用時對比:
var obj = []; for (var i = 0; i < 100; i++) { obj[i] = {}; for (var j = 0; j < 100; j++) { obj[i][j] = Math.random(); } } console.time("JSON.parse(JSON.stringify())"); var newObj = JSON.parse(JSON.stringify(obj)); console.timeEnd("JSON.parse(JSON.stringify())"); console.time("$.extend"); var newObj = $.extend(true, {}, obj); console.timeEnd("$.extend"); console.time("Loadsh.cloneDeep"); var newObj = _.cloneDeep(obj); console.timeEnd("Loadsh.cloneDeep");
經過多次實驗分析發現,二維數據結構的深拷貝方法性能最佳的為JSON.parse(JSON.stringify());
5.總結
一維數據結構的深拷貝方法建議使用:Object.assign();
二維數據結構及以上的深拷貝方法建議使用:JSON.parse(JSON.stringify());
特別復雜的數據結構的深拷貝方法建議使用:Loadsh.cloneDeep();