淺談javascript深拷貝和淺拷貝

深淺拷貝在我們平常開發中也會經常用到,下來我們來具體談談深淺拷貝。

堆和棧的區別

其實深拷貝和淺拷貝的主要區別就在於其內存中的存儲類型不同。堆和棧都是內存中劃分出來用來存儲的區域。

  • 棧:是自動分配的內存空間,它由系統自動釋放;
  • 堆:是動態分配的內存,大小不定也不會自動釋放;

在深淺拷貝學習之前,先看下ECMAScript中的數據類型。主要分爲基本數據類型和引用數據類型。

  • 基本數據類型(存放在棧中):undefined,boolean,number,string,null

存放在棧中的數據大小確定,內存空間大小可以分配,是直接按值存放的,可直接訪問。

基本數據類型值不可變

javascript中的基本數據類型(undefined、null、布爾值、數字和字符串)與對象(包括數組和函數)有着根本區別。原始值是不可更改的:任何方法都無法更改(或“突變”)一個原始值。對數字和布爾值來說顯然如此 —— 改變數字的值本身就說不通,而對字符串來說就不那麼明顯了,因爲字符串看起來像由字符組成的數組,我們期望可以通過指定索引來假改字符串中的字符。實際上,javascript 是禁止這樣做的。字符串中所有的方法看上去返回了一個修改後的字符串,實際上返回的是一個新的字符串值。

基本數據類型的值是不可變的,動態修改了基本數據類型的值,它的原始值也是不會改變的,如:

    var str = "abc";
    console.log(str[1]="l");    // l
    console.log(str);           // abc

基本類型的比較是值的比較

基本類型的比較是值的比較,只要它們的值相等就認爲他們是相等的,如:

    var a = 1;
    var b = 1;
    console.log(a === b);//true

比較的時候最好使用嚴格等,因爲 == 是會進行類型轉換的,比如:

    var a = 1;
    var b = true;
    console.log(a == b);//true
  • 引用數據類型:是存放在堆內存中的,變量實際上是一個存放在棧內存的指針,這個指針指向堆內存中的地址。每個空間大小不一樣,如:
var p1 = {name:'qiao'};
var p2 = {name:'zhang'};
var p3 = {name:'wang'};

在這裏插入圖片描述

引用類型值可變

引用類型是可以直接改變其本身的值的。

    var a = [1,2,3];
    a[1] = 5;
    console.log(a[1]); // 5
	console.log(a); // [1,5,3]

引用類型的比較是引用的比較

所以每次我們對js中的引用類型進行操作的時候,都是操作其對象的引用(保存在棧內存中的指針),所以比較兩個引用類型,是看其的引用是否指向同一個對象。如:

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

雖然變量a和變量b都是表示 1,2,3的數組,但是其在內存中的位置不一樣,即變量a和變量b指向的不是同一個對象,所以他們是不相等的。

在這裏插入圖片描述

傳值與傳址

通過上述瞭解基本數據類型與引用類型的區別之後,我們明白傳值與傳址的區別了。

  • 基本數據類型傳值

在我們進行賦值操作的時候,基本數據類型的賦值是在內存中新開闢一段棧內存,然後再將值賦值到新的棧中。例如:

var a = 10;
var b = a;
a ++ ;
console.log(a); // 11
console.log(b); // 10

在這裏插入圖片描述

即基本類型的賦值的兩個變量是兩個獨立相互不影響的變量。

  • 引用類型傳址

引用類型的賦值是傳址。即只是改變指針的指向,也就是說引用類型的賦值是對象保存在棧中的地址的賦值,這樣的話兩個變量就指向同一個對象,因此兩者之間操作互相有影響。如:

var a = {};
var b = a;
a.name = 'qiao';
console.log(a.name); // 'qiao'
console.log(b.name); // 'qiao'
b.age = 26;
console.log(b.age);// 26
console.log(a.age);// 26
console.log(a == b);// true

在這裏插入圖片描述

深淺拷貝初步認識

我們知道javascript中一般有按值傳遞和按引用傳遞兩種複製方式:

  • 按值傳遞的是基本數據類型

Number,String,Boolean,Null,Undefined,Symbol,一般存放於內存中的棧區,存取速度快,存放量小;

  • 按引用傳遞的是引用類型

Object,Array,Function,一般存放與內存中的堆區,存取速度慢,存放量大,其引用指針存於棧區,並指向引用本身。

我們經常說的深淺拷貝是針對引用類型來說的:

  • 淺拷貝:指兩個js 對象指向同一個內存地址,其中一個改變會影響另一個;
  • 深拷貝:指複製後的新對象重新指向一個新的內存地址,兩個對象改變互不影響。

深淺拷貝的區別

根本的區別在於是否是真正獲取了一個對象的複製實體,而不是引用, 深拷貝在計算機中開闢了一塊內存地址用於存放複製的對象,而淺拷貝僅僅是指向被拷貝的內存地址,如果原地址中對象被改變了,那麼淺拷貝出來的對象也會相應改變。

淺拷貝

淺拷貝常用的方法如下:
  1. 簡單的直接賦值操作
var arr = [1, 2, 3];
var newArr = arr;
newArr[0] = "11";
console.log(arr);    //  ["11", 2, 3]
console.log(newArr);    //  ["11", 2, 3]
console.log(arr==newArr);   //  true
console.log(arr===newArr);  //  true
  1. Object.assign()方法是ES6的新函數,可以把任意多個的源對象自身的可枚舉屬性拷貝給目標對象,然後返回目標對象。拷貝的是對象的屬性的引用,而不是對象本身,但是也可以實現一層深拷貝
var obj = { a: {a: "js"}, b: 22 };
var newObj = Object.assign({}, obj);
newObj.a.a = "hello js";
console.log(obj);    //  { a: {a: "hello js"}, b: 22 };
console.log(newObj);    //  { a: {a: "hello js"}, b: 22 };
console.log(obj.a.a==newObj.a.a);   //  true
console.log(obj.a.a===newObj.a.a);  //  true
  1. $.extend({},obj)使用遞歸思路實現了淺拷貝和深拷貝,第一個參數類型爲Boolean,當爲false的時候必須省略不寫則是淺拷貝,當爲true的時候爲深拷貝:
var obj = { a: {a: "js"}, b: 22 };
var newObj = $.extend({}, obj);
newObj.a.a = "hello js";
console.log(obj);    //  { a: {a: "hello js"}, b: 22 };
console.log(newObj);    //  { a: {a: "hello js"}, b: 22 };
console.log(obj.a.a==newObj.a.a);   //  true
console.log(obj.a.a===newObj.a.a);  //  true
  1. 自己動手實現一個淺拷貝
var shallowCopy = function(obj) {
    // 只拷貝對象
    if (typeof obj !== 'object') return;
    // 根據obj的類型判斷是新建一個數組還是對象
    var newObj = obj instanceof Array ? [] : {};
    // 遍歷obj,並且判斷是obj的屬性才拷貝
    for (var key in obj) {
        if (obj.hasOwnProperty(key)) {
            newObj[key] = obj[key];
        }
    }
    return newObj;
}

深拷貝

簡單深拷貝常用的方法如下(一維的數據結構):

  1. 手動的賦值操作:
var obj = { a: 1, b: 2};
var newObj = { a: obj.a, b: obj.b};
newObj.b = 111;
console.log(obj);    // { a: 1, b: 2 }
console.log(newObj);    //  { a: 1, b: 111 }
console.log(obj == newObj);    //  false
console.log(obj === newObj);    //  false
  1. Object.assign()方法是ES6的新函數,只能簡單的複製一層屬性到目標對象,還得考慮兼容性
var obj = { a: {a: "js"}, b: 22 };
var newObj = Object.assign({}, obj);
newObj.b = 222;
newObj.a.a = 'javascript';
console.log(obj);    //  { a: { a: 'javascript' }, b: 22 }
console.log(newObj);    //  { a: { a: 'javascript' }, b: 222 }
console.log(obj==newObj);   //  false
console.log(obj===newObj);  //  false

複雜深拷貝常用的方法如下(二維的數據結構及以上):

  1. JSON.parse(JSON.stringify(obj))是最簡單粗暴的深拷貝,能夠處理JSON格式的所有數據類型,但是對於正則表達式類型、函數類型等無法進行深拷貝,而且會直接丟失相應的值,還有就是它會拋棄對象的constructor。也就是深拷貝之後,不管這個對象原來的構造函數是什麼,在深拷貝之後都會變成Object。同時如果對象中存在循環引用的情況也無法正確處理
var obj = { a: {a: "js"}, b: 33 };
var newObj = JSON.parse(JSON.stringify(obj));
newObj.b = 333;
newObj.a.a = "hello js";
console.log(obj); // { a: { a: 'js' }, b: 33 }
console.log(newObj); // { a: { a: 'hello js' }, b: 333 }
console.log(obj==newObj); // false
console.log(obj===newObj); // false
  1. $.extend(true,{},obj)使用遞歸思路可以實現深拷貝,要求第一個參數必須爲true:
