:::

JavaScript中參數的傳值與傳址心得

image
JavaScript中參數傳遞到底是「傳值」(passing by value) 還是「傳址」(passing by refernce) ,似乎常常造成許多人混亂。我自己原本也一直以為JavaScript只是傳值而非傳址,而為此混亂了好一陣子。特別是在撰寫物件導向的JavaScript程式時更容易造成混亂。
有人認為JavaScript的函數(function)會依據你輸入參數的資料類型來區分傳值還是傳址,物件(object)的話會使用傳址,非物件的則是使用傳值。但實際上,我認為JavaScript的本質是傳址而非傳值。而端看你在函數裡面是否修改參數的記憶體位址(也就是建立新的資料給參數),來決定是否影響函數之外的參數資料。
以下我們就來一一地檢視一下JavaScript究竟是傳值或是傳址的參數傳遞運作方式吧。

2016/1/14更新:根據底下留言網友討論指出了我原文的錯誤,可以發現其實JavaScript本質仍是「傳值而非傳址」。雖然下面文章仍放著給大家參考,但下面的討論更有看頭。希望觀看這篇的讀者能夠連下面的討論一併閱讀,方能深入瞭解JavaScript的奧妙。

參數傳值:Number、String、Boolean等非Object類型

當輸入給函數的參數類型為非物件(object)的資料型態,例如Number(數字)、String(字串)或是Boolean(布林邏輯值)的時候,JavaScript的函數會以傳值的方式來處理。傳值的意思是說,輸入到函數裡面的參數會被複製一份,而在函數裡面對參數的操作,不會影響到外在參數的影響。
以下是一段示範用的程式碼:
<script type="text/javascript">
//宣告變數,資料類型為字串
var paramA = 'original';

//確認變數資料
document.write(paramA + '<br />');    //輸出 original

//定義函數,在函數中修改參數資料,並且輸出做確認
function changeParamA(paramA)
{
    //修改參數
    paramA = 'changed';
    
    //確認被修改之後的參數資料
    document.write(paramA + '<br />');    //輸出 changed
}

//在函數中修改參數
changeParamA(paramA);    //在函數中運作,輸出 changed

//被函數修改之後,仍然維持原本的資料
document.write(paramA + '<br />');    //輸出 original
</script>

上面的例子的輸出結果將會是:
original
changed
original

由此可知,JavaScript函數中使用參數為字串資料類型時,將會是以傳值的方式運作。同樣的道理也適用於數字、布林值上。

參數傳址:Object

當輸入給函數的參數資料類型為Object(物件)時,JavaScript會以傳址的方式來處理。傳址的意思是說,輸入的函數裡面的參數只是「記憶體中的位置」,而在函數裡面對參數的操作,其實是對記憶體位置中的該物件進行修改,因此函數之外的參數也會受到影響。
以下是一段示範用的程式碼:
<script type="text/javascript">
//宣告變數,資料類型為物件
var paramB = {
    attr: 'original'
};

//確認變數資料
document.write(paramB.attr + '<br />');    //輸出 original

//定義函數,在函數中修改參數資料,並且輸出做確認
function changeParamB(paramB)
{
    //修改參數。注意修改的方式,是直接指定物件的屬性進行修改,而非建立新的物件。    
paramB.attr = "changed";
    
    //確認被修改之後的參數資料
    document.write(paramB.attr + '<br />');    //輸出 changed
}

//在函數中修改參數
changeParamB(paramB);    //在函數中運作,輸出 changed

//被函數修改之後,物件的屬性也跟著改變了
document.write(paramB.attr + '<br />');    //輸出 changed
</script>

上面例子中輸出的結果將會是:
original
changed
changed

在上面的例子中,你可以發現到以物件資料型態輸入函數中的參數,在函數中被修改之後,在函數之外也會跟著受到影響,一般來說這會被視為傳址的參數傳遞方式。

再探Object的參數傳遞

在上一節介紹傳址的例子中,函數修改參數的方式是指定物件的屬性進行修改。但是如果函數修改參數的方式是直接指定新的物件、或是輸入其他的資料類型,那麼函數之外的參數就不會受到修改,也就是傳值的參數傳遞。
以下是一段示範用的程式碼:
<script type="text/javascript">
//宣告變數,資料類型為物件
var paramC = {
    attr: 'original'
};

//確認變數資料
document.write(paramC.attr + '<br />');    //輸出 original

