js 深拷貝 vs 淺拷貝

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

本文思惟導圖以下:
html

本文思惟導圖
本文思惟導圖

本文首發於個人我的博客:cherryblog.site/java

堆和棧的區別

其實深拷貝和淺拷貝的主要區別就是其在內存中的存儲類型不一樣。數組

堆和棧都是內存中劃分出來用來存儲的區域。ecmascript

棧(stack)爲自動分配的內存空間,它由系統自動釋放;而堆(heap)則是動態分配的內存,大小不定也不會自動釋放。函數

ECMAScript 的數據類型

在將深拷貝和淺拷貝以前,咱們先來從新回顧一下 ECMAScript 中的數據類型。主要分爲ui

基本數據類型(undefined,boolean,number,string,null

基本數據類型主要是:undefined,boolean,number,string,nullspa

基本數據類型存放在棧中

存放在棧內存中的簡單數據段,數據大小肯定,內存空間大小能夠分配,是直接按值存放的,因此能夠直接訪問。設計

基本數據類型值不可變

javascript中的原始值(undefined、null、布爾值、數字和字符串)與對象(包括數組和函數)有着根本區別。原始值是不可更改的:任何方法都沒法更改(或「突變」)一個原始值。對數字和布爾值來講顯然如此 —— 改變數字的值自己就說不通,而對字符串來講就不那麼明顯了,由於字符串看起來像由字符組成的數組,咱們指望能夠經過指定索引來假改字符串中的字符。實際上,javascript 是禁止這樣作的。字符串中全部的方法看上去返回了一個修改後的字符串,實際上返回的是一個新的字符串值3d

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

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:淺拷貝獲得

而後咱們改變 obj2name 屬性和 obj3name 屬性,能夠看到,改變賦值獲得的對象 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 的內部,是拷貝的過程。

參考文章:

相關文章
相關標籤/搜索