var obj = { a: {a: "js"}, b: 22 };
var newObj = $.extend(true, {}, obj);
newObj.a.a = "hello js";
console.log(obj);    //  { a: "js", b: 22 };
console.log(newObj);    //  { a: "hello js", b: 22 };
console.log(obj==newObj);   //  false
console.log(obj===newObj);  //  false
  1. lodash中的_.clone(obj, true)等價於_.cloneDeep(obj) 兩個方法,lodash花了大量的代碼來實現ES6引入的大量新的標準對象,並針對存在環的對象的處理也是非常出色的,因此對於深拷貝來說lodash和其他庫相比最友好:
var obj = { a: {a: "js"}, b: 22 };
var newObj = _.cloneDeep(obj);
newObj.a.a = "hello js";
console.log(obj);    //  { a: "js", b: 22 };
console.log(newObj);    //  { a: "hello js", b: 22 };
console.log(obj==newObj);   //  false
console.log(obj===newObj);  //  false
  1. 自己實現一個簡單的深拷貝deepCopyTwo(),即用遞歸去複製所有層級屬性:
function deepCopyTwo(obj) {
    let objClone = Array.isArray(obj) ? [] : {};
    if (obj && typeof obj == 'object') {
        for (const key in obj) {
            //判斷obj子元素是否爲對象,如果是,遞歸複製
            if (obj[key] && typeof obj[key] === "object") {
                objClone[key] = deepCopyTwo(obj[key]);
            } else {
                //如果不是,簡單複製
                objClone[key] = obj[key];
            }
        }
    }
    return objClone;
}
let obj = {a: 11, b: function(){}, c: {d: 22}};
let f = deepCopyTwo(obj);
console.log(f); // { a: 11, b: [Function: b], c: { d: 22 } }

深拷貝最常用的就是上述這些方法,當然還有其他的一些庫,比如deepCopy等,數組常用contact和slice來實現深拷貝,不同的方法有其最好的適用環境。
下面我們用數據具體分析一下一維數據結構和二維數據結構在不同方法下的性能對比。

深拷貝不同方法的性能對比

深拷貝一維數據結構方法對比:

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());

數組的淺拷貝

如果是數組,可以用數組的一些方法如:slice、concat 返回一個新數組的特性來實現淺拷貝。

  1. concat實現淺拷貝
var arr = ['old', 1, true, null, undefined];
var newArr = arr.concat();
newArr[0] = 'new';
console.log(arr) // ["old", 1, true, null, undefined]
console.log(newArr) // ["new", 1, true, null, undefined]
  1. slice實現淺拷貝
var arr = ['old', 1, true, null, undefined];
var newArr = arr.slice();
newArr[0] = 'new';
console.log(arr) // ["old", 1, true, null, undefined]
console.log(newArr) // ["new", 1, true, null, undefined]

但是如果數組嵌套了對象或者數組的話,比如:

var arr = [{old: 'js'}, ['js']];
var new_arr = arr.concat();
arr[0].old = 'jss';
arr[1][0] = 'jss';
console.log(arr) // [ { old: 'jss' }, [ 'jss' ] ]
console.log(new_arr) // [ { old: 'jss' }, [ 'jss' ] ]

從上述代碼中發現,新老數組都發生了變化,即如果數組元素是基本類型,拷貝一份,互不影響,而如果是對象或數組,就會只拷貝對象和數組的引用,即無論在新舊數組進行修改,兩者都會發生變化。我們把這種複製引用的拷貝方法稱之爲淺拷貝,與之對應的就是深拷貝,即concat和slice只能實現一層數據的拷貝,即淺拷貝

數組的深拷貝

如何深拷貝一個數組?不僅適用於數組還適用於對象!

  1. JSON.parse( JSON.stringify(params)
var arr = ['js', 1, true, ['js1', 'js2'], {js: 1}]
var newArr = JSON.parse( JSON.stringify(arr) );
console.log(newArr); // [ 'js', 1, true, [ 'js1', 'js2' ], { js: 1 } ]

總結:

  • 一維數據結構的深拷貝方法建議使用:Object.assign();
  • 二維數據結構及以上的深拷貝方法建議使用:JSON.parse(JSON.stringify());
  • 特別複雜的數據結構的深拷貝方法建議使用:Loadsh.cloneDeep();
  • 用一張表說明下賦值、深拷貝、淺拷貝的區別
和原數據是否指向同一對象 第一層數據爲基本數據類型 原數據中包含子對象
賦值 改變會使原數據一同改變 改變會使原數據一同改變
淺拷貝 改變不會使原數據一同改變 改變會使原數據一同改變
賦值 改變不會使原數據一同改變 改變不會使原數據一同改變
  • 只有在對象裏有嵌套對象的情況下,纔會根據需求討論是要進行深拷貝還是淺拷貝
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章