//定義函數,在函數中修改參數資料,並且輸出做確認
function changeParamC(paramC)
{
    //修改參數。注意修改的方式,是建立新的物件,而新的物件裡面也包含attr屬性。
    paramC = {
        attr: 'changed'
    };
    
    //確認被修改之後的參數資料
    document.write(paramC.attr + '<br />');    //輸出 changed
}

//在函數中修改參數
changeParamC(paramC);    //在函數中運作,輸出 changed

//被函數修改之後,仍然維持原本的資料
document.write(paramC.attr + '<br />');    //輸出 original
</script>

上面例子中輸出的結果將會是:
original
changed
original

你會發現到,即使輸入參數的資料類型為物件,但是當你在函數中為參數指定新的資料的時候,函數之外的參數並不會受到影響,是為傳值參數傳遞的運作方式。

推測JavaScript是傳址方式運作

因此由以上的例子中,我可以得到一個推測的結論:JavaScript一直都是用傳址的參數傳遞。只是根據函數對於參數是否變更參數的記憶體位址,而會有看起來像是傳值或傳址的運作差異。
在以物件傳遞、修改物件屬性的paramB例子中,函數本身並沒有修改參數的記憶體位址,也就是沒有給予paramB新建立的資料(新的資料就是一個新的記憶體位址),因此在函數中被修改的paramB,函數之外也會受到影響。
而paramA跟paramC的例子裡都是建立了新的資料給參數,修改了參數本身的記憶體位址,讓函數裡面運作的參數跟函數之外兩者互不相干,因此乍看之下就類似傳值的運作方式。

參數傳值或是傳址運作:Array

那麼我們再回頭來看看JavaScript中一種特殊的資料型態:Array(陣列)。陣列本質上屬於「Object」,那麼把陣列作為參數輸入函數時,究竟是傳值還是傳址呢?由前面的推論中可知,這是根據函數裡面對於參數的操作是否參數為建立新的資料來判斷
以下是一段示範用的程式碼:
<script type="text/javascript">
//宣告變數,資料類型為陣列
var paramAry = ['original'];

//確認變數資料
document.write(paramAry[0] + '<br />');    //輸出 original

//定義函數,在函數中修改參數資料,並且輸出做確認
function changeParamAry1(paramAry)
{
    //修改參數。注意修改的方式,是建立新的陣列。
    paramAry = ['changed'];
    
    //確認被修改之後的參數資料
    document.write(paramAry[0] + '<br />');    //輸出 changed
}

function changeParamAry2(paramAry)
{
    //修改參數。注意修改的方式,是修改陣列裡面的索引,而非建立新的陣列。
    paramAry[0] = 'changed';
    
    //確認被修改之後的參數資料
    document.write(paramAry[0] + '<br />');    //輸出 changed
}

//在函數中修改參數
changeParamAry1(paramAry);    //在函數中運作,輸出 changed

//被函數修改之後,仍然維持原本的資料
document.write(paramAry[0] + '<br />');    //輸出 original

//在函數中修改參數
changeParamAry2(paramAry);    //在函數中運作,輸出 changed

//被函數修改之後,參數的資料已經被修改
document.write(paramAry[0] + '<br />');    //輸出 changed
</script>

上面例子中輸出的結果將會是:
original
changed
original
changed
changed

因此你可以發現到,根據函數對於參數的操作,就會造成傳值或傳址的運作差異。

結語

當JavaScript越寫越複雜之後,對於參數的傳值或傳址就會越來越注重。也許是我讀的JavaScript的書不夠多,像這種基礎的概念居然都不是從書上得來,而是在網路上找尋各個程式設計師的心得與探討之後,再整理出來的。
題外話,因為這篇主要是整理理論的東西,沒什麼圖片。秉持著一篇文章一定要有一張圖的精神,就去ICON FINDER找了張JavaScript的圖示來貼在開頭XD 而最近都在談JavaScript的東西,因此我也為這個Blog加入了JavaScript的分類標籤。之前的發文就待有空時再來整理歸類到JavaScript標籤去吧。

參考資源

