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


1、棧(stack)和堆(heap)

  stack為自動分配的內存空間,它由系統自動釋放;而heap則是動態分配的內存,大小也不一定會自動釋放

2、數據類型

  JS分兩種數據類型:

    基本數據類型Number、String、Boolean、Null、 Undefined、Symbol(ES6),這些類型可以直接操作保存在變量中的實際值。

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

3、基本數據類型(存放在中)

     基本數據類型是指存放在中的簡單數據段,數據大小確定,內存空間大小可以分配,它們是直接按值存放的,所以可以直接按值訪問

var a = 10;
var b = a;
b = 20;
console.log(a); // 10值
console.log(b); // 20值

 下圖演示了這種基本數據類型賦值的過程:

4、引用數據類型(存放在內存中的對象,每個空間大小不一樣,要根據情況進行特定的配置)

    引用類型是存放在堆內存中的對象,變量其實是保存的在棧內存中的一個指針(保存的是堆內存中的引用地址),這個指針指向堆內存。

    引用類型數據在棧內存中保存的實際上是對象在堆內存中的引用地址。通過這個引用地址可以快速查找到保存中堆內存中的對象

var obj1 = new Object();

var obj2 = obj1;

obj2.name = "我有名字了";

console.log(obj1.name); // 我有名字了

說明這兩個引用數據類型指向了同一個堆內存對象。obj1賦值給obj2,實際上這個堆內存對象在棧內存的引用地址復制了一份給了obj2,但是實際上他們共同指向了同一個堆內存對象,所以修改obj2其實就是修改那個對象,所以通過obj1訪問也能訪問的到。

var a = [1,2,3,4,5];
var b = a;//傳址 ,對象中傳給變量的數據是引用類型的,會存儲在堆中;
var c = a[0];//傳值,把對象中的屬性/數組中的數組項賦值給變量,這時變量C是基本數據類型,存儲在棧內存中;改變棧中的數據不會影響堆中的數據
alert(b);//1,2,3,4,5
alert(c);//1
//改變數值  
b[4] = 6;
c = 7;
alert(a[4]);//6
alert(a[0]);//1

   從上面我們可以得知,當我改變b中的數據時,a中數據也發生了變化;但是當我改變c的數據值時,a卻沒有發生改變。

     這就是傳值與傳址的區別。因為a是數組,屬於引用類型,所以它賦予給b的時候傳的是棧中的地址(相當於新建了一個不同名“指針”),而不是堆內存中的對象。而c僅僅是從a堆內存中獲取的一個數據值,並保存在棧中。所以b修改的時候,會根據地址回到a堆中修改,c則直接在棧中修改,並且不能指向a堆內存中。

5、淺拷貝

前面已經提到,在定義一個對象或數組時,變量存放的往往只是一個地址。當我們使用對象拷貝時,如果屬性是對象或數組時,這時候我們傳遞的也只是一個地址。因此子對象在訪問該屬性時,會根據地址回溯到父對象指向的堆內存中,即父子對象發生了關聯,兩者的屬性值會指向同一內存空間。

var a={key1:"11111"}
function Copy(p){
   var c ={};
   for (var i in p){
      c[i]=p[i]
   }    
   return c;
}
a.key2 = ["小輝","小輝"]
var b = Copy(a);
b.key3 = "33333"
alert(b.key1)//11111
alert(b.key3)//33333
alert(a.key3);//undefined
b.key2.push("大輝")
alert(a.key2);//小輝,小輝,大輝

但是若是修改的屬性變為對象或數組時,那么父子對象之間就發生關聯,從上可知:

原因是key1的值屬於基本類型,所以拷貝的時候傳遞的就是該數據段;但是key2的值是堆內存中的對象,所以key2在拷貝的時候傳遞的是指向key2對象的地址,無論復制多少個key2,其值始終是指向父對象的key2對象的內存空間。

//ES6實現淺拷貝的方法
var a = {name:"暖風"}
var b= Object.assign({},a);
b.age = 18;
console.log(a.age);//undefined
----------------------------------
//數組
var a = [1,2,3];
var b = a.slice();
b.push(4);
b//1,2,3,4
a//1,2,3
----------------------------------
var a = [1,2,3];
var b = a.concat();
b.push(4);
b//1,2,3,4
a//1,2,3
----------------------------------
var a = [1,2,3];
var b = [...a]
b//1,2,3,4
a//1,2,3

 6、深拷貝

或許以上並不是我們在實際編碼中想要的結果,我們不希望父子對象之間產生關聯,那么這時候可以用到深拷貝。既然屬性值類型是數組和或象時只會傳址,那么我們就用遞歸來解決這個問題,把父對象中所有屬於對象的屬性類型都遍歷賦給子對象即可。測試代碼如下:

var a={key1:"11111"}
function Copy(p,c){
   var c =c||{};
   for (var i in p){
    if(typeof p[i]==="object"){
      c[i]=(p[i].constructor ===Array)?[]:{}
      Copy(p[i],c[i]);
    }else{
       c[i]=p[i]
    }
   }    
   return c;
}
a.key2 = ["小輝","小輝"]
var b = {}
b = Copy(a,b); 
b.key2.push("大輝");
b.key2//小輝,小輝,大輝
a.key2//小輝,小輝   

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

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

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

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

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

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

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

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

3、復制變量時的不同

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

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

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

參考原文:

  https://www.cnblogs.com/huangshikun/p/6510482.html

  https://www.cnblogs.com/cxying93/p/6106469.html


免責聲明!

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



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