JavaScript-Immutable.JS

有機會的話想導入看看。

簡述

Immutable(不可變)在開發中是一個很重要的概念,它有幾個優點:

  • 可預測性
  • 可追朔性
  • 減少 Side Effect

在 JavaScript 裡「Array」和「Object」都是 mutable(可變)的資料型態,這時候如果沒有用「Immutable 的觀念」來操作的話就會引發一些不可預測的行為。

舉例來說,假設我有一個 Array,我想取出最後一筆的值,這時候就會出現「原本的 Array 也被改掉了」的情況發生,像這樣:

1
2
3
4
const array = [1, 2, 3, 4, 5]
const lastOne = array.pop()
console.log(lastOne) // 5
console.log(array) // [ 1, 2, 3, 4 ]

我確實拿到了最後一個值 5,但注意原本的 array 也被改掉了。

為了不改變原本的 array,我們可以用「拷貝」的方式來做,像這樣:

1
2
3
4
5
const array = [1, 2, 3, 4, 5]
const lastOne = [...array].pop()

console.log(lastOne) // 5
console.log(array) // [1, 2, 3, 4, 5]

這邊是用「淺拷貝」來做複製,當資料型態更複雜時你可能會需要用「深拷貝」來處理。只是順便提一下,不是這篇要提的重點。

總之這種作法就是「Immutable」的觀念,我每一次都是「產生一個全新的物件」,而不是直接去改「原本的 Reference」,所以原本的 array 永遠不會被動到。

不過這就引發了另外一個問題:

如果我只是想改某個地方,卻又要先把原始資料整筆複製一份的話,豈不是有點浪效能?

所以這篇就來介紹 Immutable.js,一個由臉書開源出來的套件。

List

建立方式

這個基本上就是 JS 中的 Array,建立方式也很簡單:

1
2
3
const list = List([1, 2, 3, 4, 5])
console.log(list) // List [ 1, 2, 3, 4, 5 ]
console.log(list.toJS()) // [ 1, 2, 3, 4, 5 ]

toArray

其中一個轉成 JS 的方法,但要注意是「淺拷貝」:

1
2
3
4
5
6
7
8
9
// 淺拷貝
const list = List([[1], 2, 3, 4, 5])
const toArray1 = list.toArray()
const toArray2 = list.toArray()

console.log(toArray1) // [ [ 1 ], 2, 3, 4, 5 ]
console.log(toArray2) // [ [ 1 ], 2, 3, 4, 5 ]
console.log(toArray1 === toArray2) // false
console.log(toArray1[0] === toArray2[0]) // true

toJS

轉成 JS 的方法,這個才是「深拷貝」:

1
2
3
4
5
6
7
8
9
// 深拷貝
const list = List([[1], 2, 3, 4, 5])
const toArray1 = list.toJS()
const toArray2 = list.toJS()

console.log(toArray1) // [ [ 1 ], 2, 3, 4, 5 ]
console.log(toArray2) // [ [ 1 ], 2, 3, 4, 5 ]
console.log(toArray1 === toArray2) // false
console.log(toArray1[0] === toArray2[0]) // false

isList

檢查值是不是 List 這個型態:

1
2
3
4
5
const array = [1, 2, 3, 4, 5]
const list = List([[1], 2, 3, 4, 5])

console.log(List.isList(array)) // false
console.log(List.isList(list)) // true

of

如果要透過 List 來建立新的 List 的話,要注意值只能是 Array,不可以是別的值:

1
2
3
4
// 正確的作法
const list1 = List([1, 2, 3])
// 錯誤的作法(TypeError)
const list2 = List(1, 2, 3)

如果要傳非 Array 的值的話要改用 of 來建立:

1
2
3
4
5
6
7
const list1 = List.of(1, 2, 3)
const list2 = List.of({ name: 'PeaNu', age: 20 })
const list3 = List.of(false, true)

console.log(list1.toArray()) // [ 1, 2, 3 ]
console.log(list2.toArray()) // [ { name: 'PeaNu', age: 20 } ]
console.log(list3.toArray()) // [ false, true ]

size

用來查看長度(length)的屬性:

1
2
const list = List([1, 2, 3])
console.log(list.size) // 3

set

用來設定陣列值的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
const list = List([1, 2, 3])

const list2 = list.set(3, 4)
console.log(list2.toArray()) // [ 1, 2, 3, 4 ]

const list3 = list.set(0, 100)
console.log(list3.toArray()) // [ 100, 2, 3 ]

const list4 = list.set(5, 10)
console.log(list4.toArray()) // [ 1, 2, 3, undefined, undefined, 10 ]

const list5 = list.set(-1, 0)
console.log(list5.toArray()) // [1, 2, 0]

delete

刪除陣列值,可以想成是 Imuutable 的 splice(0, index)

1
2
3
4
5
6
7
8
9
const list = List([1, 2, 3, 4, 5])
const list1 = list.delete(0)
console.log(list1.toArray()) // [ 2, 3, 4, 5]

const list2 = list.delete(3)
console.log(list2.toArray()) // [1, 2, 3, 5]

const list3 = list.delete(-1)
console.log(list3.toArray()) // [1, 2, 3, 4]

insert

在指定的 index 中插值:

1
2
3
4
5
6
const list = List([1, 2, 3, 4, 5])
const list2 = list.insert(0, 0)
console.log(list2.toArray()) // [ 0, 1, 2, 3, 4, 5 ]

const list3 = list.insert(2, 'Hi')
console.log(list3.toArray()) // [ 1, 2, 'Hi', 3, 4, 5 ]

push & pop