總共31 則留言 ( 我要發問 , 隱藏留言 顯示留言 )

  1. 寫的很不錯....
    借我轉錄哦

    回覆刪除
  2. 感謝你的稱讚!我會繼續努力的!

    回覆刪除
  3. 版主寫的真好!學習到了不少~~想請教一下,為什麼字串會是傳值咧,而非傳址這一點我一直搞不清楚。難不成字串也是基本型態,在javascript裡。

    回覆刪除
  4. 字串是基本型態而非物件沒錯(JavaScript不是Ruby啊),但問題點並不是要指定給的值的類型,而是你是修改的是變數本身還是變數(物件型態)的屬性

    回覆刪除
  5. 關於這個傳址還是傳值的問題,我最近在看Learning Python的時候,發現Python也會有非常類似的問題(感覺上Python的資料型態就是跟JavaScript很像)

    關注的重點並不是傳過去的資料,而是接受的資料是怎樣的類型。

    物件的屬性、陣列的某一個索引,他們儲存的都是「記憶體的位置」而已。
    因此當記憶體位置被替換時,所有用同一個物件、陣列的程式都會受到影響。

    這是非常嚴重的問題,但只要能夠好好認識、熟悉程式語言的特性,我們依然可以快樂地和他相處喔!

    回覆刪除
  6. 在function的參數欄位上宣告的參數是一個變數值,亦即該function的私有變數;該變數值被投入值之後該值就指向投入值的物件的記憶體位置,而function中將參數欄位變數值重新給予一個值,這時候指向的就是新的值,所以....Js一直都是傳址呼叫,因為就連String等這些被一般語言認定的基本變數型態,其實也是衍生自Object,而且傳址呼叫比傳值還是快很多

    回覆刪除
  7. To 7樓匿名:

    原來如此,感謝你的說明。

    回覆刪除
  8. 意外找到這篇文章
    超詳細的感謝大大精闢的分析

    回覆刪除
  9. To 9樓匿名,

    不客氣。
    不過請一併連著下面comment一起看,下面延伸討論也很有一看的價值。

    回覆刪除
  10. 版主參考一下
    http://msdn.microsoft.com/zh-tw/library/d53a7bd4(v=vs.94).aspx

    回覆刪除
  11. 如果更簡單地理解的話, 就是基本型態跟物件型態在stack裡面存的東西不同,
    基本型態(string, number, boolean, undefined, null)直接存值, 而物件型態存的是指標.
    當參數傳遞的時候, 是copy一份stack裡面的值到function的scope裡, 因而造成不同的結果...

    回覆刪除
  12. To YC

    你說的沒錯,分清楚「基本形態」跟「物件形態」是很重要的

    回覆刪除
  13. 哇!! 這是我最需要的基礎教學
    感謝版主詳細的解說!!!!

    回覆刪除
  14. 版主您好,不小心路過此處,
    內容很豐富,但在11樓和13樓的留言之後,
    不太瞭解為何版主您的結論還是
    『我認為JavaScript的本質是傳址而非傳值』

    事實上,13樓所述的意思與您剛好相反,
    即是『JavaScript的本質是傳值而非傳址』,
    有時候會有傳址呼叫的感覺,
    是因為在javascript裡,object的值就是這個object的址
    可參考w3schools的描述:
    http://www.w3schools.com/js/js_function_parameters.asp

    除了提到Arguments are Passed by Value外,
    最後一段有兩條加粗的句子,簡單說可以是修改參數的規則:
    1. Changes to arguments are not visible (reflected) outside the function.
    2. Changes to object properties are visible (reflected) outside the function.
    改變傳入的arguments對函式外是不會有任何影響的,
    但如果傳入的是object,更改object的properties後在函式外也會看到其變動,
    因為函式內得到的是object的位址,
    更改object的properties只是到那個位址去修改值,這是被允許的,
    但即使傳遞的是object,第一條規則仍需遵守,
    所以在您最後舉的例子中,產生一個新陣列指派給paramAry,
    是想要改變函式外傳進來的『值』,
    這種更改對函式外的變數來說是不會有影響的。

    在11樓連結的網頁也有一段解釋:
    『雖然物件和陣列是以傳址方式來傳遞,但如果您直接在函式中以新值來覆寫它們,
    則新值並不會反映到函式以外。只有對物件屬性或陣列元素的變更才能在函式以外顯示。』
    那意思其實也是一樣的。

    JavaScript近幾年越來越重要,很多觀念也越來越清楚,
    希望版主能將文章稍做修改避免讓剛學習的人一開始就有所混洧,
    感謝。

    回覆刪除
  15. To 花生狼,

    您說的非常好,我把說明加到原文裡面去了。
    感謝您!

    回覆刪除
  16. 13樓+1
    傳變數的值,只是變數裡面有時候裝的值是址(例如物件),再來就是scope問題,function外的變數是global,內部是local(含function宣告時定義的參數),

    版主的例子把function外的變數和參數取相同的名稱,但即使名稱相同,仍是scope不同的兩個不同變數,以第一個傳物件的例子來說,呼叫函數時傳入的paramB其scope是global,其內部存的物件scope也是global,在呼叫函數的過程中global的paramB把其值,也就是其中的物件傳入存到函數的local變數paramB中,此時函數中的paramB只是裝著global物件的local變數,

    第一個例子修改的paramB.attr這個屬性其scope是global的,所以改了之後函數外面也有效果,而第二個例子裡面重新指定新物件的動作,其實只是替換掉了local 的paramB裡面的東西,把原本的global scope的物件換成了新指定的local物件(函數中宣告的物件scope是local),此時函數裡面的paramB變數和其內部儲存的物件scope都是local,已經和外面的東西完全無關了,只是名稱一模一樣的兩個不同變數,分別裝著一模一樣但scope不同的兩個物件,後面的array例子也是相同的原因

    回覆刪除
    回覆
    1. 我想問題應該不是scope。
      如果你說是在function外面的global scope宣告變數,在傳入function中處理的話,那第一個例子var paramA = 'original'; 就應該會被影響。

      我還是覺得這是關係到傳送給function的是「基本形態」(number, string, boolean)還是「object」(包括array,這也是一個object)。
      前者會傳值,function內修改變數不會影響到function外
      但是後者是傳址,JavaScript傳送的是object的位址,而非複製一個新的object

      大概就這樣的差別,應該不是scope的問題啦。

      刪除
  17. 第一個例子傳進函數的是值,而非global 變數本身的位址,在裡面被修改值的是local的paramA,並未影響global的paramA,所以外部讀其值還是original,不受影響,詳細點說就是var paramA = 'original' , 宣告了一個scope global的變數paramA,並賦字串值 'original' 然後宣告了changeParamA函數,並定義一個參數名稱也叫paramA,但這是函數的local變數,與global的paramA不同,接下來呼叫changeParamA函數,並傳入外部global變數paramA的值,也就是字串 'original' 存入內部local變數paramA, 接著在函數內部對 local 變數 paramA做重新賦值的動作,賦予其新字串'changed'覆寫掉字串'original' ,這個動作並未不會影響外部的global 變數 paramA的值,因為雖然變數名稱相同,但兩個變數scope不同,位址也不同,對內部local paramA修改其值不會影響外部的global paramA的值,在外部用document.write(paramA + '
    ');讀到的值當然還是字串'original'囉,為何應該會被影響呢? 不論傳入的是基本形態的值還是物件,造成範例結果的原因是在修改到的是global變數的位址的值還是local變數的位址的值這個層面,所以我才會說跟scope有關,如果有誤還請指正^^

    回覆刪除
  18. 作者已經移除這則留言。

    回覆刪除
  19. 作者已經移除這則留言。

    回覆刪除
  20. 作者已經移除這則留言。

    回覆刪除
    回覆
    1. 您想用scope的方式來解釋也可以啦……
      總之參數如果是基本形態,那就是local scope

      看來看去,影響關鍵還是要看傳送的是基本形態還是物件。
      但是這時候還是回歸到傳值還是傳址比較容易理解,scope反而讓人覺得複雜許多。

      刪除
  21. 我看法直觀 通俗一些, 如下:
    如果說 知道 這變數型態的長度的話, 那傳值無妨, 因為兩邊都配一樣長度, 內容直接 copy 過去了
    如果說 壓根不知道 這變數型態的長度, 那唯一還能確定長度的就只剩存放這變數的"址", 所以就只好兩邊 都配 一樣的 "址"的長度, 把"址" copy 過去了

    到底傳值 還是 傳址 都是依據 "能否知道該變數的長度" 而定

    這應該是一般 language 都會遇到的問題啦

    回覆刪除
    回覆
    1. 倒是不能這樣理解

      這篇在講的就是JavaScript的特性而已
      JavaScript有些它自己的原罪

      刪除
  22. 哥,可以了解一下淺拷貝。 參數進函式後基本上該參數就是淺拷貝一份。 陣列是JS中特別的物件,他本值還是物件

    回覆刪除
    回覆
    1. 10年前我對JavaScript的認知還蠻粗淺的。就現在來看,我也覺得這篇寫的觀念並不正確。

      近年來JavaScript開始大幅度進化,甚至也有了class這個語法糖
      https://shubo.io/javascript-class/

      再加上現在Node.js中對於import、require、export方式不同,都會讓物件以不同的方式運作
      比起10年以前,現在真的複雜很多

      真的是很感慨技術變化如此快速

      刪除
  23. 又來看誤人子弟的文章 朝聖

    回覆刪除