1. 程式人生 > >javascript學習總結之物件的深拷貝和淺拷貝

javascript學習總結之物件的深拷貝和淺拷貝

前言

最近在寫ES6的文章的時候發現重複遇到關於javascript深拷貝和淺拷貝的問題,然後查找了一些資料,根據資料和自己的理解做了以下筆記,畢竟javascript關於深拷貝和淺拷貝的問題在一些面試的時候有些面試官可能會進行提問,一起來看看吧!

資料型別

在瞭解淺拷貝和深拷貝之前,我們先回顧一下javascript中的資料型別,因為在講淺拷貝和深拷貝的時候就是就是對原始資料型別(基本資料型別)和物件資料型別(引用資料型別)的拷貝

在javascript中,我們將資料型別分為兩種,原始資料型別(基本資料型別)和物件型別(引用資料型別)

基本資料型別

基本資料型別的值是按值訪問的,基本資料型別的值是不可變的

常見的基本資料型別:Number,String,Boolean,Undefined,Null

引用資料型別

引用型別的值是按引用訪問的,引用型別的值是動態可變的

常見的引用型別:Object,Function,Array

由於資料型別的訪問方式不同,它們的比較方式也是不一樣的,我們來看一下下面的示例

(1)基本資料型別和引用資料型別的比較

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>深拷貝和淺拷貝入門</title>
    </head>
    <body>
        <script type="text/javascript">
            var a=100;
            var b=100;
            console.log(a===b);//true
            var c={a:1,b:2};
            var d={a:1,b:2};
            console.log(c===d);//false
        </script>
    </body>
</html>

總結

  • 基本資料型別的比較是值的比較,所以在示例中a===b為true
  • 引用型別的比較是引用地址的比較,所以在示例c===d為false,因為c和d的地址不同

鑑於綜上兩點我們大概知道所謂的淺拷貝和深拷貝可能就是對於值的拷貝和引用的拷貝(基本資料型別都是對值的拷貝),在這裡主要講解關於引用型別的拷貝

淺拷貝

淺拷貝是物件共用一個記憶體地址,物件的變化相互影響。比如常見的賦值引用就是淺拷貝

(1)簡單物件的淺拷貝

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>物件的淺拷貝</title>
    </head>
    <body>
        <script type="text/javascript">
            var obj1={name:'cat'};
            var obj2=obj1;
            obj2.name='dog';
            console.log(obj1);//{name:'dog'}
            console.log(obj2);//{name:'dog'}
        </script>
    </body>
</html>

我們發現當我們改變obj2的值的時候obj1的值也會發生改變,這裡到底發生了什麼,請看圖解

當我們將obj2的值賦值給obj1的時候,僅僅只是將obj2的地址給了obj1而不是obj1重新在記憶體中開闢空間,所以obj1的地址和obj2的地址指向相同,改變obj2的時候obj1也會發生改變。

(2)使用迴圈實現淺拷貝

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>使用迴圈實現淺拷貝</title>
    </head>
    <body>
        <script type="text/javascript">
            var person={
                name:'tt',
                age:18,
                friends:['aa','bb','cc']
            }
            function shallowCopy(source){
                if(!source||typeof source!=='object'){
                    throw new Error('error');
                }
                var targetObj=source.constructor===Array?[]:{};
                for(var keys in source){
                    if(source.hasOwnProperty(keys)){
                        targetObj[keys]=source[keys]
                    }
                }
                return targetObj;
            }
            var p1=shallowCopy(person);
            console.log(p1);//{name:'tt',age:18,friends:['aa','bb','cc']}
        </script>
    </body>
</html>

在上面的程式碼中,我們建立了shallowCopy函式,它接收一個引數也就是被拷貝的物件,步驟分別是

(1):首先建立了一個物件
(2):然後for...in迴圈傳進去的物件為了避免迴圈到原型上面會被遍歷到的屬性,使用 hasOwnProperty 限制迴圈只在物件自身,將被拷貝物件的每一個屬性和值新增到建立的物件當中
(3):最後返回這個物件

那麼看到這裡,我們發現p1拿到了和person一樣的物件,那麼p1=person又有什麼區別了,我們看下下面的示例

(3)簡單物件的淺拷貝

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>簡單物件的淺拷貝</title>
    </head>
    <body>
        <script type="text/javascript">
            var person={
                name:'tt',
                age:18,
                friends:['oo','cc','yy']
            }
            function shallowCopy(source){
                if(!source||typeof source!=='object'){
                    throw new Error('error');
                }
                var targetObj=source.constructor===Array?[]:{};
                for(var keys in source){
                    if(source.hasOwnProperty(keys)){
                        targetObj[keys]=source[keys]
                    }
                }
                return targetObj;
            }
            var p1=shallowCopy(person);
            var p2=person;
            //這個時候我們修改person的資料
            person.name='tadpole';
            person.age=19;
            person.friends.push('tt');
            console.log(p2.name);//tadpole
            console.log(p2.age);//19
            console.log(p2.friends);//['oo','cc','yy','tt']
            console.log(p1.name);//tt
            console.log(p1.age);//18
            console.log(p1.friends);//['oo','cc','yy','tt']
        </script>
    </body>
</html>

上面建立了一個新變數p2,將person的值賦值給p2,然後比較這兩個值

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

 

深拷貝

