2024年3月21日

プロパティフラグとディスクリプタ

ご存知の通り、オブジェクトはプロパティを格納できます。

これまで、プロパティは単純な “key-value” ペアでしたが、実際にはオブジェクトプロパティはより柔軟で強力なものです。

この章では、追加の設定オプションについて説明し、次の章では、それらを見えない形、getter/setter 関数にする方法について見ていきます。

プロパティフラグ

オブジェクトプロパティには、 value の他に、3つの特別な属性があります(“フラグ” と呼ばれています)。

  • writabletrue の場合、変更可能です。それ以外の場合は読み取り専用です。
  • enumerabletrue の場合、ループで列挙されます。それ以外の場合は列挙されません。
  • configurabletrue の場合、プロパティを削除したり属性の変更ができます。

一般的にこれらは姿を見せることがないため、まだ見ていませんでした。“通常の方法” でプロパティを作成するとき、これらはすべて true です。が、いつでもそれを変更することができます。

まず、フラグを取得する方法を見てみましょう。

メソッド Object.getOwnPropertyDescriptor で、プロパティの 完全な 情報を参照することができます。

構文は次の通りです:

let descriptor = Object.getOwnPropertyDescriptor(obj, propertyName);
obj
情報を取得するオブジェクトです。
propertyName
プロパティ名です。

返却値はいわゆる “プロパティディスクリプタ” オブジェクトと呼ばれます。: それは値とすべてのフラグを含んでいます。

例:

let user = {
  name: "John"
};

let descriptor = Object.getOwnPropertyDescriptor(user, 'name');

alert( JSON.stringify(descriptor, null, 2 ) );
/* プロパティディスクリプタ:
{
  "value": "John",
  "writable": true,
  "enumerable": true,
  "configurable": true
}
*/

Object.defineProperty でフラグの変更ができます。

構文:

Object.defineProperty(obj, propertyName, descriptor)
obj, propertyName
処理するオブジェクトとプロパティです。
descriptor
適用するプロパティディスクリプタです。

プロパティが存在する場合、defineProperty はそのフラグを更新します。存在しない場合は指定された値とフラグでプロパティを作ります。その場合に、もしフラグが指定されていなければ false とみなされます。

例えば、ここではプロパティ name はすべて false のフラグで作られます。:

let user = {};

Object.defineProperty(user, "name", {
  value: "John"
});

let descriptor = Object.getOwnPropertyDescriptor(user, 'name');

alert( JSON.stringify(descriptor, null, 2 ) );
/*
{
  "value": "John",
  "writable": false,
  "enumerable": false,
  "configurable": false
}
 */

“通常の方法で” 作成された user.name と上記を比較してください。今すべてのフラグは false です。このようにしたくなければ、descriptortrue をセットするのがよいでしょう。

では、例を使ってフラグの影響を見てみましょう。

書き込み不可(Non-writable)

writable フラグを変更して user.name を書き込み不可(再代入不可)にしてみましょう:

let user = {
  name: "John"
};

Object.defineProperty(user, "name", {
  writable: false
});

user.name = "Pete"; // Error: Cannot assign to read only property 'name'...

これで、defineProperty で上書きをしない限りは、誰も user.name を変えることはできません。

strict mode の場合のみエラーが表示されます

非 strict mode の場合、書き込み不可プロパティへの書き込みをしてもエラーは発生しません。ですが、操作は依然として成功はしません。非 strict 下では、フラグ違反の操作は単に無視されます。

これは先程と同じ例ですが、スクラッチでプロパティを作成します:

let user = { };

Object.defineProperty(user, "name", {
  value: "Pete",
  // 新しいプロパティに対して、true のものは明示的に列挙する必要があります
  enumerable: true,
  configurable: true
});

alert(user.name); // Pete
user.name = "Alice"; // Error

列挙可能でない(Non-enumerable)

カスタムの toStringuser に追加しましょう。

通常、オブジェクトが持つ組み込みの toString は列挙可能ではありません。それは for..in では表示されません。しかし私たちが自身の toString を追加した場合、デフォルトではこのように for..in で表示されます。:

let user = {
  name: "John",
  toString() {
    return this.name;
  }
};

// デフォルトでは、両方のプロパティは列挙されます:
for (let key in user) alert(key); // name, toString

列挙されたくなければ、enumerable:false をセットします。すると、組み込みの関数同様、for..in ループで列挙されなくなります。:

let user = {
  name: "John",
  toString() {
    return this.name;
  }
};

Object.defineProperty(user, "toString", {
  enumerable: false
});

