超級無敵重要的 Immutable 觀念


Posted by saffran on 2021-02-05

  • mutation 的意思是「突變,變異」 noun
  • immutable 的意思是「不可改變的」 adjective

參考資料 JavaScript data types and data structures

Primitive values

除了 object (也就是陣列、物件)之外的東西,基本上都是 primitive types (原始型態)

屬於 primitive types(例如 number, string, boolean)的值就是 immutable values(不可變的值),也被稱為「primitive values」

對於 primitive types 的東西,永遠都不會這樣寫 str.toUpperCase(),因為:如果只是呼叫 function,是絕對沒辦法改變 str 的值

str.toUpperCase() 只會回傳新的字串,所以要再把新的字串重新賦予給另一個變數: var newStr = str.toUpperCase()newStr 才會是「轉成大寫後的字串」

接下來,會用一些範例來說明 immutable values 的觀念

範例

a 這個變數是「可以改變的」
但是 'hello' 這個字串是「不會改變的」

var a = 'hello'
a = 'tree'

意思是:

第一行 var a = 'hello'

一開始給 a 的值是 ‘hello’
'hello' 這個字串存放的「記憶體位置」假設是 0x01

var a = 'hello'
// a: 'hello' 0x01

第二行 a = 'tree'

a = 'tree' 時,並不是「把 0x01 上面的 ‘hello’」直接改成 'tree'

而是會把「新的字串 'tree'」用 = 重新賦予給變數 a (新的字串 ‘tree’ 會放在一個新的記憶體位置 0x02 上)

所以,變數 a 的值最後會是 'tree'

注意!原本在 0x01 的 'hello' 是「不會改變的」

var a = 'hello'
// a: 'hello' 0x01

a = 'tree'
// a: 'tree' 0x02

如果第二行是 a = a + 'tree'

var a = 'hello'
// a: 'hello' 0x01
a = a + 'tree'
// a: 'hellotree' 0x02

同樣地:

  • 第一行 var a = 'hello'
    會創造一個「記憶體位置 0x01」來存放 'hello' 這個字串
  • 第二行 a = a + 'tree'
    會先執行 a + 'tree',回傳一個新的字串 'hellotree',再把 'hellotree' 用 = 重新賦予給 a (新的字串 'hellotree' 會放在一個新的記憶體位置 0x02 上)
  • 所以,變數 a 的值最後就是 ‘hellotree’

注意!原本在 0x01 的 'hello' 是「不會改變的」

「不可變」的特性,會用在哪裡呢?

範例一

錯誤寫法 a.toUpperCase()

錯誤的想法:會認為「只要呼叫 a.toUpperCase() 之後」,a 就會轉成大寫

但其實,只呼叫 a.toUpperCase() 並不會把 a 轉成大寫(不會改變 a 原本的值)

因為:
'hello' 這個字串本身是「不可變」的

所以,只要「變數 a 是屬於 primitive type(也就是 number, string, boolean)」,就絕對沒辦法用「呼叫 function 的方式」去改變「變數 a 原本的值」

a.toUpperCase() 的確會「return(回傳)」一個新的字串 'HELLO'

但是!如果沒有把這個回傳的新字串 ‘HELLO’ 賦予給任何變數,是不會有任何事情發生的

var a = 'hello'
a.toUpperCase()
console.log(a)
// output: hello

正確寫法一:重新賦予給 a a = a.toUpperCase()

a.toUpperCase() 回傳一個新的字串 'HELLO',再用 = 重新賦予給變數 a (新的字串 ‘HELLO’,會在一個新的記憶體位置上)

var a = 'hello'
a = a.toUpperCase()
console.log(a)
// output: HELLO

第一行 var a = 'hello'

'hello' 這個字串所存放的記憶體位置,假設是 0x01

var a = 'hello'
// a: 'hello' 0x01

第二行 a = a.toUpperCase()

