寒いこの時期はObjectだって凍ります。

リードUI開発者 宇賀

みなさんこんにちは!UI開発者の宇賀です🍣🍵!
先日までちょっと気の早い春の陽気が訪れていたかと思えば、雪が降ったりして...やっぱりまだまだ冷え込みますね!⛄❄

さて、みなさんはObject.freeze()というメソッドをご存じでしょうか?冷たそうな名前の通り、オブジェクトをカチコチに固めてくれるメソッドの1つです。

今回はfreeze()メソッドのようにJavaScriptのオブジェクトを固める方法をいくつかご紹介していきたいと思います!💨💨💨

Object.defineProperty()とObject.defineProperties()

オブジェクトのプロパティにはいくつかの属性があります。

属性名 意味 初期値
Writable属性 書き換えできるか true
Enumerable属性 for...inObject.keysで列挙されるか true
Configurable属性 属性が変更できるか、プロパティを削除できるか true

setgetについては割愛

Object.defineProperty()は、これらの属性をプロパティごとに定義して、オブジェクトをより厳密に管理できるようにするためのメソッドです。

何もしていないオブジェクトであれば、次のようにプロパティを更新したり削除したりできます。

const obj = {
  hoge: 1
};

obj.hoge = 100;
console.log(obj.hoge); // > 100
obj.hoge = 200;
console.log(obj.hoge); // > 200
delete obj.hoge;
console.log(obj.hoge); // > undefined

Object.defineProperty()を使って属性を変更すると、次のようにプロパティが書き込み禁止になったり削除できないようになります。

const obj = {
  hoge: 1
};

Object.defineProperty(obj, 'hoge', {
  value: 100,
  writable: false,
  configurable: false
});

console.log(obj.hoge); // > 100
obj.hoge = 200;
console.log(obj.hoge); // > 100
delete obj.hoge;
console.log(obj.hoge); // > 100

strictモードでは、非writableなプロパティを更新しようとしたり、非configurableなプロパティを削除しようとしたりするとTypeErrorの例外が吐かれます。strictモードかどうかで振る舞いが変わるので注意が必要です。

複数のプロパティをまとめて設定したいときは、Object.defineProperties()が有効です。

const obj = {};

Object.defineProperties(obj, {
  hoge: {
    value: 100,
    writable: false,
    configurable: false
  },
  piyo: {
    value: 200,
    writable: false,
    configurable: false
  }
});

console.log(obj.hoge); // > 100
console.log(obj.piyo); // > 200

ちなみに、属性の設定状況を確認するにはObject.getOwnPropertyDescriptor()またはObject.getOwnPropertyDescriptors()を利用します。

Object.getOwnPropertyDescriptors(obj); // 設定情報が返る

defineProperty()メソッドを使えば、値の書き換えや削除を制限できることがわかりました。しかしながら、利点でもあるのですがプロパティ1つ1つに対して属性の設定を書かなれければならないのは少々煩雑に感じることもあります。それに、意図しないプロパティの追加は抑制できません。

そこで、オブジェクト全体を丸ごとどうにかしちゃうメソッドをご紹介します。

Object.preventExtensions()

このメソッドは、プロパティの追加を禁止するためのものです。似たようなものにReflect.preventExtensions()というのも存在します。返り値と引数に違いがありますが、いずれもプロパティの追加を抑制します。

const obj = {};

Object.preventExtensions(obj);

obj.hoge = 100;
console.log(obj.hoge); // > undefined
const obj = {};

Reflect.preventExtensions(obj);

obj.hoge = 100;
console.log(obj.hoge); // > undefined

ご覧の通り簡単にオブジェクトの拡張を禁止できます。こちらもstrictモードでは拡張時に例外を吐くようになります。オブジェクトが拡張可能かどうかを確認するには、Object.isExtensible()というメソッドが利用できます。

const obj = {};

console.log(Object.isExtensible(obj)); // > true
Object.preventExtensions(obj); // 拡張を制限
console.log(Object.isExtensible(obj)); // > false

なお、preventExtensions()メソッドは拡張を制限するだけで、プロパティの削除や書き換えは制限しません。プロパティの削除も禁止にするには別の方法が用意されているので、次はそちらを見てみましょう。

Object.seal()

続きまして、オブジェクトを封印するObject.seal()です。メソッド名が封印(seal)とは、なんだかかっこいいですね笑

const obj = {
  piyo: 200
};

Object.seal(obj);

obj.hoge = 100;
console.log(obj.hoge); // > undefined