// これで toString は消えました:
for (let key in user) alert(key); // name

列挙可能でないプロパティは Object.keys からも除外されます。:

alert(Object.keys(user)); // name

変更できない(Non-configurable)

組み込みオブジェクトやプロパティに対しては、変更不可フラグ(configurable:false)がプリセットされることがあります。

変更できないプロパティは削除したり変更することができません。

例えば、Math.PI は書き込み不可で、列挙不可であり、変更不可です:

let descriptor = Object.getOwnPropertyDescriptor(Math, 'PI');

alert( JSON.stringify(descriptor, null, 2 ) );
/*
{
  "value": 3.141592653589793,
  "writable": false,
  "enumerable": false,
  "configurable": false
}
*/

したがって、プログラマは Math.PI の値を変えることも上書きすることもできません。

Math.PI = 3; // Error

// delete Math.PI もまた動作しません

Math.PI を再度 writable にすることもできません。:

// Error, configurable: false なので
Object.defineProperty(Math, "PI", { writable: true });

Math.PI についてはは何もできません。

変更不可なプロパティの作成は一方通行であり、それを defineProperty 戻すことはできません。

注意: configurable: false はプロパティフラグの変更や削除を禁止しますが、値を変更することは可能です

ここでは、 user.name は変更不可ですが、依然として変更はできます(書き込み可なので):

let user = {
  name: "John"
};

Object.defineProperty(user, "name", {
  configurable: false
});

user.name = "Pete"; // 動作します
delete user.name; // Error

また、以下は組み込みのMath.PI のように user.name を “永遠に封印された定数” にしています。

let user = {
  name: "John"
};

Object.defineProperty(user, "name", {
  writable: false,
  configurable: false
});

// user.name とフラグは変更できません
// これらはすべて動作しません:
user.name = "Pete";
delete user.name;
Object.defineProperty(user, "name", { value: "Pete" });
唯一可能な属性変更: writable true → false

フラグ変更に関する小さな例外があります。

変更不可プロパティに対して、writable: truefalse に変更し、値の変更を防ぐことができます。しかし、その逆はできません。

Object.defineProperties

一度に多くのプロパティが定義できるメソッド Object.defineProperties(obj, descriptors)もあります。

構文は次の通りです:

Object.defineProperties(obj, {
  prop1: descriptor1,
  prop2: descriptor2
  // ...
});

例えば:

Object.defineProperties(user, {
  name: { value: "John", writable: false },
  surname: { value: "Smith", writable: false },
  // ...
});

なので、一度に多くのプロパティをセットできます。

Object.getOwnPropertyDescriptors

一度にすべてのプロパティのディスクリプタを取得するには、Object.getOwnPropertyDescriptors(obj) が使用できます。

Object.defineProperties と合わせて、“フラグを意識して” オブジェクトをクローンする方法として使うことができます。:

let clone = Object.defineProperties({}, Object.getOwnPropertyDescriptors(obj));

通常、オブジェクトをクローンするとき、次のようにプロパティをコピーするために代入を使います。:

for (let key in user) {
  clone[key] = user[key]
}

…ですが、これはフラグはコピーしません。そのため、“より良い” クローンを望むなら、 Object.defineProperties が好まれます。

もう1つの違いは、for..in はシンボルプロパティを無視しますが、Object.getOwnPropertyDescriptors はシンボリックなものを含む すべての プロパティディスクリプタを返します。

グローバルにオブジェクトを隠す

プロパティディスクリプタは個々のプロパティのレベルで動作します。

そこには、オブジェクト 全体 へのアクセスを制限するメソッドもあります。:

Object.preventExtensions(obj)
オブジェクトにプロパティを追加するのを禁止します。
Object.seal(obj)
プロパティの追加、削除を禁止し、既存のすべてのプロパティに configurable: false をセットします。
Object.freeze(obj)
プロパティの追加、削除、変更を禁止し、既存のすべてのプロパティに configurable: false, writable: false をセットします。

また、それらを確認する方法もあります:

Object.isExtensible(obj)
プロパティの追加が禁止されている場合に false を返します。それ以外は true です。
Object.isSealed(obj)
プロパティの追加、削除が禁止されており、すべての既存のプロパティが configurable: false を持っている場合に true を返します。
Object.isFrozen(obj)
プロパティの追加、削除、変更が禁止されており、すべての現在のプロパティが configurable: false, writable: false の場合に true を返します。

これらのメソッドは実際にはめったに使われません。

チュートリアルマップ