JavaScript進階(三) 值傳遞和引用傳遞

從C語言開始

有時候講一些細節或是底層的東西,我喜歡用C語言來說,由於用C更方便來描述內存裏面的東西。先舉一個例子,swap函數,相信有一些編程經驗的人都見識過,聲明以下,函數體我就不寫了,各位腦補一下。
  1. void swap1(int a, int b);  
  2. void swap2(int* a, int* b)  

這裏swap1是不能交換兩個數的值的,swap2能夠。那爲何呢?有教材會說,第一個是值傳遞,第二個是引用傳遞,傳遞的是指針,因此第二個能夠。好吧,這個解釋和沒說同樣,那下面我就來解釋一下,調用這兩個函數的時候,到底發生了什麼,爲何一個能夠交換,另外一個不能夠。爲了方便描述,我把這兩個函數的調用代碼也寫出來
  1. int main() {  
  2.     int a = 3;  
  3.     int b = 4;  
  4.     swap1(a, b);    //此時a = 3, b = 4;  
  5.     int* pa = &a;  
  6.     int* pb = &b;     //爲了方便解釋,增長這兩個臨時變量,不然直接寫swap2(&a, &b)的話,這行代碼作的事情太多,很差解釋。  
  7.     swap2(pa, pb);    //此時a = 4, b = 3;  
  8.     return 0;  
  9. }  

函數的執行是在棧中,下圖描述了swap1執行開始和結束的時候,棧中的狀況。
左圖爲執行前,右圖爲執行後。當main函數調用swap1函數的時候,將兩個入參a,b壓棧,壓棧採用的是複製的方式,當swap1執行的時候,修改了swap1棧空間的兩個值,可是main函數中的兩個值沒有受影響。這就是值傳遞。把入參的值複製壓棧來傳入參數。下面來看swap2的狀況
左圖爲執行前,右圖爲執行後。其中最左一列爲內存地址。這裏地址,壓棧方向,地址順序均爲示例。各位看到沒有,所謂引用傳遞,其實仍是值傳遞,傳遞的時候仍是採用複製壓棧,只是傳遞的「值」是個地址。swap2執行的時候,執行*a = temp.給*a 賦值,這行語句的意思是修改a這個地址指向的內存的值,因爲這個地址指向的位置在main方法的棧空間中,因此實現了修改原來值。
 
以上就是C語言在實現swap函數時候內存的細節,下面咱們來探討一下JS中調用函數的狀況。因爲JS中沒有&這個取地址符號,那麼JS中傳遞的究竟是什麼呢?

回到JS

 
JS中的數據類型有數字,布爾,數組,字符串,對象,null, undefined. 那麼當用這些數據類型來做爲函數的參數的時候,究竟是引用傳遞仍是值傳遞呢?先說結論:布爾,數字是值傳遞,字符串,數組,對象是引用傳遞。事實上字符串,數組也能夠做爲對象。null和undefined在傳遞的時候究竟是什麼,我不清楚。若是有熟悉的大神請幫忙解釋一下。在這裏小弟先謝了。
 
布爾,數字在做爲參數傳遞的時候,其實現和C語言同樣,這裏不作贅述。也是將調用者的局部變量複製壓棧,傳遞給被調用者。下面我來詳細的描述一下對象是如何傳遞的。以一個函數來舉例,假設你須要實現一個函數,將一個傳入的數組反序,reverse,下面有兩個實現,請各位來看一下有什麼問題:
  1. function reverse1(array) {  
  2.     var temp = [];  
  3.     for (var i = array.length - 1; i > -1; i--) {  
  4.         temp.push(array[i]);  
  5.     }  
  6.     array = temp;  
  7. }  
  8.   
  9. function reverse2(array) {  
  10.     var temp = [];  
  11.     for (var i = array.length - 1; i > -1; i--) {  
  12.         temp.push(array[i]);  
  13.     }  
  14.     for (var i = 0; i < array.length; i++) {  
  15.         array[i] = temp[i]  
  16.     }  
  17. }  

這兩個函數都是先將一個反序完成的數組存儲在temp裏面,而後賦值給入參array,就是賦值的方式有所不一樣。這個不一樣的賦值方式也致使告終果的不一樣,結果就是reverse1沒法完成工做,reverse2能夠。爲了解答這個問題,我先講一下JS裏面,內存中對象是如何存儲的。當一行代碼 var temp = [] 被運行的時候,內存中是這樣的:javascript