delete obj.piyo;
console.log(obj.piyo); // > 200

seal()は、オブジェクトの拡張とプロパティの削除を禁止するプロパティです。

さらに前述のObject.defineProperty()などによる属性の変更も禁止します。まさに封印、という感じですね笑

例にもれず、こちらもstrictモードではdelete文を使ったり、拡張しようとすると例外を吐きます。オブジェクトが封印されているかどうかを確認するためのメソッドはObject.isSealed()です。

const obj = {};

console.log(Object.isExtensible(obj)); // > true
console.log(Object.isSealed(obj)); // > false
Object.seal(obj); // 封★印
console.log(Object.isExtensible(obj)); // > false
console.log(Object.isSealed(obj)); // > true

完全に封印したように見えて実はseal()だとプロパティの書き換えは可能なままです。そこで、最後に拡張も削除も属性変更も値の書き換えも制限するメソッドをご紹介します。

Object.freeze()

満を持して登場。すべてを凍結させる最強の呪文(?)、Object.freeze()です。

const obj = {
  piyo: 200
};

Object.freeze(obj);

obj.hoge = 100;
console.log(obj.hoge); // > undefined

console.log(obj.piyo); // > 200
obj.piyo = 100;
console.log(obj.piyo); // > 200
delete obj.piyo;
console.log(obj.piyo); // > 200

このメソッドは、プロパティの書き換えも禁止します。なお、strictモードでは値の書き換えやdelete文の使用、プロパティの拡張をしようとすると例外を吐きます。オブジェクトくんは最強必殺技を前になすすべなく完全に凍り付きました。

オブジェクトが凍結されているかどうかはObject.isFrozen()というメソッドで確認できます。

const obj = {};

console.log(Object.isExtensible(obj)); // > true
console.log(Object.isSealed(obj)); // > false
console.log(Object.isFrozen(obj)); // > false
Object.freeze(obj); // 凍❄結
console.log(Object.isExtensible(obj)); // > false
console.log(Object.isSealed(obj)); // > true
console.log(Object.isFrozen(obj)); // > true

子階層より下も全部固める

これまで登場したメソッド群は、共通して1つだけ注意点があります。

それは、オブジェクトのプロパティがオブジェクトだった場合、そのオブジェクトは編集可能であるということです。

const parent = {
  child: {
    hoge: 100
  }
};

Object.freeze(parent);

console.log(parent.child.hoge); // > 100
parent.child.hoge = 200;
console.log(parent.child.hoge); // 200に書き換わっている

console.log(Object.isFrozen(parent)); // > true
console.log(Object.isFrozen(parent.child)); // > false

このような場合はparent.childオブジェクトも別途凍結しなければなりません。よくあるサンプルコードですが、次のコード内の関数deepFreeze()のような再帰的な関数を用意することで、隅々まで凍結させることができます。

const deepFreeze = (obj) => {
  Object.freeze(obj);

  for (const key in obj) {
    const item = obj[key];

    if (
       obj.hasOwnProperty(key) &&
       typeof item === 'object' &&
       item !== null &&
       !Object.isFrozen(item)
    ) {
      deepFreeze(item);
    }
  }
};
const parent = {
  child: {
    child: {
      child: {
        prop: 100
      }
    }
  }
};

deepFreeze(parent);

parent.child.child.child.prop = 200;
console.log(parent.child.child.child.prop); // > 100

console.log(Object.isFrozen(parent)); // > true
console.log(Object.isFrozen(parent.child)); // > true
console.log(Object.isFrozen(parent.child.child)); // > true
console.log(Object.isFrozen(parent.child.child.child)); // > true

まとめ

  • defineProperty()はプロパティの属性を設定するもの
  • preventExtensions()はオブジェクトの拡張を制限するもの
  • seal()はさらに削除と属性の変更も禁止するもの
  • freeze()はプロパティの値を書き換えることも禁止するもの
preventExtensions seal freeze
プロパティの追加 × × ×
プロパティの削除 × ×
プロパティの書き換え ×
プロパティの属性を変更 × ×

冬→寒い→冷たい→氷→Object.freeze()やな。という短絡的な発想でしたが、いかがだったでしょうか?🥳✨

これらのメソッドを用いて、意図しないオブジェクトの改変を防いでいければいいですね🙆‍♀️

この記事がおもしろかった!と思った方はぜひSNSでシェアしてくれるとうれしいです!

早くあったかくなってほしいー!🌸🌸🌸