イミュータブルという言葉を知っていますか?プログラミングにおける重要なキーワードの1つらしいのですが、私は知りませんでした。
イミュータブルとは何か?どうすればそれを実現できるのか?JavaScript を例にとって考察しようと思います。
イミュータブルとは
イミュータブル(immutable)とは、日本語で「不変」という意味の言葉です。イミュータブルなオブジェクトと言ったら、状態が変わらないオブジェクトのことを指します。
イミュータブルなオブジェクトは状態が変えられないため、変数のオブジェクトを更新する場合は、オブジェクトを作り直さなければいけません。
なぜイミュータブルなオブジェクトにするのか
なぜわざわざオブジェクトを作り直すのでしょうか。イミュータブルなオブジェクトを使用する利点は、いくつか考えられます。
以前の状態を簡単に再現できる
イミュータブルなオブジェクトは、状態が変化しないことが保証されているため、そのオブジェクトを保持しておくだけでいつでも元の状態を再現することができます。
データの更新を簡単に検出できる
イミュータブルなオブジェクトは、前と同じオブジェクトを参照している限り、状態も変化していないことがわかります。そのため、オブジェクトが持っているすべての状態を1つずつ取り出して比較する必要がありません。
React ではパフォーマンスを最適化できる
JavaScript ライブラリの React では、イミュータブルな pure component を構築することで、効率的な再レンダーが可能になります。1
JavaScript でオブジェクトをイミュータブルなものとして扱う
JavaScript において、文字列や数値、真偽値等のデータは絶対的にイミュータブルです。そのため、これらのデータを扱う場合はイミュータブルを意識する必要はありません。2
一方で、オブジェクトや配列は中の状態が変えられるため、基本的にイミュータブルなデータではありません。
// オブジェクトの場合
let obj1, obj2;
obj1 = {a: 1};
obj2 = obj1;
obj2["1"] = 2;
obj2.a = "b";
console.log(obj1); // {1: 2, a: "b"}
// 配列の場合
let ary1, ary2;
ary1 = [1, 2];
ary2 = ary1;
ary2.push(3);
ary2[0] = "a";
console.log(ary1); // ["a", 2, 3]
オブジェクトや配列をイミュータブルなデータとして扱いたい場合は、少し工夫が必要です。基となるオブジェクトの状態は変えずに、新しい状態を持った別のオブジェクトを作成し、古いデータと置き換えて使用します。
// オブジェクトの場合
let obj1, obj2;
obj1 = {a: 1};
obj2 = obj1;
obj2 = Object.assign({}, obj2, {"1": 2});
obj2 = Object.assign({}, obj2, {a: "b"});
console.log(obj1); // {a: 1}
// 配列の場合
let ary1, ary2;
ary1 = [1, 2];
ary2 = ary1;
ary2 = ary2.concat(3);
ary2 = ["a"].concat(ary2.slice(1));
console.log(ary1); // [1, 2]
イミュータブルなオブジェクト操作に使えるメソッド
Object.assign()
は、ES6 から使えるようになった新しいメソッドです。コピー先として空のオブジェクトを指定することで、コピー元オブジェクトからプロパティをシャローコピー (1段階の深さのコピー)した新しいオブジェクトを返します。
配列では concat()
メソッドや slice()
メソッドを使うことで、元の配列を変更せずに要素を結合したり、元の配列を変更せずに要素を切り取ったり、非破壊的な配列操作が可能になります。反対に、sort()
や push()
等のメソッドは元の配列を直接変更してしまうため、イミュータブルな配列には使用できません。
他に、オブジェクトのプロパティや配列の要素に値を直接代入するようなコードも、イミュータブルなデータ更新では使うことができません。
データの変化を伴うメソッドのことを破壊的メソッド、データの変化を伴わないメソッドを非破壊的メソッドといいます。イミュータブルなオブジェクト操作では非破壊的メソッドを使いましょう。
参照の値をコピーする意味
上記のメソッドは、コピー元のプロパティがオブジェクトを参照している場合、参照の値のみをコピーします。また、オブジェクトを別の変数に代入する際も、参照の値をそのまま代入して、同じオブジェクトを参照するようにしています。
let ary1 = [
{a: 1},
{b: 2},
{c: 3},
];
let ary2 = ary1;
ary1 = ary1.slice(1);
console.log(ary1[0] == ary2[1]); // true
このように同じオブジェクトを参照することで、オブジェクトの状態が等しいかどうかを簡単に調べることができます。オブジェクトを1から作り直すわけではないので、メモリが無駄に使用されることもありません。
スプレッド構文でより簡潔に
オブジェクトや配列をイミュータブルなデータとして扱うコードは、スプレッド構文3を使ってより簡潔に書くことができます。
// オブジェクトの場合
let obj1, obj2;
obj1 = {a: 1};
obj2 = obj1;
obj2 = {...obj2, "1": 2};
obj2 = {...obj2, a: "b"};
console.log(obj1); // {a: 1}
// 配列の場合
let ary1, ary2;
ary1 = [1, 2];
ary2 = ary1;
ary2 = [...ary2, 3];
ary2 = ["a", ...ary2.slice(1)];
console.log(ary1); // [1, 2]
オブジェクトのスプレッド構文は ES2018 から、配列のスプレッド構文は ES6 から使えるようになりました。
まとめ
オブジェクトの状態は変えず、新しく作り直すようにすることで、イミュータブルなオブジェクトを実現することができました。
ES6 から使えるようになった新しいメソッドやスプレッド構文を活用して、イミュータブルなオブジェクトを実装しましょう。