步驟一:
a.toUpperCase() 會「return(回傳)」一個新的字串 'HELLO'

但是!如果沒有把這個回傳的新字串 ‘HELLO’ 賦予給任何變數,是不會有任何事情發生的

步驟二: a = a.toUpperCase()
再把「新的字串 ‘HELLO’」,用 = 重新賦予給變數 a (新的字串 ‘HELLO’ 會在新的記憶體位置 0x02 上)

var a = 'hello'
// a: 'hello' 0x01

a = a.toUpperCase()
// a.toUpperCase(): 'HELLO'
// a: 'HELLO' 0x02

console.log(a)

正確寫法二:賦予給新的變數 var temp = a.toUpperCase()

延續上面的範例,也可以宣告一個新的變數 temp,去接收 a.toUpperCase() 回傳的「新的字串 'HELLO'」

var a = 'hello'
// a: 'hello' 0x01

var temp = a.toUpperCase()
// a.toUpperCase(): 'HELLO'
// temp: 'HELLO' 0X02

console.log(temp)
// output: HELLO

範例二

change 函式可以傳一個字串進去

  • 呼叫 change 函式: change(a)
  • 最後再把 a 的值印出來: console.log(a)
var a = 'hello'

function change(str){
  str = 'monday'
}
change(a)
console.log(a)
// output: hello

可以看到,最後印出來變數 a 的值依然還是 'hello',並不會被 change 函式變成 'monday'

原因也是一樣:只使用「呼叫 function」的方式,並沒辦法改變「變數 a 原本的值」

除非是「重新賦予給變數 a」一個新的值,變數 a 才會有新的值

只有 object(像是陣列、物件)是「可變的」

array(也是 object 的一種)

參考資料 Array.prototype

會改變原本陣列的 methods(Mutator methods)

Mutator methods 的用法:

就直接呼叫 function 即可,例如: arr.push(50)
不需要再用一個變數去接收 return 的值,例如這個錯誤寫法: arr = arr.push(50)

如果我本來就是想要改變「原本的 array」,所使用的函式就會直接改動「原本的 array」,基本上不會再回傳一個新的 array 了(會回傳別的東西)

範例一 push()

push() 會直接改變原本的陣列,並不會產生另一個新的陣列

var arr = [7 ,8, 9]
arr.push(50)
console.log(arr)
// output: [ 7, 8, 9, 50 ]

原因是:
push() 確實是真的去改變了在「原本記憶體位置」上面存的 [7 ,8, 9]

第一行 var arr = [7 ,8, 9]

存放 [7 ,8, 9] 的記憶體位置,假設是 0x01

var arr = [7 ,8, 9]
// arr: [7, 8, 9]  0x01

arr.push(50)
console.log(arr)

第二行 arr.push(50)

會直接在「0x01 這個記憶體位置」上,改變它存的 [7 ,8, 9],變成 [7, 8, 9, 50]

var arr = [7 ,8, 9]
// arr: [7, 8, 9, 50]  0x01

arr.push(50)
console.log(arr)

錯誤寫法

arr = arr.push(50) 是錯的!根本不需要再把 arr.push(50) 重新賦予給 arr

用這個錯誤寫法,印出來的值會不同--> 會印出 4

var arr = [7 ,8, 9]

arr = arr.push(50) // 錯誤寫法!!
console.log(arr)
// output: 4

為什麼會印出 4 呢?

可參考 Mutator methods - Array.prototype.push()

因為 Array.prototype.push() 這個 method 會「在原本陣列的最後面新增一個或多個元素」,並且「return 原本陣列的新長度」

所以,回傳的「4」就是 arr 陣列 [7, 8, 9, 50] 的長度

這句 arr = arr.push(50) 會做兩件事情:

  1. 先執行 arr.push(50),把 arr 陣列變成 [7, 8, 9, 50]
  2. arr.push(50) 這個 function 會回傳 [7, 8, 9, 50] 的長度,也就是 4,這個 4 會被重新賦予給變數 arr

所以,變數 arr 最後的值就變成 4 了

範例二 reverse()

正確寫法

var arr = [7 ,8, 9]
arr.reverse()
console.log(arr)
// output: [ 9, 8, 7 ]

錯誤寫法

arr = arr.reverse() 是錯誤寫法
但是,為什麼印出來的結果也會是 [ 9, 8, 7 ] 呢?
因為 arr.reverse() 除了直接改變原本的 array 之外,
也會 return 一個新的「順序倒過來的 array」

var arr = [7 ,8, 9]
arr = arr.reverse()
console.log(arr)
// output: [ 9, 8, 7 ]

不會改變原本陣列的 methods(Accessor methods)

通常來說,這些 function 回傳的值都不會是一個 array

因此,需要一個「變數」去接收這個 return 的值,例如: var result = arr.join('!')

範例一 join()

join() 不會改變原本的 arr 陣列
join() 回傳的值,會是一個 string

string 跟 array 是完全不同的東西,所以不可能去改動「原本的 array」變成 string,因此就用「回傳一個新的字串」的方式來回傳結果

var arr = [7 ,8, 9]
arr = arr.join('!')
console.log(arr)
// output: 7!8!9

範例二 indexOf()

indexOf(8) 來找「8」的 index 位置,也就是 1

錯誤寫法 arr.indexOf(8)

var arr = [7 ,8, 9]
arr.indexOf(8)
console.log(arr)
// output: [ 7, 8, 9 ]

可以看到:arr 陣列的值,並沒有被 arr.indexOf(8) 改變

因為 arr.indexOf(8) 會回傳一個新的值,也就是 1
但是並不會影響到原本的 arr 陣列,
所以印出的還是 [ 7, 8, 9 ]

正確寫法 var index = arr.indexOf(8)

因為 arr.indexOf(8) 會回傳一個新的值,也就是 1
再把 1 這個值重新賦予給變數 index
因此,就會印出 1 了

var arr = [7 ,8, 9]
var index = arr.indexOf(8)
console.log(index)
// output: 1

splice()slice() 要分清楚

splice() 會改變原本的陣列:在原本的陣列「新增、移除元素」

slice() 不會改變原本的陣列:會回傳「擷取後的新陣列」

總結

在使用 function 時,要非常清楚「可變、不可變」的特性,否則會出現很多 bug:

針對 primitive types 呼叫 function,永遠不會改變原本的值,只會回傳一個新的值(需要一個變數去接收 return 的值)

例如:

var str = 'FOREST'
var newStr = str.toLowerCase()
console.log(newStr)
// output: forest

針對 object types 呼叫 function,會有兩種情況:

[情況一] 會改變原本的陣列/物件,也會回傳一個新的值(不需要變數去接收 return 的值,因為 return 的值通常都沒有很重要,而我要的就只是改變後的陣列/物件而已)

例如:

var arr = ['Jan', 'Feb', 'March', 'April', 'May']
arr.splice(2, 1, 'hello')
console.log(arr)
// output: [ 'Jan', 'Feb', 'hello', 'April', 'May' ]

[情況二] 不會改變原本的陣列/物件,只會回傳一個新的陣列/物件(需要一個變數去接收 return 的陣列/物件)

例如:

var arr = ['Jan', 'Feb', 'March', 'April', 'May']
var newArr = arr.slice(2, 4)
console.log(newArr)
// output: [ 'March', 'April' ]

如果想了解的更深入,可參考下面資料:
https://ithelp.ithome.com.tw/articles/10206190
http://antrash.pixnet.net/blog/post/70456505-stack-vs-heap
https://medium.com/@yauhsienhuang/stack-acdcc11263a0


#javascript







Related Posts

變數命名的善意

變數命名的善意

Some relative page about the "dependent types"

Some relative page about the "dependent types"

Day 56 - Flask

Day 56 - Flask


Comments