熟悉的 push 與 pop,不過是 Immutable 版:

1
2
3
4
5
const list = List([1, 2, 3, 4, 5])
const list2 = list.push(6)

console.log(list.toArray()) // [ 1, 2, 3, 4, 5 ]
console.log(list2.toArray()) // [ 1, 2, 3, 4, 5, 6 ]
1
2
3
4
5
const list = List([1, 2, 3, 4, 5])
const list2 = list.pop()

console.log(list.toArray()) // [ 1, 2, 3, 4, 5 ]
console.log(list2.toArray()) // [ 1, 2, 3, 4 ]

shift & unshift

熟悉的 shift 與 unshift,不過是 Immutable 版:

1
2
3
4
5
6
7
const list = List([1, 2, 3, 4, 5])
const list2 = list.unshift(0)
const list3 = list.shift()

console.log(list.toArray()) // [ 1, 2, 3, 4, 5 ]
console.log(list2.toArray()) // [ 0, 1, 2, 3, 4, 5 ]
console.log(list3.toArray()) // [ 2, 3, 4, 5 ]

merge

把兩個陣列合併:

1
2
3
4
const a = List([1, 2, 3, 4, 5])
const b = List([6, 7, 8, 9])

console.log(a.merge(b).toArray()) // [1, 2, 3, 4, 5, 6, 7, 8, 9]

sortBy

想快速用「字典序」來排序的時候還不錯用:

1
2
3
const foods = List([{ name: 'Orange' }, { name: 'Grape' }, { name: 'Banana' }, { name: 'Candy' }])
const sortedFoods = foods.sortBy((food) => food.name)
console.log(sortedFoods.toArray())

輸出結果:

1
;[{ name: 'Banana' }, { name: 'Candy' }, { name: 'Grape' }, { name: 'Orange' }]

Map

簡單來說就是物件,基本上都跟 List 的用法差不多,所以這裡就直接示範了。

建立方式

直接把 Object 傳進去就行了:

1
2
const map = Map({ name: 'PeaNu', age: 20, gender: 'man' })
console.log(map.toObject()) // { name: 'PeaNu', age: 20, gender: 'man' }

set

設定內容(單層):

1
2
const map = Map().set('name', 'PeaNu').set('age', 20).set('gender', 'man')
console.log(map.toObject()) // { name: 'PeaNu', age: 20, gender: 'man' }

setIn

設定內容(多層):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const a = Map({
name: 'PeaNu',
age: 20,
gender: 'man',
relation: {
wife: 'PPB',
father: 'JOJO',
friend: 'DIO'
}
})
const newOne = a.setIn(['relation', 'father'], 'Caesar')

// {
// name: 'PeaNu',
// age: 20,
// gender: 'man',
// relation: {
// wife: 'PPB',
// father: 'Caesar',
// friend: 'DIO'
// }
// }
console.log(newOne.toObject())

delete

刪除某個屬性(單個):

1
2
3
4
5
6
7
8
const map = Map({
name: 'PeaNu',
age: 20,
gender: 'man'
})
const map2 = map.delete('name')
console.log(map.toObject()) // { name: 'PeaNu', age: 20, gender: 'man' }
console.log(map2.toObject()) // { gender: 'man', age: 20 }

deleteAll

刪除多個欄位(傳陣列):

1
2
3
4
5
6
7
const map = Map({
name: 'PeaNu',
age: 20,
gender: 'man'
})
const map2 = map.deleteAll(['name', 'age'])
console.log(map2.toObject()) // { gender: 'man' }

merge

合併:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const a = Map({
name: 'PeaNu',
age: 20,
gender: 'man'
})

const b = Map({
brother: 'JOJO',
father: 'DIO',
wife: 'PPB'
})

// {
// name: 'PeaNu',
// age: 20,
// gender: 'man',
// brother: 'JOJO',
// father: 'DIO',
// wife: 'PPB'
// }
console.log(a.merge(b).toObject())

mergeWith

一般的 merge 如果碰到「相同值」的話會直接覆寫,如果你希望「不要覆蓋」,想做一些處理的話可以改用這個方式:

1
2
3
4
5
6
7
8
9
10
11
12
const a = Map({ a: 10, b: 20, c: 30 })
const b = Map({ a: 40, b: 50, e: 60, f: 30 })

const result1 = a.mergeWith((oldVal, newVal) => {
// Ex: a(10) + a(40)
return oldVal + newVal
}, b)

const result2 = a.merge(b)

console.log(result1.toObject()) // { a: 50, b: 70, c: 30, e: 60, f: 30 }
console.log(result2.toObject()) // { a: 40, b: 50, c: 30, e: 60, f: 30 }

map

給 Object 用的 map,我覺得很酷:

1
2
3
const obj = Map({ a: 10, b: 20 })
const obj2 = obj.map((num) => num * 2)
console.log(obj2.toObject()) // { a: 20, b: 40 }

reverse

可以反轉內容:

1
2
3
const obj = Map({ a: 10, b: 20 })
const obj2 = obj.reverse()
console.log(obj2.toObject())

sort

神奇的排序:

1
2
3
4
5
6
7
const obj = Map({ a: 5, b: 12, c: 10, d: 8, e: 24, f: 3 })

const asc = obj.sort((a, b) => b - a)
const desc = obj.sort((a, b) => a - b)

console.log(asc.toObject()) // { e: 24, b: 12, c: 10, d: 8, a: 5, f: 3 }
console.log(desc.toObject()) // { f: 3, a: 5, d: 8, c: 10, b: 12, e: 24 }
Vue-Composition API(Computed) Vue-Composition API(Ref & Reactive)
Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×