Day 27:別管變數 Pass by Whatever,尋找容易理解的銀色子彈 (Silver Bullet)

(Source: 網路圖片)

昨天的文章談到 Pass by value 和 Pass by reference。

一個程式語言的變數運作機制究竟是 Pass by value 還是 Pass by reference,不管在 JavaScript 或其他語言,一直都爭議不斷。

而爭議不斷的癥結點源自於名詞定義和觀點的不同。

技術名詞是為了描述概念而存在,而不是概念為了技術名詞而存在

不需要對名詞字眼鑽牛角尖,最重要的是背後的概念,也就是體現出來的「行為」

以行為結果來看,JavaScript 的變數行為大致可以歸納成以下:

  • 碰到原生型別 (Primitive),表現行為是 Pass by value。

  • 碰到物件型別 (Object),如果只是對物件內容作操作(例如陣列元素或物件屬性),表現行為是 Pass by reference。

  • 碰到物件型別 (Object),如果對物件作重新賦值,表現行為是 Pass by value。

知道行為結果,但說不出為什麼

然而上述的行為整理對我而言,只是表面行為的描述,難以對背後原理提出一個解釋或理解模式。

例如同樣是物件型別 (Object),為什麼重新賦值和非重新賦值的狀況,表現行為會不同?

也許深入鑽研到系統底層核心對記憶體的操縱機制,自然能提出完美解釋,但這對大多數上層應用程式開發者來說成本太高。

就算無法解釋為什麼,只要記熟上述行為結果,大概也足以應付程式中變數行為的推導。

但我對這種死記硬背規則的方式滿抗拒的。

尤其軟體開發要記的知識已經太多,身為一個金魚腦開發者,必須盡量用「理解」取代「硬記」,否則即使短時間內記得,也容易隨著時間變得印象模糊,更別提「硬記」這件事本身有多累。

(Source: 網路圖片)

對於情境千變萬化的議題,我偏好試著去找出一個簡單、容易掌握、而且能夠自圓其說的模式,無論遇上任何情境都能用同一個模式推導出正確行為,就像一顆銀色子彈

銀色子彈(英語:Silver bullet)是一種由白銀製成的子彈,有時也被稱為銀彈。在西方的宗教信仰和傳說中,它作為一種武器,成為唯一能和狼人、女巫及其他怪物對抗的利器。

銀色子彈也可用於比喻,喻作強有力的,一勞永逸的,適應各種場合的解決方案。

(引用自維基百科)

推導變數行為的銀色子彈:盒子圖像概念

我對銀色子彈的條件: 1. 化繁為簡,必須依靠記憶的前提規則越少越好 (否則叫金魚腦怎麼辦)。 2. 內容盡量質樸,越具體、越簡單、越貼近生活經驗,就越容易理解和運用,能圖像化更佳,有時候甚至帶點稚氣也無所謂 (有趣的童書和沉悶的論文,我更願意擁抱前者)。 3. 能適用所有情境,或至少必須適用絕大部分情境,僅需額外備註極少數的例外 (否則也不稱其為銀色子彈)。

針對「推導各種程式碼情境的變數行為」,我的銀色子彈就是「盒子圖像概念」。

其實這個思路我不認為是什麼創建,很多文章或教學講述的都是類似的概念。

這篇文章比較像進行一個明確的歸納整理,套上盒子這種貼近生活的比喻,搭配圖像化的理解,期望讓這個概念成為一個具體且親切的模式,能被簡易且有系統地運用和分享。

根據我的經驗,到目前為止碰到各種令人困惑混淆的程式碼情境,都可以用這套「盒子圖像概念」有個說得通的解釋,順利推導變數行為,幫助 Debug。

必須先聲明一件事,避免造成誤解:這個「盒子圖像概念」純粹是對程式上層行為一個概念式的理解方法,用於上層變數行為的推導,不代表系統底層對記憶體位址的實際操作原理

對於深諳底層系統機制的高手來說,這個概念在許多地方的理解或描述可能是十分粗糙、遠遠不夠精確。

但以「推導變數行為」的目的而言,相信可以達到一定效果。

俗話說,黑貓白貓,能抓老鼠的就是好貓。

這顆銀色子彈也許不夠精美,最重要的是希望能幫助變數行為的程式碼撰寫更加容易。

盒子圖像概念的起手式

記憶體每一塊空間是一個盒子

記憶體有很多個儲存空間,就像一個個不同的盒子 (或抽屜的概念也行)。

  • 盒子的名字 => 變數名稱

  • 盒子編號是第幾號 => 記憶體位址

  • 盒子裡面的東西 => 記憶體儲存的值

比如這樣一個儲存資料的變數:

用盒子的圖像概念來思考:

宣告變數就是跟電腦討一個盒子

例如下面是宣告變數的動作:

已宣告變數但還沒賦值之前,盒子內的值就是 undefined

使用實字 (Literals) 或 new 關鍵字,其實都是跟電腦討一個匿名盒子

實字 (Literals) 包含各種型別,例如數字實字、字串實字、陣列實字、物件實字等。

此外,new 這個關鍵字,就意涵著「跟電腦取得一塊新的記憶體」。

但光是討了盒子沒有用,我們不知道盒子的編號,無法將資料取出來用,匿名盒子對開發者來說沒有用

在高階程式語言裡,記憶體位址幾乎不可控,通常由電腦分配。所以需要把這些資料進一步傳到有名字的盒子裡,才能受開發者掌控

將資料放進有名字的盒子

由於電腦系統底層對記憶體儲存資料的機制,根據資料的型別不同,盒子間傳遞值的方式有兩種狀況: 1. 原生型別 (Primitive):

  • 簡單型資料可以直接複製一份進變數盒子內。

  • 這種類型我們稱為 Pass by Value

    1. 物件型別 (Object):

  • 複雜型資料無法直接複製進有名字的盒子內,所以只會把「資料的位址」放進變數盒子,讓程式自動根據位址去找到擁有實際資料的匿名盒子。

  • 這種類型我們稱為 Pass by Reference

沒有用的匿名盒子很快會被消滅

俗話說限量是殘酷的,記憶體空間有限,為了能讓記憶體作最大化的運用,系統會回收已經用不到的盒子,以便提供給下一個人使用。

用不到的盒子的標準是什麼?就是沒機會再被呼叫到

匿名盒子對開發者來說就是用不到的盒子,因為沒有辦法呼叫使用,在完成複製資料的任務之後,匿名盒子就會被系統回收走。

發現有一個位在 0x093 的匿名盒子倖存下來,為什麼?

因為這個匿名盒子和變數 person 之間有著引用關係 (Reference),這個關係就像一條隱形的紅線,牽起兩個盒子之間親密的關係。

(Source: 網路圖片)

透過變數 person,還有機會存取到 0x093 盒子的資料,因此 0x093 盒子還不會被回收。

換句話說,只要身上還綁著紅線 (引用關係),代表還被別人需要,盒子就不會被回收

引用關係 (Reference) 可以取消或改變

就像紅線可以被斷,引用關係可以被取消或移情別戀。

例如下面的例子:

不管是將 person 指派原生型別的資料,或是給予新的引用關係,都代表原本的引用失效,使得匿名盒子不再被任何人引用,進而被回收。

只要還存在被別人需要的引用關係 (Reference),匿名盒子就不會被回收

反之,如果不存在被需要的引用關係 (Reference),匿名盒子就視同被消滅

(Source: 網路圖片)

盒子圖像概念新手村結束,準備上戰場

以上的概念都還滿單純,運用的幾乎都是多數開發者早已熟知的基礎概念。

但已經足以解釋為什麼 Pass by Reference 的情況下,函數參數有時候會變更到外部參數、有時候不會。

(A) 會變更到外部參數的範例

範例程式碼如下:

執行結果:

我們跟著程式碼,用盒子圖像概念逐步來看。

1. 宣告 person 並給予初始值

這時候還很單純,就是一個變數盒子 person 引用一個匿名盒子,匿名盒子裡存著實際資料 { name: "OneJar" }

如下圖示意:

2. 呼叫函數 rename(),並傳入 person 作為參數

這短短的一行程式其實作了很多事,這裡要用慢鏡頭分解。

2.1. 建立一個函數執行環境 (Function Execution Context)

當呼叫某個函數執行時,JavaScript 首先會建立一個新的函數執行環境 (Function Execution Context)。

函數的參數名稱也是一種變數宣告,因此這裡會產生一個名為 obj 的變數盒子,值是 undefined

2.2. 承接傳入的參數

obj 的盒子建立後,就能去承接傳進來的參數 person 的值。person 的值是一個位址 0x093,所以 obj 內存的也是位址 0x093

變數 obj 透過位址,等於和 0x093 的匿名盒子建立紅線,因此可以存取到實際資料 { name: "OneJar" }

3. 執行函數內的操作物件

前置作業完成後,就可以正式來看函數內的執行動作:

這裡要對變數 obj 的屬性 name 作操作。

由於透過 obj,存取的實際資料是位址 0x093 盒子,因此變數 person 印出來的資料也變成 {name: "XXX"}

(B) 不會變更到外部參數的範例

範例程式碼如下:

執行結果:

同樣用盒子的概念來看這個例子。

前面部分是一樣的,我們可以稍微快轉到「2.2. 承接傳入的參數」之後:

3. 執行函數內的操作物件

這裡一樣用慢鏡頭來看。

3.1. 為物件實字產生一個匿名盒子

先跟電腦要一個匿名盒子來暫時儲存 { name: "XXX" } 這份資料:

3.2. 將匿名盒子的資料傳給變數盒子 obj

由於匿名盒子裡裝的是物件型別的資料,無法直接複製一份進變數盒子,因此是存入一個位址的值。

可以看到,其實從這一步開始,變數 objperson 就已經沒有任何牽連,因此最後 person 印出來仍是 {name: "OneJar"}

總結

JavaScript 究竟是 Pass by value、Pass by reference、還是 Pass by sharing,我想在有一個權威性單位定出一個明確的定義觀點之前,恐怕很難有爭執完全結束的一天。

但比起在技術名詞上鑽牛角尖,我更傾向找到一個容易掌握的模式,增加對行為上的理解。

畢竟對現實的專案開發來說,比起訂立一個漂漂亮亮的技術名詞,確保對程式行為的掌握才是首要任務。

在「盒子圖像概念」裡,Value 採用「資料的內容」的觀點角度,分為 Pass by value 和 Pass by reference 兩種行為

根據上面的逐步示範,Pass by reference 完全能夠解釋為什麼有時會影響到外部變數、有時不會,並不需要額外的 Pass by sharing 讓事情更複雜。

目前為止,我個人碰到各種令人混淆的程式碼情境,都可以套用「盒子圖像概念」來解釋和推導,幫助 Debug。

事實上這個概念我仍持續在嘗試補充,因為有些細節的解釋總覺得還不夠圓滑。

如果看倌發現有程式碼情境是這套模式無法解釋,歡迎分享,我很願意修正這套模式。

就像技術名詞的目的是為了表達概念,銀色子彈是為了解決問題,重要的不是它們的存在本身,而是它們能不能達到目的

References

Last updated

Was this helpful?