Help us understand the problem. What is going on with this article?

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

More than 3 years have passed since last update.

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

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

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away