LoginSignup
41
36

More than 5 years have passed since last update.

ES6のconstを使い倒すレシピ3 - immutable.js編 〜 JSおくのほそ道 #036

Last updated at Posted at 2015-12-06

こんにちは、ほそ道です。

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にしていますが、別段困るシチュエーションには遭遇していません。

require
const I = require('immutable');

immutable.List

例えばArrayのpushは破壊的メソッドで、レシーバーの配列自体を変更してしまいますが、immutable.jsのListのpushは子要素を追加した新しいListを返し、レシーバーのListは変更されません。

immutable.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 ]
Array
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はデフォルト値を表すオブジェクトを引数として、コンストラクタ関数を返す関数です。

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とは違います。

remove
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を受け取るようなメソッドへ渡す場合はこちらを使うと良いと思います。

toJS
const list = I.fromJS([100, 200, 300, 400]);
const array = list.toJS();
console.log(array);    // [ 100, 200, 300, 400 ]

fromJSメソッドはArrayからimmutable.Listへの変換が可能です。

fromJS
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を活用してみました。
アプリケーションの中にどう組み込んで行くべきかは今後にまとめ編として紹介してみたいと思います。

41
36
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
41
36