# 關於 Assignment (賦值) 這檔事

# Assign Value 這稀鬆平常的事情,卻常常會暗埋陷阱

最近朋友跟我討論到他在公司 Code Review Javascript Project 時對於 Javascript 賦值的「詭異行為」不明所以。

這讓我想到,在我過往只要接觸一門程式語言,我總要先去特地了解一下這門語言的「賦值行為」,因為每種語言對於賦值的行為總有些為差異。

# 「Pass-by-value」, 「Pass-by-reference」, 「Pass-by-sharing」 還是,「Pass-by-assignment」?

我們時常聽到程式的賦值行為有以下兩種定義:

  • Pass-by-value: 複製參數的值傳入,所以原參數內容不會被影響。
  • Pass-by-reference: 傳入參數的參考 (或是人們常說的 傳入記憶體位置 ),所以原參數內容會被影響。

以下我們以 C 語言為例

#include <stdio.h>
int pass_by_reference(int *val) { *val = 3; }
int pass_by_value(int val) { val = 4; }
int main() {
    int normalVal = 5;
    printf("We determine a value called `normalVal` = %d\n", normalVal);
    pass_by_value(normalVal);
    printf("After the function `pass_by_value` is called, the `normalVal` = %d\n", normalVal);
    pass_by_reference(&normalVal);
    printf("After the function `pass_by_reference` is called, the `normalVal` = %d\n", normalVal);
}

可以看到,當使用 pass_by_value 的方法將 value 傳入 function 時, function 內部無論變數的 value 如何變更,都不會影響到外部;當使用 pass_by_reference 的方法傳入時, function 內部將參數的 value 變更,外部也會受到影響。

然而在 Javascript 與 Python 以上兩種語言中,參數的傳遞又好像不是單純依照這兩種準則運行,因此常常看到有人說 「Javascript 是 Pass-by-sharing」「Python 是 Pass-by-assignment」

到底什麼是 Pass by sharing ? 什麼是 Pass by assignment ?

我們以以下三種情境來展示這四種行為的在 Javascript 與 python 的差異

情境一:傳入一個物件,在 function 內部改變物件的內容,不做回傳的動作

以下是 Javascript 行為

/** Javascript */
var obj = {"a":1, "b":2};
function ModifyObjet(object) {
    object.c = 3;  // assignment
}
ModifyObjet(object);
cobsole.log(obj); // {"a":1, "b":2, "c": 3}

這是 Python 的行為,看起來結果跟 Javascript 一樣, 「在改變 object 內容後 obj 的內容都會改變」 ,對吧?

// Python
var obj = {"a":1, "b":2};
def Modify_Objet(object):
    object["c"] = 3;
print(obj); // {"a":1, "b":2, "c": 3}

在這情境下, Javascript 跟 Python 展現出來的結果看起來就像是 Pass by reference。

接下來看看情境二

情境二:傳入一個物件,並且重設傳入的物件的內容,一樣不做回傳的動作

以下是 Javascript 行為

/** Javascript */
var obj = {"a":1, "b": 2};
function changeContain(object) {
    object = {"d": 4, "e": 5};
}
changeContain(obj);
console.log(obj); // {"a": 1, "b": 2};

以下是 Python 行為

// python
obj = {"a":1, "b":2}
def Change_Contain(object):
   object = {"d": 4, "e": 5}
Change_Contain(obj)
print(object) // {"a":1, "b":2}

我們發現,在 function 內重新對 object 賦值,並未改變 obj 的內容,表現得就像 Pass-by-value 一樣

剛剛不是說 Javascript 跟 Python 是 Pass-by-reference 嗎? 怎麼這兩個 function 的結果表現,又跟 Pass-by-value 一樣了?

這兩個看起來一樣的動作 (修改變數 object 的值) 卻展現不一樣的結果,時常讓人覺得困惑。

我們用 Python 來解釋吧~

可以做一點實驗: 將 python 中的變數的記憶體位置印出來,確認是否相同。

  • 僅修改 obj 內部的值,而不是直接重新賦值。

上圖是將 object a 傳入 function 中,修改變數 a 「內部」的值。

a 的記憶體位置與 obj 的記憶體位置相同,但是我們可以更進印出 obj[“a”] , obj[“b”] 的記憶體位置,並與 a[“a”] , a[“b”] 的記憶體位置比較,會發現 obj[“a”] , obj[“b”] 記憶體位置改變了。

  • 對 obj 直接重新賦值,而非修改 obj 內部的值。

上圖是將 object a 傳入 function 中,修改變數 a 值。

a 的記憶體位置與 obj 的記憶體位置相同。

由此,我們可以歸納出一個結論:

當變數 a 傳入 function 中時, 變數 obj 指向變數 a 的所指向的記憶體位置;當我們重新賦值給變數 obj 時,變數 obj 指向新值的記憶體位置。

同理,當我們改變 obj[“a”], obj[“b”] 的值時,也是將 obj[“a”], obj[“b”] 指向新的記憶體位置。

因為 python 中變數依照 「assign」的對象改變記憶體位置,因此有一說為 「pass-by-assignment」

同理,回到 Javascript:

由於 Javascript 事實上由 browser(若為 NodeJs 則為 V8 Engine)翻譯,因此我們無法直接觀看變數的記憶體位置。

但是我們可以藉由剛剛觀察 Python 的行為發現,其實兩者的操作邏輯是相同的,但由於 Javascript 並沒有真正意義上的記憶體位置,有的僅是「共享相同的變數空間」,所以有了 Pass-by-sharing 的說法。

# 結論

在寫這篇文章時,查詢了很多文獻,發現也有人講説 Javascript always pass by value

其實關於 assignment 這件事,很多時候定義都有些微的不同。依照分析的出發點不同解釋的角度也就不盡相同,但是描述的事情是一樣的。

本篇文章是依照我個人對 Javascript , Python 的理解、實驗的結果與查詢的資料,統合歸納出來的結論。

可能有些未盡完善的部分,若有指證,非常希望能留言讓我知道。

我很推薦「 深入探討 JavaScript 中的參數傳遞:call by value 還是 reference? 」這篇文章,應該會比我說得再更清晰明瞭。

# 參考文獻

  • 深入探討 JavaScript 中的參數傳遞:call by value 還是 reference?
  • [Javascript] Pass By Value And Pass By Reference In JavaScript
  • Pass By Sharing in Javascript (and Why it Matters)
  • Python 的參數傳遞是 call-by-assignment

Like z20240z's work