JS基本資料型別和引用資料型別的區別及深淺拷貝
1、棧(stack)和堆(heap)
stack為自動分配的記憶體空間,它由系統自動釋放;而heap則是動態分配的記憶體,大小也不一定會自動釋放
2、資料型別
JS分兩種資料型別:
基本資料型別:Number、String、Boolean、Null、 Undefined、Symbol(ES6),這些型別可以直接操作儲存在變數中的實際值。
引用資料型別:Object(在JS中除了基本資料型別以外的都是物件,資料是物件,函式是物件,正則表示式是物件)
3、基本資料型別(存放在棧中)
基本資料型別是指存放在棧
|
下圖演示了這種基本資料型別賦值的過程:
4、引用資料型別(存放在堆記憶體中的物件,每個空間大小不一樣,要根據情況進行特定的配置)
引用型別是存放在堆記憶體中的物件,變數其實是儲存的在棧記憶體中的一個指標(儲存的是堆記憶體中的引用地址),這個指標指向堆記憶體。
引用型別資料在棧記憶體中儲存的實際上是物件在堆記憶體中的引用地址。通過這個引用地址可以快速查詢到儲存中堆記憶體中的物件
var obj1 = new Object(); var obj2 = obj1; obj2.name = "我有名字了"; console.log(obj1.name); // 我有名字了
說明這兩個引用資料型別指向了同一個堆記憶體物件。obj1賦值給obj2,實際上這個堆記憶體物件在棧記憶體的引用地址複製了一份給了obj2,但是實際上他們共同指向了同一個堆記憶體物件,所以修改obj2其實就是修改那個物件,所以通過obj1訪問也能訪問的到。
1 2 3 4 5 6 7 8 9 10 |
|
從上面我們可以得知,當我改變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,4 ---------------------------------- var a = [1,2,3]; var b = a.concat(); b.push(4); b//1,2,3,4 a//1,2,4 ---------------------------------- var a = [1,2,3]; var b = [...a] b//1,2,3,4 a//1,2,4
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
學習,是為了變成更好的自己, 將來的你,一定會感謝現在努力的自己!