其中藍色的是棧,黑框的是堆,用來動態分配內存,最右綠色的表示這段堆的起始地址。也就是說當聲明一個對象的時候,棧中保存的內容只是一個指針,真正的內容在堆中。以此爲基礎,咱們再來看一下當函數reverse1執行的時候,內存中如何實現的。爲方便舉例,假設傳入的數組爲[1,2,3];java

上圖爲執行前,下圖爲執行後。當函數reverse1執行時,in做爲參數傳入。傳入參數時,相似C語言的引用傳遞,將地址複製了一份,壓棧傳到子函數中。因此兩個函數中的變量是指向同一個位置的。當reverse1執行時,temp中存儲了array的反序,最後一行賦值的時候,你就看到了以下面的圖表示的那樣,reverse1中的array確實指向了新的反序數組,可是調用者中的局部變量in卻絲毫未動。因此致使了reverse1沒法完成反序功能。編程

那麼咱們再看reverse2. reverse2中的第二個循環逐個給數組的內容複製,其實它操縱的內存空間就是array指向的區域,咱們又知道array和in指向了同一個區域,因此in指向的區域也被改變了。數組

總結一下以上所說的,函數

 

  1. JS中布爾,數字爲基本數據類型,是值傳遞。沒法做爲引用傳遞。因此JS中沒法實現基本數據類型的swap函數。
  2. 對象是引用傳遞。當傳遞對象給子函數時,傳遞的是地址。子函數使用這個地址來操做修改傳入的對象。可是若是在子函數修改該地址指向的位置時,這個改變將沒法做用於調用者。
  3. 引用傳遞其實仍是值傳遞,只是傳入的值是個地址,而且該地址指向了一段保存了對象數據的內存。這點和C中的引用傳遞相似。

特別說一下String

String是JS的內置對象,因此根據上文所說,它是引用傳遞。那麼下面我請你寫一個函數,將傳入的String修改,給它兩頭加上引號。因此很明顯,下面這樣的函數就是錯誤的了
  1. function foo(s) {  
  2.     s = "\"" + s + "\""  
  3. }  
那麼正確的函數應該怎寫呢?你可能會想,應該使用String對象的函數來修改String的內容。這麼想是對的,可是很不幸,JS提供的String沒有任何一個能夠修改String內容的函數。有人說不對,好比字符串鏈接函數,concat,轉大小寫函數toUpperCase,toLowerCase。事實上這兩個函數只是返回了一個新的String對象,其本來的值兵沒有改動。這個你能夠去作實驗看看。因此String對象被創建好以後,就再也沒法改動了,因此沒法用一個子函數來修改它的值。又因爲String能夠用 == 來判斷其內容是否相等,因此它的各方面特性都很像基本數據類型。可是還有一點不同,請看下面的例子:
  1. var a = 1  
  2. var b = 1  
  3. a == b            //true  
  4. a === b           //true  
  5.   
  6. var s1 = "sdf"  
  7. var s2 = "sdf"  
  8. s1 == s2          //true  
  9. s1 === s2         //true  
  10. s3 = new String("sdf")  
  11. s1 === s3         //false  

對於數字,估計各位沒有疑問吧。那麼對於字符串來講,== 比較的是兩個字符串的內容,這個應該也沒有疑問。那麼===呢?而且爲何s1===s2爲true,s1===s3爲false呢?
 
當用===來比較字符串的時候,事實上比較的是兩個對象的地址。s1的值「sdf」這個字符串的地址,s3則是一個新的對象的地址。他們不相等,這個很好理解。那麼s1 和 s2如何解釋呢?這由於JS引擎有一個靜態字符串存儲區,當聲明一個字符串常量的時候,會先去該存儲區查找有沒有相同的字符串,若是有就返回該字符串,沒有再在靜態字符串區從新初始化一個字符串對象。這就解釋了爲何s1 === s2.
 
順便說一句,就是字符串的不可變性,以及常量字符串區這兩個特性,Java和JS是同樣的。然而C++的STL中的std::string是可變的。

注:本文中的JS執行時的內存示例圖並非真正的JS引擎執行時候物理內存的樣子。物理內存的實現取決於JS引擎。
相關文章
相關標籤/搜索