こんにちは、ほそ道です。
Object.freeze編でやったようにオブジェクトの状態を完全に凍結させたい場合はfreezeしてしまえばよいのですが、アプリケーションを作っていると当然、拡張/変更が必要になるシチュエーションは出てくると思います。
今回はライブラリを使って不意なデータ変更によって生まれる複雑性に対抗する方法を紹介していきます。
const宣言したオブジェクトの不変性を保ちながら変更に対応する
前回、deepFreezeという関数を作るときに、freezeされた新しいオブジェクトを生成して返すようにしましたが、そのようにして破壊的メソッドを使用せずに、変更を加えた新しいオブジェクトを返す、というようにするのが拡張/変更による想定外を生まないための一つの方法であると思います。
で、いろいろなオブジェクトに対して、より安全性を高めつつ拡張・変更を行うための一つの方策として、ほそ道は今のところfacebookのimmutable.jsを使うようにしてます。
これによってconst a = なんとか
の「なんとか」をコピーして拡張・子要素の変更を行いながら別のオブジェクトとして扱うことで、aはずっと「なんとか」のままであることが保証されます。
例えて言うと下記のように動作するものです。
const a = {x: 1}
const b = a.set(x, 2);
console.log(b); // {x: 2}
immutable.jsに向かう前に
それ系の動作を提供するライブラリは幾つかあります。
ほそ道は、下記のようなライブラリたちを軽く検討しました。
一旦、facebookがリキ入れて作っているであろうということと、コレクションオブジェクトがレシーバーとなっている点がJS配列などと同じで直感的であろうということでimmutable.jsを使っていますが、その他のライブラリもそのうちしっかり触っていきたいなと思っています。
immutable.js
今回はimmutable.jsの仕組みやメソッドについて深くは言及せずに、const
とのコンビネーションという観点で紹介します。
今後、immutable.jsについては別枠でやろうかなと思っています。
immutable.jsには、flatMapなど
通常のArrayが持っていないような処理系も持っているので、不変化以外にも魅力があります。
余談ですが、ほそ道はimmutable.jsをインポートする際にI
という名前でインポートしてます。
公式だとImmutable
でインポートしていますが、長いのでI
にしていますが、別段困るシチュエーションには遭遇していません。
const I = require('immutable');
immutable.List
例えばArrayのpush
は破壊的メソッドで、レシーバーの配列自体を変更してしまいますが、immutable.jsのListのpush
は子要素を追加した新しいListを返し、レシーバーのListは変更されません。
const a = I.List([1, 2, 3, 4, 5]);
const b = a.push(6);
console.log(a.toJS()); // [ 1, 2, 3, 4, 5 ]
console.log(b.toJS()); // [ 1, 2, 3, 4, 5, 6 ]
const a = [1, 2, 3, 4, 5];
a.push(6);
console.log(a); // [ 1, 2, 3, 4, 5, 6 ]
immutable.Record
Objectに相当するものとしてはRecord
があります。
使い方がちょっと癖あるのでざっくりしたチュートリアルとともに。
immutable.Record
はデフォルト値を表すオブジェクトを引数として、コンストラクタ関数を返す関数です。
const Rec = I.Record({x: 1, y: 2});
const a = new Rec();
console.log(a); // Record { "x": 1, "y": 2 }
console.log(a.toJS()); //{ x: 1, y: 2 }
生成されたコンストラクタにオブジェクトを渡してインスタンスを生成すると、デフォルト値を上書きしたものが返されます。
const Rec = I.Record({x: 1, y: 2});
const a = new Rec({x: 10});
const b = new Rec({y: 20});
console.log(a.toJS()); // { x: 10, y: 2 }
console.log(b.toJS()); // { x: 1, y: 20 }
プロパティには.
でアクセスできます。
set
で値を変更して新しいオブジェクトを返します。オリジナルは変更されません。
const Rec = I.Record({x: 1});
const a = new Rec();
const b = a.set('x', 10);
console.log(a.x); // 1
console.log(b.x); // 10
remove
を使うとプロパティはデフォルト値になります。delete
とは違います。
const Rec = I.Record({x: 1});
const a = new Rec({x: 100});
const b = a.remove('x');
console.log(b.x); // 1
で、大事なところなのですが新しい属性を拡張することはできません。
このオブジェクトが格納する可能性のある属性は初めのコンストラクタ生成時に全て宣言しましょう。という仕組みになっているのですね。
const Rec = I.Record({x: 1});
const a = new Rec();
const b = a.set('y', 2); // Error: Cannot set unknown key "y" on Record
fromJS/toJS
immutable.List
からArrayへの変換はtoJS一発です。
Arrayを受け取るようなメソッドへ渡す場合はこちらを使うと良いと思います。
const list = I.fromJS([100, 200, 300, 400]);
const array = list.toJS();
console.log(array); // [ 100, 200, 300, 400 ]
fromJS
メソッドはArray
からimmutable.List
への変換が可能です。
const list = I.fromJS([100, 200, 300, 400]);
console.log(list); // List [ 100, 200, 300, 400 ]
ちなみにこのfromJSですが、通常のオブジェクトを食わせた場合はMap
オブジェクトになります。
const map = I.fromJS({x: 100, y: 200, z: 300});
console.log(map); // Map { "x": 100, "y": 200, "z": 300 }
console.log(map instanceof I.Map); // true
console.log(map.get('x')); // 100
と、いった感じでconst
によって宣言した値を想定外の変更が起こらないようにimmutable.jsを活用してみました。
アプリケーションの中にどう組み込んで行くべきかは今後にまとめ編として紹介してみたいと思います。