【JavaScript】const では無理、freeze() や seal() を使ってオブジェクトを変更不可に

近年の JavaScript (ECMAScript) では const を使って定数を定義できます。しかし、オブジェクトの中身までは保護されません。オブジェクトの中身を保護するには保護のレベルに応じて凍結または封印を行う必要があります。今回は、const の限界と凍結および封印について詳しく見ていきましょう。

const を使ってオブジェクトを定義すると…

近年は変数の定義に letconst を使うことが多くなったのではないでしょうか。少なくとも筆者はもう var を使うことは皆無です。const は定数を定義できるわけですが、近年はコードのメンテナンス性からも変数の値を変更しないほうが良いというトレンドもあり、ほとんどを const で変数定義する人も増えているのではないでしょうか。

一方で const を使ってオブジェクトを定義した場合、そのオブジェクトの中身も変更できなくなることを期待したのではないでしょうか。しかし、実際はそうではありません。

次のコードは const を使ってオブジェクトを定義したうえで、その中身である "taro" プロパティの値を変更しようとしています。

const obj = { "taro": 20, "jiro": 30 };
obj.taro = 25;
console.log(obj.taro); // 25

本来なら値を代入する時点でエラーになってほしいところですが、実際には何も起こらずコードの通りに値の代入は成功してしまいます。

というのも、const は定義した変数そのものへの再代入を禁止しているだけで、その中身までは保護しないためです。次のコードは const で変数 obj を定義したのち、その obj に別のオブジェクトを代入しています。 これであればエラーになります。

const obj = { "taro": 20, "jiro": 30 };
obj = { "saburo": 40 };

オブジェクトの凍結

では、本当の意味で変更不可能なオブジェクトを作ることはできないのでしょうか。変数定義で一発で変更不可能なオブジェクトは作れませんが、ひと手間かければ実現可能です。

const obj = { "taro": 20, "jiro": 30 };
Object.freeze(obj);

Object オブジェクトの freeze() メソッドを使って、オブジェクトを変更不可能にします。この変更不可能にすることを凍結 (freeze) と言います。

凍結されたオブジェクトは、既存のプロパティの値の変更はもちろんのこと、既存のプロパティの削除、新たなプロパティの追加も禁止されます。

const obj = { "taro": 20, "jiro": 30 };
Object.freeze(obj);

obj.taro = 25; // 既存のプロパティの値の変更
delete obj.jiro; // 既存のプロパティの削除
obj.saburo = 40; // 新たなプロパティの追加

console.log(JSON.stringify(obj)); // {"taro":20,"jiro":30}

このように凍結されたオブジェクトに対する操作はすべて無効になっています。各種操作の後に凍結オブジェクトの中身を見ると何も変化していません。

この結果を見て少し変に思われたのではないでしょうか。そうです、エラーにならず、最後の console.log() の実行に到達してしまっています。

実は実行されたコードが Strict モードかそうでないかで挙動が変わります。 前述のコードは Strict モードではないため、エラーが発生せず、あたかも何事もなかったかのようにふるまってしまいました。しかし、スクリプトの冒頭に "use strict" ディレクティブを記述すれば、期待通りにエラーになります。

'use strict';

const obj = { "taro": 20, "jiro": 30 };
Object.freeze(obj);

obj.taro = 25; // この時点でエラーになります。

オブジェクトの封印

前述のオブジェクトの凍結では、該当のオブジェクトに対していかなる変更も許しませんでした。しかし、既存のプロパティの値の変更だけは許して、プロパティの追加と削除を許さない、という少し緩めの制限が欲しい場合もあるでしょう。その場合は、Object オブジェクトの seal() メソッドを使います。これを封印 (seal) と呼びます。

const obj = { "taro": 20, "jiro": 30 };
Object.seal(obj);

obj.taro = 25; // 既存のプロパティの値の変更
delete obj.jiro; // 既存のプロパティの削除
obj.saburo = 40; // 新たなプロパティの追加
console.log(JSON.stringify(obj)); // {"taro":25,"jiro":30}

上記のコードでは変数 obj を封印したうえで、既存プロパティの値の変更、既存のプロパティの削除、新たなプロパティの追加を試みたうえで、変数 obj の中身を覗いています。ご覧の通り、既存のプロパティの値の変更は成功しているものの、プロパティの追加と削除は失敗しています。

前述のコードは Strict モードではない状態で実行しましたが、もし Strict モードで実行すれば、既存のプロパティを削除しようとした時点でエラーになりスクリプトは終了します。

凍結または封印されているかを判定する方法

凍結されているかを判定するには、Object オブジェクトの isFrozen() メソッドを使います。

const obj = { "taro": 20, "jiro": 30 };
console.log(Object.isFrozen(obj)); // false
Object.freeze(obj);
console.log(Object.isFrozen(obj)); // true

isFrozen() メソッドは引数に指定したオブジェクトが凍結されていれば true を、そうでなければ false を返します。

同様に、封印されているかを判定するには、Object オブジェクトの isSealed() メソッドを使います。

const obj = { "taro": 20, "jiro": 30 };
console.log(Object.isSealed(obj)); // false
Object.seal(obj);
console.log(Object.isSealed(obj)); // true

isSealed() メソッドは引数に指定したオブジェクトが封印されていれば true を、そうでなければ false を返します。

クラスのゲッターでの応用

筆者がオブジェクトの凍結が重宝しそうと思うケースは、クラスのインスタンスのゲッターでしょうか。クラスからインスタンスを生成し、そのインスタンスが保持する値を取得する場合を考えましょう。その値がオブジェクトだとします。

'use strict';

class Family {
  constructor() {
    this._members = {
      'taro': 35,
      'hanako': 34
    };
  }

  get members() {
    return this._members;
  }
}

const family = new Family();
family.members.taro = 30; // プロパティの値の変更は可
console.log(JSON.stringify(family.members)); // {"taro":30,"hanako":34}

このコードでは、Family クラスのインスタンス family を生成し、そこから members プロパティの中の taro の値を書き換えようとしています。このコードはもちろん成功します。しかし、インスタンスのゲッターによって返したオブジェクトの中身も変更不可にしたいことは多いのではないでしょうか。それを実現するには、get members()this._members をディープコピーし、それを凍結してから返します。

'use strict';

class Family {
  constructor() {
    this._members = {
      'taro': 35,
      'hanako': 34
    };
  }

  get members() {
    const members = Object.assign({}, this._members);
    Object.freeze(members);
    return members;
  }
}

const family = new Family();
family.members.taro = 30; // エラーとなりプロパティの値の変更は不可

上記コードは凍結されたインスタンスのメンバーに対して中身の値を書き換えようとしたためエラーになります。もちろん、isFrozen() メソッドを使って凍結されているかどうかを事前にチェックすることも可能です。

if(Object.isFrozen(family.members) === false) {
  // 凍結されている以上、このコードが実行されることはない
  family.members.taro = 30;
}

まとめ

オブジェクトの凍結や封印は、自分ひとりでコードを書いている分にはメリットを感じにくいかもしれません。しかし、ライブラリーやモジュールという形で他人に利用してもらう場合には、きっと重宝するでしょう。

今回は以上で終わりです。最後まで読んでくださりありがとうございました。それでは次回の記事までごきげんよう。

Share