JS基本數據類型和引用數據類型的區別及深淺拷貝


前言

首先我們先來了解一下什么叫棧堆基本數據類型引用數據類型

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

2.基本的數據類型:String, Number, boolean, Null, Undefined,Symbol(ES6新增)  

  特點: 存儲的是該對象的實際數據,(存放在棧中)

3.對象數據類型(也稱為引用數據類型):Array,Object,Function

  特點: 存儲的是該對象在棧中引用,真實的數據存放在堆內存里,(存放在堆內存中的對象,每個空間大小不一樣,要根據情況進行特定的配置)

:在JS中除了基數據類型以外的都是對象,數據是對象,函數是對象,正則表達式是對象

1、區別: 淺拷貝/深度拷貝
判斷: 拷貝是否產生了新的數據還是拷貝的是數據的引用
知識點:對象數據存放的是對象在棧內存的引用,直接復制的是對象的引用

2、常用的拷貝技術
  1). arr.concat(): 數組淺拷貝
  2). arr.slice(): 數組淺拷貝
  3).Object.assign()對象淺拷貝
  4). JSON.parse(JSON.stringify(arr/obj)): 數組或對象深拷貝, 但不能處理函數數據
  5). 淺拷貝包含函數數據的對象/數組
  6). 深拷貝包含函數數據的對象/數組

 

1.淺拷貝

在ES6中,Object對象新增了一個assign方法,可以實現對象的淺復制。這里談談Object.assign方法的具體用法,因為稍后會分析jQuery的extend方法,實現的原理同Object.assign方法差不多。

對於基本數據類型來說,復制一個變量值,本質上就是copy了這個變量。一個變量值的修改,不會影響到另外一個變量。

 

<script type="text/javascript">
    /*1.直接賦值給一個變量*/
    let obj = {username:'genius'};
    console.log(obj);      //genius
    //拷貝數組/對象  沒有生成新的數據而是復制了一份引用。
    let arr = [1,4,{username:'lucas',age:23}];
    let arr2 = arr;
    arr2[0] = 'abcd0';
    console.log(arr,arr2);  //倆個輸出結果相同

    /*Object.assign*/
    let colors=['red','green','blue'];  
    let colors2=Object.assign([],colors);  
    colors2[0]="orange";  
    console.log(colors2);//['orange','green','blue']  
    console.log(colors);//['red','green','blue']  

    /*3.concat方式*/
    let arr = [1,3,{username:'lucas'}];
    let testArr = [2,4];
    let arr2 = arr.concat(testArr);  //連接函數
    //arr2[2] = {username:'tiantian'};
    console.log(arr2);  //輸出如下:數組中有對象
    // 輸出數組:
    // 0:1
    // 1:3
    // 2:{username:'lucas'}  //這個值會隨下面的更改而變
    // 3:2
    // 4:4

    /*4.slice方式*/
    console.log('-------------------');
    arr2[2].username = 'fengdudu';   //修改了arr的值,因為arr2[2]等於arr[2]
    console.log(arr);                 //和922行輸出結果一樣,只是下標2的內容改變了
    let arr3 = arr.slice();         //有startindex,endindex選項
    arr3[2].username = 'HHHHH';         //修改了arr的值,因為arr2[2]等於arr[2]
    console.log(arr);                 //和922行輸出結果一樣,只是下標2的內容改變了

    /*5.深度克隆*/
    console.log('===================');
    let arr4 = JSON.parse(JSON.stringify(arr));
    console.log(arr4);          //只拷貝了arr並沒有testArr的數組,所以只有下標3長度
    arr4[2].username = 'duncan';
    console.log(arr,arr4);
    arr[2].username='MM';     //修改了arr、arr2、arr3的下標2的值。
    //JSON.stringify要求你放入的是原生的JS對象或數組,不能放數組。
    //JSON.stringify(arr)執行后arr的數據已經是JSON 字符串了,然后parse又轉字符串,最終拿到一個JSON字符串,然后轉換為一個對應的JS數組。
    console.log('-------------------');
</script>

 :只會對只是一級屬性復制,比淺拷貝多深拷貝了一層而已,所以還是無法達到深度克隆的目的.

2.深拷貝

對於復雜數據類型來說,同基本數據類型實現的不太相同。對於復雜數據類型的復制,要注意的是,變量名只是指向這個對象的指針。當我們將保存對象的一個變量賦值給另一個變量時,實際上復制的是這個指針,而兩個變量都指向都一個對象。因此,一個對象的修改,會影響到另外一個對象。

在實際的開發項目中,前后端進行數據傳輸,主要是通過JSON實現的。

JSON對象下有兩個方法,一是將JS對象轉換成字符串對象的JSON.stringify方法;一個是將字符串對象轉換成JS對象的JSON.parse方法。

這兩個方法結合使用可以實現對象的深復制。也就是說,當我們需要復制一個obj對象時,可以先調用JSON.stringify(obj),將其轉換為字符串對象,然后再調用JSON.parse方法,將其轉換為JS對象。就可以輕松的實現對象的深復制。

 例1

function deepClone(obj){
    let _obj = JSON.stringify(obj),
        objClone = JSON.parse(_obj);
    return objClone
}    
let a=[0,1,[2,3],4];
    b=deepClone(a);
    
    a[0]=1;
    a[2][0]=1;
console.log(a,b);