深拷貝是將物件放到一個新的記憶體中,兩個物件的改變不會相互影響或者你可以理解為淺拷貝由於只是複製一層物件的屬性,當遇到有子物件的情況時,子物件就會互相影響。所以,深拷貝是對物件以及物件的所有子物件進行拷貝

(1)遞迴呼叫淺拷貝實現深拷貝

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>遞迴實現深拷貝</title>
    </head>
    <body>
        <script type="text/javascript">
            var obj1={
                name:'cat',
                show:function(){
                    console.log('名稱:'+this.name);
                }
            }
            var obj2=deepClone(obj1);
            obj2.name='pig';
            obj1.show();//cat
            obj2.show();//pig
            function deepClone(obj){
                var objClone=Array.isArray(obj)?[]:{};
                if(obj&&typeof obj==='object'){
                    for(key in obj){
                        if(obj.hasOwnProperty(key)){
                            //判斷obj子元素是否為物件,如果是,遞迴複製
                            if(obj[key]&&typeof obj[key]==='object'){
                                objClone[key]=deepClone(obj[key])
                            }else{
                                //如果不是,簡單複製
                                objClone[key]=obj[key]
                            }    
                        }
                    }
                }
                return objClone;
            }
            
        </script>
    </body>
</html>

對於深拷貝的物件,改變源物件不會對得到的物件有影響。只是在拷貝的過程中源物件的方法丟失了,這是因為在序列化 JavaScript 物件時,所有函式和原型成員會被有意忽略

(2)利用 JSON 物件中的 parse 和 stringify實現深拷貝

JOSN 物件中的 stringify 可以把一個 js 物件序列化為一個 JSON 字串,parse 可以把 JSON 字串反序列化為一個 js 物件,通過這兩個方法,也可以實現物件的深拷貝

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>利用 JSON 物件中的 parse 和 stringify</title>
    </head>
    <body>
        <script type="text/javascript">
            var obj1={
                name:'cat',
                show:function(){
                    console.log(this.name);
                }
            }
            var obj2=JSON.parse(JSON.stringify(obj1));
            obj2.name='dog';
            console.log(obj1.name);//cat
            console.log(obj2.name);//dog
            obj1.show();//cat
            obj2.show();//TypeError: obj2.show is not a function
        </script>
    </body>
</html>

注意:JSON.parse()和JSON.stringify()能正確處理的物件只有Number、String、Array等能夠被json表示的資料結構,因此函式這種不能被json表示的型別將不能被正確處理,經過轉換之後,function丟失了,因此JSON.parse()和JSON.stringify()還是需要謹慎使用

(3)使用Object.assgin()方法實現深拷貝

這種方法我在javascript學習總結之Object.assign()方法詳解有過講解,但是我在看資料的時候有發現了一點點的問題,所以在這裡補充一下

定義:Object.assign() 方法用於將所有可列舉的屬性的值從一個或多個源物件複製到目標物件。它將返回目標物件

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>使用Object.assgin()方法</title>
    </head>
    <body>
        <script type="text/javascript">
            let srcObj = {'name': 'lilei', 'age': '20'};
            let copyObj2 = Object.assign({}, srcObj, {'age': '21'});
            copyObj2.age = '23';
            console.log(srcObj);//{name:'lilei',age:20}
            console.log(copyObj2);//{name:'lilei',age:23}
        </script>
    </body>
</html>

看起來好像是深拷貝了,那其實這裡let copyObj2 = Object.assign({}, srcObj, {'age': '21'}); 我們把srcObj 給了一個新的空物件。同樣目標物件為 {},我們再來測試下

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>使用Object.assgin()方法</title>
    </head>
    <body>
        <script type="text/javascript">
            let srcObj = {'name': 'lilei', 'age': '20'};
            let copyObj2 = Object.assign({}, srcObj, {'age': '21'});
            copyObj2.age = '23';
            console.log(srcObj);//{name:'lilei',age:20}
            console.log(copyObj2);//{name:'lilei',age:23}
            srcObj = {'name': '明', grade: {'chi': '50', 'eng': '50'} };
            copyObj2 = Object.assign({}, srcObj);
            copyObj2.name = '紅';
            copyObj2.grade.chi = '60';
            console.log(srcObj);//{name:'紅',grade:{chi:60,eng:50}}
        </script>
    </body>
</html>

從例子中可以看出,改變複製物件的name 和 grade.chi ,源物件的name沒有變化,但是grade.chi卻被改變了。因此我們可以看出Object.assign()拷貝的只是屬性值,假如源物件的屬性值是一個指向物件的引用,它也只拷貝那個引用值。 
也就是說,對於Object.assign()而言, 如果物件的屬性值為簡單型別(string, number),通過Object.assign({},srcObj);得到的新物件為‘深拷貝’;如果屬性值為物件或其它引用型別,那對於這個物件而言其實是淺拷貝的。這是Object.assign()特別值得注意的地方,補充一句,Object.assig({},src1,src2) 對於scr1和src2之間相同的屬性是直接覆蓋的,如果屬性值為物件,是不會對物件之間的屬性進行合併的。

總結

本篇部落格主要講解了資料型別,淺拷貝的實現方式,深拷貝的實現方式,從資料型別的講解中一步一步引入到關於淺拷貝和深拷貝的實現方式,在這裡我們必須學會關於遞迴實現深拷貝的實現方式,這個有可能在面試的時候會實現手寫代