1. 程式人生 > 其它 >js 深拷貝 vs 淺拷貝

js 深拷貝 vs 淺拷貝

js 深拷貝 vs 淺拷貝


 

本文主要講一下 js 的基本資料型別以及一些堆和棧的知識和什麼是深拷貝、什麼是淺拷貝、深拷貝與淺拷貝的區別,以及怎麼進行深拷貝和怎麼進行淺拷貝。

本文思維導圖如下:

本文思維導圖

 

本文首發於我的個人部落格:cherryblog.site/

堆和棧的區別

其實深拷貝和淺拷貝的主要區別就是其在記憶體中的儲存型別不同。

堆和棧都是記憶體中劃分出來用來儲存的區域。

棧(stack)為自動分配的記憶體空間,它由系統自動釋放;而堆(heap)則是動態分配的記憶體,大小不定也不會自動釋放。

ECMAScript 的資料型別

在將深拷貝和淺拷貝之前,我們先來重新回顧一下 ECMAScript 中的資料型別。主要分為

基本資料型別(undefined,boolean,number,string,null

基本資料型別主要是:undefined,boolean,number,string,null

基本資料型別存放在棧中

存放在棧記憶體中的簡單資料段,資料大小確定,記憶體空間大小可以分配,是直接按值存放的,所以可以直接訪問。

基本資料型別值不可變

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

基本資料型別的值是不可變的,動態修改了基本資料型別的值,它的原始值也是不會改變的,例如:

    var str = "abc";

    console.log(str[1]="f");    // f

    console.log(str);           // abc複製程式碼

這一點其實開始我是比較迷惑的,總是感覺 js 是一個靈活的語言,任何值應該都是可變的,真是圖樣圖森破,我們通常情況下都是對一個變數重新賦值,而不是改變基本資料型別的值。就如上述引用所說的那樣,在 js 中沒有方法是可以改變布林值和數字的。倒是有很多操作字串的方法,但是這些方法都是返回一個新的字串,並沒有改變其原有的資料。

所以,記住這一點:基本資料型別值不可變。

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

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

    var a = 1;
    var b = 1;
    console.log(a === b);//true複製程式碼

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

    var a = 1;
    var b = true;
    console.log(a == b);//true複製程式碼

引用型別

引用型別存放在堆中

引用型別(object)是存放在堆記憶體中的,變數實際上是一個存放在棧記憶體的指標,這個指標指向堆記憶體中的地址。每個空間大小不一樣,要根據情況開進行特定的分配,例如。

var person1 = {name:'jozo'};
var person2 = {name:'xiaom'};
var person3 = {name:'xiaoq'};複製程式碼

 

堆記憶體

 

引用型別值可變

引用型別是可以直接改變其值的,例如:

    var a = [1,2,3];
    a[1] = 5;
    console.log(a[1]); // 5複製程式碼

引用型別的比較是引用的比較

所以每次我們對 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 = {}; // a儲存了一個空物件的例項
var b = a;  // a和b都指向了這個空物件

a.name = 'jozo';
console.log(a.name); // 'jozo'
console.log(b.name); // 'jozo'

b.age = 22;
console.log(b.age);// 22
console.log(a.age);// 22

console.log(a == b);// true複製程式碼

 

引用型別的賦值

 

淺拷貝

在深入瞭解之前,我認為上面的賦值就是淺拷貝,哇哈哈,真的是圖樣圖森破。上面那個應該只能算是“引用”,並不算是真正的淺拷貝。
一下部分參照知乎中的提問: javascript中的深拷貝和淺拷貝

賦值(=)和淺拷貝的區別

那麼賦值和淺拷貝有什麼區別呢,我們看下面這個例子:

    var obj1 = {
        'name' : 'zhangsan',
        'age' :  '18',
        'language' : [1,[2,3],[4,5]],
    };

    var obj2 = obj1;


    var obj3 = shallowCopy(obj1);
    function shallowCopy(src) {
        var dst = {};
        for (var prop in src) {
            if (src.hasOwnProperty(prop)) {
                dst[prop] = src[prop];
            }
        }
        return dst;
    }

    obj2.name = "lisi";
    obj3.age = "20";

    obj2.language[1] = ["二","三"];
    obj3.language[2] = ["四","五"];

    console.log(obj1);  
    //obj1 = {
    //    'name' : 'lisi',
    //    'age' :  '18',
    //    'language' : [1,["二","三"],["四","五"]],
    //};

    console.log(obj2);
    //obj2 = {
    //    'name' : 'lisi',
    //    'age' :  '18',
    //    'language' : [1,["二","三"],["四","五"]],
    //};

    console.log(obj3);
    //obj3 = {
    //    'name' : 'zhangsan',
    //    'age' :  '20',
    //    'language' : [1,["二","三"],["四","五"]],
    //};複製程式碼

先定義個一個原始的物件 obj1,然後使用賦值得到第二個物件 obj2,然後通過淺拷貝,將 obj1 裡面的屬性都賦值到 obj3 中。也就是說:

  • obj1:原始資料
  • obj2:賦值操作得到
  • obj3:淺拷貝得到

然後我們改變 obj2 的 name 屬性和 obj3 的 name 屬性,可以看到,改變賦值得到的物件 obj2 同時也會改變原始值 obj1,而改變淺拷貝得到的的 obj3 則不會改變原始物件 obj1。這就可以說明賦值得到的物件 obj2 只是將指標改變,其引用的仍然是同一個物件,而淺拷貝得到的的 obj3 則是重新建立了新物件。

然而,我們接下來來看一下改變引用型別會是什麼情況呢,我又改變了賦值得到的物件 obj2 和淺拷貝得到的 obj3 中的 language 屬性的第二個值和第三個值(language 是一個數組,也就是引用型別)。結果見輸出,可以看出來,無論是修改賦值得到的物件 obj2 和淺拷貝得到的 obj3 都會改變原始資料。

這是因為淺拷貝只複製一層物件的屬性,並不包括物件裡面的為引用型別的資料。所以就會出現改變淺拷貝得到的 obj3 中的引用型別時,會使原始資料得到改變。

深拷貝:將 B 物件拷貝到 A 物件中,包括 B 裡面的子物件,

淺拷貝:將 B 物件拷貝到 A 物件中,但不包括 B 裡面的子物件

-- 和原資料是否指向同一物件 第一層資料為基本資料型別 原資料中包含子物件
賦值 改變會使原資料一同改變 改變會使原資料一同改變
淺拷貝 改變不會使原資料一同改變 改變會使原資料一同改變
深拷貝 改變不會使原資料一同改變 改變不會使原資料一同改變

深拷貝

看了這麼半天,你也應該清楚什麼是深拷貝了吧,如果還不清楚,我就剖腹自盡(ಥ_ಥ)

深拷貝是對物件以及物件的所有子物件進行拷貝。

那麼問題來了,怎麼進行深拷貝呢?

思路就是遞迴呼叫剛剛的淺拷貝,把所有屬於物件的屬性型別都遍歷賦給另一個物件即可。我們直接來看一下 Zepto 中深拷貝的程式碼:

    // 內部方法:使用者合併一個或多個物件到第一個物件
    // 引數:
    // target 目標物件  物件都合併到target裡
    // source 合併物件
    // deep 是否執行深度合併
    function extend(target, source, deep) {
        for (key in source)
            if (deep && (isPlainObject(source[key]) || isArray(source[key]))) {
                // source[key] 是物件,而 target[key] 不是物件, 則 target[key] = {} 初始化一下,否則遞迴會出錯的
                if (isPlainObject(source[key]) && !isPlainObject(target[key]))
                    target[key] = {}

                // source[key] 是陣列,而 target[key] 不是陣列,則 target[key] = [] 初始化一下,否則遞迴會出錯的
                if (isArray(source[key]) && !isArray(target[key]))
                    target[key] = []
                // 執行遞迴
                extend(target[key], source[key], deep)
            }
            // 不滿足以上條件,說明 source[key] 是一般的值型別,直接賦值給 target 就是了
            else if (source[key] !== undefined) target[key] = source[key]
    }

    // Copy all but undefined properties from one or more
    // objects to the `target` object.
    $.extend = function(target){
        var deep, args = slice.call(arguments, 1);

        //第一個引數為boolean值時,表示是否深度合併
        if (typeof target == 'boolean') {
            deep = target;
            //target取第二個引數
            target = args.shift()
        }
        // 遍歷後面的引數,都合併到target上
        args.forEach(function(arg){ extend(target, arg, deep) })
        return target
    }複製程式碼

在 Zepto 中的 $.extend 方法判斷的第一個引數傳入的是一個布林值,判斷是否進行深拷貝。

在 $.extend 方法內部,只有一個形參 target,這個設計你真的很巧妙。
因為形參只有一個,所以 target 就是傳入的第一個引數的值,並在函式內部設定一個變數 args 來接收去除第一個引數的其餘引數,如果該值是一個布林型別的值的話,說明要啟用深拷貝,就將 deep 設定為 true,並將 target 賦值為 args 的第一個值(也就是真正的 target)。如果該值不是一個布林型別的話,那麼傳入的第一個值仍為 target 不需要進行處理,只需要遍歷使用 extend 方法就可以。

這裡有點繞,但是真的設計的很精妙,建議自己打斷點試一下,會有意外收穫(玩轉 js 的大神請忽略)。

而在 extend 的內部,是拷貝的過程。

參考文章: