- mutation 的意思是「突變,變異」 noun
- immutable 的意思是「不可改變的」 adjective
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 呢?
因為 Array.prototype.push()
這個 method 會「在原本陣列的最後面新增一個或多個元素」,並且「return 原本陣列的新長度」
所以,回傳的「4」就是 arr
陣列 [7, 8, 9, 50]
的長度
這句 arr = arr.push(50)
會做兩件事情:
- 先執行
arr.push(50)
,把arr
陣列變成[7, 8, 9, 50]
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