淺談immutable javascript

有在使用react/redux的人都聽過immutable.js,不過就單純討論javascript的immutability,或者說是javascript的物件不變性,可以參考這個immutable js影片教學。影片雖短但清楚明瞭。

基本上javascript複製物件有兩種的方式:標準型(primitive type)以及參考型(reference type)。如果是javascript的基礎形態如string、integer、布林值等,直接複製是沒問題的:

1
2
3
4
5
6
var a = 1;
var b = a;
b = 2;
console.log(a);
//結果a為1,b為2,符合immutability。

這種方式就是所謂的primitive type,複製物件的值改變時,不會影響原來的物件。

但javascript的immutability特性並不好,特別是在Object類型上。我們知道Object型別包含物件與陣列,舉例來說:

1
2
3
4
var a = [1, 2, 3];
var b = {name: "Amy", age: 25};
//javascript物件

以下程式說明在陣列或物件型別複製時,javascript都會使用參考型(reference type):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 物件
var a = {name: "Peter"};
var b = a;
var b.name = "Jason";
console.log(a.name);
// Jason, a.name被修改了!
// 陣列
var c = [1,2,3,4];
var d = c;
d[0] = 5;
console.log(c);
// [5,2,3,4] 陣列c被修改了!

這就產生了不好的結果,因為我們沒有要修改a.name的值,但是卻因為b.name的變動連同a.name也一起改變了。這種情況也同樣發生在ruby上。解決的方式是建立一筆新的物件,而透過新的物件以套用的方式帶入相對應的值,例如:

1
2
3
4
5
6
7
8
var a = {name: "Peter", age: 25};
var b = Object.assign({}, a);
// Object.assign()首個參數是空物件,再來插入a,或者以逗號分隔的多個物件
// 也可使用jQuery的$.extend或lodash的_.assign
b.name = "Apple";
console.log(a);
// {name: "Apple", age: 25}

至於陣列的部分,我們可以要考慮增加或刪除的情況,可以使用concat方法來增加、filter方法來刪除:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var a = [1,2,3,4];
var b = a.concat(5);
console.log(b);
// [1,2,3,4,5]
console.log(a);
// [1,2,3,4] a沒有受到影響
var c = [1,2,3,4];
var d = c.filter((val) => val !== 4);
// 這邊用到ES6的arrow function來簡化語法
console.log(c);
// [1,2,3,4], c沒有受到影響
console.log(d);
// [1,2,3]

最後在影片中有秀了一段物件混合陣列的做法,即便使用Object.assign()仍有陣列要處理:

1
2
3
4
5
6
7
8
9
10
11
12
var a = {name: "Will", things: [1,2,3]};
var b = Object.assign({}, a, {name: "Fred"});
b.things.push(4);
console.log(a);
// {name: "Will", things: [1,2,3,4]} a的things已經改變!
var c = {name: "Will", things: [1,2,3]};
var d = Object.assign({}, c, {name: "David"});
d.things = c.things.concat(4);
console.log(c);
// {name: "Will", things: [1,2,3]} c沒有受到改變!

小結:物件的部分要特別小心,除了使用Object.assign(), concat, filter之外,map與reduce也能夠產生新的陣列。