效果如下:

 

 例2

 1 <script type="text/javascript">
 2     /*
 3         思考:
 4             如何實現深度拷貝(克隆)
 5             拷貝的數據里有對象/數組
 6             拷貝的數據里不能有對象/數組,即使有對象/數組可以繼續遍歷對象、數組拿到里邊每一項值,一直拿到是基本數據類型,然后再去復制,就是深度拷貝。
 7     */
 8 
 9     //知識點儲備
10     /*
11         如何判斷數據類型:arr-Array null -Null
12         1.typeof返回的數據類型有:String,Number,Boolean,Undefined,Object,Function。
13         2.Object.prototype.toString.call(obj)。
14     */
15     
16     let result = 'abcd';
17     result = null;
18     result = [1,3];
19     console.log(Object.prototype.toString.call(result).slice(8,-1)); //[object Array],sclice截取字符串后:Array(拿到分類)。
20     //console.log(typeof Object.prototype.toString.call(result));  //string
21 
22 
23     //for in 循環對象(屬性名)、數組(下標),推薦在循環對象屬性的時候,使用for...in,在遍歷數組的時候的時候使用for...of。
24     //for in 循環遍歷對象屬性名
25     let obj = {username:'zhangsan',age:22};
26     for(let i in obj){
27         console.log(i);  //username age
28     }
29 
30     //for in 循環遍歷數組下標
31     let arr = [1,3,'abc'];
32     for(let i in arr){        //數組的可以用for of對應數組值
33         console.log(i); //0 1 2
34     }
35 
36     //定義檢測數據類型的功能函數
37     function checkedType(target){
38         return Object.prototype.toString.call(target).slice(8,-1);
39     }
40     console.log(checkedType(result));  //Array
41 
42     //實現深度克隆--對象/數組
43     function clone(target){
44         //判斷拷貝的數據類型
45         //初始化變量的result 成為最終克隆的數據
46         let result,targetType = checkedType(target);
47         if(targetType === 'Object'){
48             result = {};
49         }else if(targetType === 'Array'){
50             result = [];
51         }else{
52             return target;   //如果是基本數據類型:(String, Number, boolean, Null, Undefined)就直接反回去。
53         }
54 
55         //遍歷目標數據
56         for(let i in target){
57            //獲取遍歷數據結構的每一項值。
58             let value = target[i];   //i=1,i=2,i=..
59               //判斷目標結構里的每一值是否存在對象/數組
60             if(checkedType(value) === 'Object' || checkedType(value) === 'Array'){ //如果對象OR數組里嵌套了對象/數組
61                 //繼續遍歷獲取到的value值
62                 result[i] = clone(value);    //這個只執行一次,數組里只有一個對象
63             }else{  //獲取到的value值是基本的數據類型或者是函數。
64                 result[i] = value;  //因為arr3數組的下標0和1都是Number類型,只有下標2才是Object(轉去執行1046行)
65             }
66         }
67         return result;
68     }
69     let arr3 = [1,2,{username:'dudu',age:32}];
70     let arr4 = clone(arr3);    //相當於復制了一份arr3的基本數據
71     console.log(arr4);
72     arr4[2].username = 'gate';
73     arr4[2].age = 65;
74     console.log(arr3,arr4);  //arr3下標2是{username:'dudu':age:32},arr4下標2是{username:gate,age:65}
75 </script>

 

輸出如下:

 

 

總結基本數據類型和引用數據類型區別

1、聲明變量時內存分配不同

 *原始類型:在棧中,因為占據空間是固定的,可以將他們存在較小的內存中-棧中,這樣便於迅速查詢變量的值

 *引用類型:存在堆中,棧中存儲的變量,只是用來查找堆中的引用地址。

    這是因為:引用值的大小會改變,所以不能把它放在棧中,否則會降低變量查尋的速度。相反,放在變量的棧空間中的值是該對象存儲在堆中的地址。地址的大小是固定的,所以把它存儲在棧中對變量性能無任何負面影響

2、不同的內存分配帶來不同的訪問機制

    在javascript中是不允許直接訪問保存在堆內存中的對象的,所以在訪問一個對象時,首先得到的是這個對象在堆內存中的地址,然后再按照這個地址去獲得這個對象中的值,這就是傳說中的按引用訪問

    而原始類型的值則是可以直接訪問到的。

3、復制變量時的不同

 1)原始值:在將一個保存着原始值的變量復制給另一個變量時,會將原始值的副本賦值給新變量,此后這兩個變量是完全獨立的,他們只是擁有相同的value而已。

 2)引用值:在將一個保存着對象內存地址的變量復制給另一個變量時,會把這個內存地址賦值給新變量,

    也就是說這兩個變量都指向了堆內存中的同一個對象,他們中任何一個作出的改變都會反映在另一個身上。
    (這里要理解的一點就是,復制對象時並不會在堆內存中新生成一個一模一樣的對象,只是多了一個保存指向這個對象指針的變量罷了)。 多了一個指針
4、參數傳遞的不同(把實參復制給形參的過程
首先我們應該明確一點:ECMAScript中所有函數的參數都是按值來傳遞的。
  但是為什么涉及到原始類型與引用類型的值時仍然有區別呢?還不就是因為內存分配時的差別。  
   1)原始值:只是把變量里的值傳遞給參數,之后參數和這個變量互不影響。
   2)引用值:對象變量它里面的值是這個對象在堆內存中的內存地址,這一點你要時刻銘記在心!
    因此它傳遞的值也就是這個內存地址,這也就是為什么函數內部對這個參數的修改會體現在外部的原因了,因為它們都指向同一個對象。

 

 部分參考資料:JS基本數據類型和引用數據類型的區別

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM