Skip to content

2026-10-16

/ 18 分鐘閱讀

/ Functional JavaScript First Steps

不可變資料:用 map 取代原地修改,認識持久化資料結構 ​

這一篇進入新章節 Immutability。前面的章節一路強調純函數與無狀態,這一節把同樣的要求延伸到資料本身:不要修改資料。

什麼是修改資料 ​

函數式程式設計希望程式具有決定性,盡量避免「東西在背後被改掉、自己卻沒預料到」的情況。純函數是這個想法的一部分,而它的另一個結果,就是要避免修改(mutate)資料。

講師開玩笑說,這裡的 mutate 不是指資料被放射性蜘蛛咬了、獲得超能力,而是指原地(in-place)修改資料:同一個實體,例如同一個陣列,裡面的值變得不一樣了。這種可以原地修改的性質就是 mutability。

在不可變(immutable)的世界裡,不做這件事。假設函數接收了一個陣列,絕對不去動那個原始陣列,而是用一個更新過的新版本取代它,原始版本保持原樣。

範例:替城市改名 ​

講師用一個很簡單的陣列當例子,裡面是幾個印度城市的名字。其中有些名字非常過時,帶有殖民色彩,已經不再使用。例如講師父親長大的城市,在講師小時候叫 Bombay,現在則叫 Mumbai。所以這份資料需要更新。

原地修改的寫法 ​

會修改資料的寫法,是直接把新值 'Mumbai' 指派給陣列索引 1 的元素。這在 JavaScript 裡應該很常見:

js
let cities = ['Delhi', 'Bombay', 'Bangalore'];

cities[1] = 'Mumbai';

cities; // ['Delhi', 'Mumbai', 'Bangalore']

現在 cities 不一樣了:之前裡面有 Bombay,現在沒有,換成了 Mumbai。但對 JavaScript 來說,cities 本身還是同一個陣列,指向的是記憶體裡的同一個東西。

問題出在狀態 ​

一個陣列或任何值在背後被改掉,就代表又出現了狀態:一個會隨時間改變的值。同一個 cities,現在指的卻是不同的資料。

如果有個函數在更新前、更新後各用了一次 cities,就會遇到有狀態程式帶來的頭痛問題。這正是迭代與遞迴那篇說函數式要避免狀態的原因:值會隨時間改變,就沒辦法光看輸入預測結果。

不修改的寫法:用 map 產生新陣列 ​

比較好的做法是不更新舊的物件(這裡是陣列),而是建立一個裝著正確、更新後資料的新陣列。其中一種方式是用內建的 map,它相當純粹,會回傳新陣列、不會修改原陣列。也可以用前面自己實作的 map:

js
const oldCities = ['Delhi', 'Bombay', 'Bangalore'];

const newCities = oldCities.map((city) => {
    if (city === 'Bombay') return 'Mumbai';
    if (city === 'Bangalore') return 'Bengaluru';
    return city;
});

newCities; // ['Delhi', 'Mumbai', 'Bengaluru']
oldCities; // ['Delhi', 'Bombay', 'Bangalore']

map 會拿 oldCities 裡的每個元素,判斷要不要更新,需要的話就回傳對應的新值:

  • Delhi:還是 Delhi,照原樣回傳
  • Bombay:改名了,回傳 Mumbai
  • Bangalore:改名了,回傳 Bengaluru

傳進高階函數 map 的內層函數就是轉換函數,只負責接收舊資料、回傳新資料。map 會對陣列裡每個元素做這件事,最後回傳一個套用了轉換的新陣列。

舊資料完全不變 ​

關鍵在於 oldCities 從頭到尾都沒變。假設同事正在用 oldCities,而且要是所有值都被換掉,同事的測試就會失敗,現在這些測試依然沒問題。oldCities 還是 oldCities,newCities 則是以舊資料為基礎產生、可以拿來用的新資料。

一直複製的代價 ​

這時可能會有疑問:陣列只有三個元素時當然沒問題,但每次用 map、filter 或 reduce 都會回傳新陣列,代表要騰出更多空間放更多資料。舊資料留著、新資料又加進程式裡,全部都得記在記憶體中,會開始佔掉不少空間。

資料很複雜時,效率也可能變差。這跟遞迴那篇遇到的狀況類似:輸入越來越大時,就開始出現效能問題。「做一份修改過的新副本,而不是修改原始資料」這個策略,在時間與空間上都可能變得沒效率。

不可變資料結構 ​

許多函數式語言與工具都有一種策略(或者說一種東西)來處理這個問題,通常稱為不可變資料結構(immutable data structures),有時也叫持久化資料結構(persistent data structures)。

以這個例子來說,它是一種特殊的陣列,也可以想成特殊的物件之類的。替它建立副本時,比每次把陣列裡的每個元素全部複製一遍更有效率。講師覺得這是非常酷、非常有趣的實作細節,但這次不會花太多時間在上面。

用一般的資料結構時,更新一個有很多元素的陣列,就得配置一整個同樣長度的新陣列。不可變資料結構則內建了特殊的最佳化,只需要複製有變動的部分。

表面是陣列,底下是樹 ​

持久化資料結構表面上看起來像陣列,底層其實是一棵樹。講師用一個動物園的例子說明:

  1. 有一個叫 zoo 的陣列,依序是猴子、兔子、熊貓、熊,後面還有其他動物
  2. 一架 UFO 降落,把兔子壓扁了,但地球上多了一隻新動物:外星人
  3. 想做出一個只改一個值(兔子變外星人)的新陣列 new,但不想把所有沒變的東西都重新複製一份

持久化資料結構會把值分組,因為對每個值個別處理不一定有效率。假設這裡是兩個一組,建立 new 時,只要更新樹裡那個裝著兔子的節點,換成外星人。過程中還是有一點點複製,同一組的猴子被複製了一份。

除此之外,新的資料結構直接指向記憶體裡原本就有的其他舊資料,不必再建立一份新副本。

省下的是什麼 ​

靠著底層這些很聰明的技巧,複製一個只改了一個值的新陣列會快很多。原陣列裡沒變的那些元素,不再需要花時間一個個配置新的記憶體,可以跳過所有沒變的部分,只更新需要更新的地方。講師說這個主題還有很多可以講,課程的延伸閱讀裡有相關連結。

JavaScript 沒有內建 ​

有些函數式語言內建了這種資料結構,例如 Clojure,但 JavaScript 不是其中之一,沒有開箱即用的功能。

幸運的是,JavaScript 有一個龐大的生態系,很多人也想在 JavaScript 裡寫函數式程式,所以已經有人實作了這類東西,可以直接拿來用。

複習 ​

程式設計裡的資料可變性(mutability)是什麼? ​

原地修改資料,例如直接修改陣列裡的值,而不是建立新陣列。在不可變的程式設計裡會避免這種做法,改成建立一個更新過的新版本資料。

函數式程式設計怎麼更新陣列裡的值? ​

不原地修改原始陣列,而是用 map() 這類方法建立一個包含所需變更的新陣列。map() 會回傳新陣列,不會修改原本的資料。

什麼是不可變資料結構或持久化資料結構? ​

一種特殊的資料結構,建立更新過的資料副本時,只複製並更新有變動的部分,藉此最佳化記憶體用量。通常實作成樹狀結構,共用沒有變動的資料參照。

為什麼在函數式程式設計裡,避免修改資料很重要? ​

修改資料可能造成難以預測的狀態變化,讓程式比較不具決定性。同一份資料在程式不同地方、不同時間點被使用時,也可能因此出問題。

持久化資料結構怎麼在產生更新資料時提升效能? ​

它利用巧妙的樹狀結構,只複製並更新有變動的元素,不需要配置與複製整個資料結構,因此減少了記憶體配置與處理時間。

在不可變資料結構的脈絡下,要用什麼策略有效率地建立更新後的陣列? ​

只複製資料結構中有變動的部分。

持久化資料結構的關鍵特性是什麼? ​

透過參照沒有變動的資料,把記憶體配置降到最低。

更新陣列裡的城市名稱時,示範了哪一種函數式程式設計技巧? ​

用 map 建立新陣列,而不是修改原本的陣列。

不可變資料結構幫忙解決了函數式程式設計裡的哪個挑戰? ​

建立新資料結構帶來的效能負擔。

小測驗 ​

程式設計裡的資料可變性(mutability)是什麼? 原地修改資料,例如直接修改陣列裡的值,而不是建立新陣列。在不可變的程式設計裡會避免這種做法,改成建立一個更新過的新版本資料
函數式程式設計怎麼更新陣列裡的值? 不原地修改原始陣列,而是用 map() 這類方法建立一個包含所需變更的新陣列。map() 會回傳新陣列,不會修改原本的資料
什麼是不可變資料結構或持久化資料結構? 一種特殊的資料結構,建立更新過的資料副本時,只複製並更新有變動的部分,藉此最佳化記憶體用量。通常實作成樹狀結構,共用沒有變動的資料參照
為什麼在函數式程式設計裡,避免修改資料很重要? 修改資料可能造成難以預測的狀態變化,讓程式比較不具決定性。同一份資料在程式不同地方、不同時間點被使用時,也可能因此出問題
持久化資料結構怎麼在產生更新資料時提升效能? 它利用巧妙的樹狀結構,只複製並更新有變動的元素,不需要配置與複製整個資料結構,因此減少了記憶體配置與處理時間
在不可變資料結構的脈絡下,要用什麼策略有效率地建立更新後的陣列? 只複製資料結構中有變動的部分
持久化資料結構的關鍵特性是什麼? 透過參照沒有變動的資料,把記憶體配置降到最低
更新陣列裡的城市名稱時,示範了哪一種函數式程式設計技巧? 用 map 建立新陣列,而不是修改原本的陣列
不可變資料結構幫忙解決了函數式程式設計裡的哪個挑戰? 建立新資料結構帶來的效能負擔

此文章是 FrontendMasters 上的 Functional JavaScript First Steps, v2 課程筆記

最後更新時間:

Buy Me A Coffee

系列章節 第 19 篇 / 共 21 篇

0 %
MIT Licensed | Copyright © 2025-present Wen-Hsiu's Blog
Photo by Federica Galli on Unsplash