FP - Avoid Mutable Data

FP - Avoid Mutable Data

紀錄一下上課時學習到的函式導向的一些基礎觀念。

Mutable 可變的

若是一個資料可以在產生之後可以被變更,它就是可變的。
在ES6之後, 我們可以使用const來宣告常數, 他就不能再被重新賦值, 可是值得注意的是, 以const宣告的物件還是可變的

1
2
3
4
5
6
7
8
9
10
11
12
13

const arr = [1, 3, 2, 4];

// sort 會將一個陣列原地做排序, 不回傳新陣列
const sortArray = function (arr1) {
return arr1.sort();
}

const newNums = sortArray(arr);

console.log("newNums : ", newNums); //[1, 2, 3, 4]
console.log("arr : ", arr);
// [1, 2, 3, 4]

由於物件傳參考特性(陣列也是物件), newNum獲得的陣列, 即使他們有不同的名字, 它跟arr所定義的陣列享有共同的記憶體空間, 對js來說他們就是一樣的。

functional programming的一個精神就是要achieve immutability, 達成資料的不可變更性, 因此這次我們可以使用object底下的一個method - freeze來試試

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

"use strict"

const arr = [1, 3, 2, 4];
Object.freeze(arr);


const sortArray = function (arr1) {
return arr1.sort();
}

const newNums = sortArray(arr);

console.log("newNums : ", newNums); //[1, 2, 3, 4]
console.log("arr : ", arr);

跳錯
VM3737:6 Uncaught TypeError: Cannot assign to read only property '0' of object '[object Array]'

Object.freeze可以凍結物件, 使它不可被新增屬性, 以及變更、刪除既有屬性

FP way - cloning

既然我們又不想要變更物件, 又希望能夠對該物件進行一些操作, 要怎麼兼顧兩方來達成呢? 一個常見的做法是cloning - 複製原有的物件, 操作這份複製品, 然後回傳它

Object.assign({}, obj)

1
2
3
4
5
6
7
8
9
10
11
12
13
14

let obj = {
firstName: "Steven",
lastName: "Winston",
score: 60,
completion: true,
}

let obj2 = Object.assign({}, obj);
console.log(obj, obj2, obj === obj2)

// {firstName: "Steven", lastName: "Winston", score: 60, completion: true}
// {firstName: "Steven", lastName: "Winston", score: 60, completion: true}
// false

obj2是obj的複製品, 且兩者不會互相傳參考, 互不相等

1
2
3
4
obj2.firstName = "Jone"
"Jone"
console.log(obj.firstName, obj2.firstName)
// Steve Jone

obj2的值改變後並不會影響到obj

Spread Operator

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

let arr = [1, 2, 3, 4, 5];
let newArr = [...arr];
console.log(arr); // [1, 2, 3, 4, 5]
console.log(newArr); // [1, 2, 3, 4, 5]
console.log(arr === newArr); // false

let obj = {
a: 1,
b: "str",
c: function() { console.log("hey!")}
}
let newObj = {...obj};
console.log(obj); // {a: 1, b: "str", c: ƒ}
console.log(newObj); // {a: 1, b: "str", c: ƒ}
console.log(obj === newObj) // false

let obj = {
a: 1,
b: {
c: 1
}
}
let newObj = {...obj};
newObj.b.c = 3;
console.log(obj.b.c, newObj.b.c) // 3 3

spread operator是三個點(…), 它可以幫助我們把物件和陣列裡的鍵值配對給拆開, 如果是物件就會包在{}中, 如果是陣列就包在方括號[]中。

但是要注意的是,他跟Object.assign一樣是屬於淺層複製

Objecy.assign遇到的問題

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
let obj = {
name: "Mary",
score: "70",
questions: {
q1: {
success: true,
value: 2,
},
q2: {
success: false,
value: -2,
},
}
}

let obj2 = Object.assign({}, obj);

console.log(obj === obj2); // false
console.log(obj.questions === obj2.questions); // true

obj.questions.q1.value = 10;
console.log(obj.questions.q1.value, obj2.questions.q1.value) // 10 10

在obj中我們定義了一個屬性為questions, 它是一個物件, 裡面有包含屬性q1和q2,接著使用另一個變數obj2來複製obj,到這邊都跟剛剛一樣

我們預期的結果是:obj和obj2應該是兩個完全不同的物件, 我們對obj所做的修改不應該影響到obj2, 然而再修改了obj中question物件中q1的value後, 沒想到obj2也被改變了

淺層拷貝和深層拷貝

object.assign是屬於淺層拷貝, 意思就是, 它僅僅會複製來源物件的屬性值, 若是該屬性為物件的話, 該屬性的值即為該物件的參照,因此被複製時也是複製了該物件的參照,並不會改變它。

Object.assign

如何達成深層複製?
使用JSON.parse(JSON.stringify(obj))

1
2
3
4
5
6

let obj3 = JSON.parse(JSON.stringify(obj));

console.log(obj.questions === obj3.questions) // false
obj.questions.q1.value = 100;
console.log(obj.questions.q1.value, obj3.questions.q1.value) // 100 10

JSON.stringify方法會將物件轉型為字串, 再透過JSON.parse將該字串轉型為物件, 在這個過程中, 物件已經失去它原本的參照, 因此parse過來的物件所享有的參照已經不是當初他的來源物件所享有的參照了

這次更改obj內的question中的q1並不會影響到obj3, 這兩者已經是不同的物件,不再共享參照了

陣列的深淺複製

1
2
3
4
5
6
7
8
9
10
11

let arr = [
{a: 1},
{b: 2},
{c: 3}
];

let copy = Object.assign({}, arr);
let copy2 = JSON.parse(JSON.stringify(arr));

console.log(arr, copy, copy2);

使用object.assign複製的copy會被轉型成物件, JSON則保留了原本的陣列型別

1
2
3
4
5
6
// 改變原始陣列中的值
arr[0].a = 15;

// 注意! object.assign後, 陣列被轉為物件了
console.log(copy["0"].a) // 15
console.log(copy2[0].a) // 1

JSON.parse後的複製品跟原本的物件已經互不相干, 這點在陣列也同樣適用

JSON.stringify & JSON.parse可能踩到的雷

若是物件屬性的值為函式或是undefined, JSON.stringify無法將他們轉為字串, 複製的結果依據來源物件的型別為物件或陣列會有所不同

1
2
3
4
5
6
7
8
9
10
11
12
13
14

let obj = {
a: function() {
console.log("a prop in obj")
},
b: 1,
c: undefined,
d: "hello!",
}

let obj2 = JSON.parse(JSON.stringify(obj));

console.log(obj);
console.log(obj2);

1
2
3
4
5
6
7
8
let arr = [
function(){}, 2, undefined, 4
];

let arr2 = JSON.parse(JSON.stringify(arr));

console.log(arr);
console.log(arr2);

來源物件型別為物件的時候, function或undefined會被跳過, 而若是陣列, 則會轉為null

總結

1
2
3
4
5
6
7
8
9
10
11
12
13
14

const arr = [1, 3, 2, 4];


const cloneObj = function(obj) {
return JSON.parse(JSON.stringify(obj));
}


const newNums = cloneObj(arr).sort();

console.log("newNums : ", newNums); //[1, 2, 3, 4]
console.log("arr : ", arr);
// [1, 3, 2, 4]

newNums使用了sort之後, 不會再影響到原始的arr了!

參考

Functional Programming in JavaScript: A Practical Guide
這位講師解說的觀念都很清楚好懂, 也有很豐富的練習, 如果對函式導向有興趣不妨在特價時購買!

Your browser is out-of-date!

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

×