JavaScriptのObject.definePropertyの使い方!単一のプロパティを定義する

JavaScriptのObject.definePropertyメソッドは、指定されたオブジェクトに単一の新しいプロパティを直接追加したり、既存のプロパティの属性を変更したりするために使用されます。

これは、プロパティのだけでなく、そのプロパティが書き込み可能か列挙可能か設定変更可能か、あるいはゲッター/セッターを持つかといった詳細な振る舞いを細かく制御したい場合に使用することができます。
通常のプロパティ代入(例: obj.prop = value)では設定できない、より詳細なプロパティの制御を可能にします。

この記事ではObject.definePropertyメソッドの基本的な使い方、引数、戻り値、主要な使用例、そして使用する際の重要な注意点について詳しく解説します。

Object.definePropertyメソッドの基本的な構文

Object.definePropertyメソッドの基本的な構文は以下の通りです。

Object.defineProperty(obj, propName, descriptor)
  • obj: 必須。プロパティを定義または変更したいオブジェクトです。
  • propName: 必須。定義または変更したいプロパティの名前(文字列またはシンボル)です。
  • descriptor: 必須。プロパティの属性を定義するプロパティディスクリプタオブジェクトです。

重要な点

*Object.definePropertyは、オブジェクトのプロパティのデフォルトの属性を上書きするのに使用されます。通常のプロパティ代入(例: obj.prop = value)で作成されたプロパティは、デフォルトでwritable: true, enumerable: true, configurable: true となりますが、defineProperty を使うとこれらを細かく制御できます。
* このメソッドは、新しいプロパティの追加だけでなく、既存のプロパティの属性変更にも使えます。

引数1(obj)

  • 必須です。
  • プロパティを定義または変更する対象のオブジェクトです。

引数2(propName)

  • 必須です。
  • 文字列またはシンボルで指定します。

引数3(descriptor)

  • 必須です。
  • このオブジェクトは、propNameで指定されたプロパティの属性を定義します。

プロパティディスクリプタには、主に2種類あります。

1. データディスクリプタ (Data Descriptor): プロパティの値を直接持つ場合。

  • value: プロパティの実際の値。デフォルトは undefined
  • writable: trueの場合、valueを変更できる。デフォルトはfalse
  • enumerable: trueの場合、for...inループやObject.keysで列挙できる。デフォルトはfalse
  • configurable: trueの場合、プロパティのディスクリプタを変更したり、プロパティを削除したりできる。デフォルトはfalse

2. アクセサディスクリプタ (Accessor Descriptor): ゲッター/セッター関数を持つ場合。

  • get: ゲッター関数。プロパティが読み取られたときに呼び出される。デフォルトは undefined
  • set: セッター関数。プロパティが設定されたときに呼び出される。デフォルトは undefined
  • enumerable: true の場合、列挙できる。デフォルトは false
  • configurable: true の場合、設定変更・削除できる。デフォルトは false

ディスクリプタの表

上記ふたつを表にまとめました。

属性名 データ型 デフォルト 説明
value 任意 undefined プロパティの値
writable boolean false 値を変更できるか
enumerable boolean false for...inなどで列挙されるか
configurable boolean false 削除や変更が可能か
get function undefined プロパティ読み取り時に呼び出される
set function undefined プロパティ設定時に呼び出される

注意

データディスクリプタとアクセサディスクリプタの属性を同時に指定することはできません。
例えば、valuegetを同じプロパティに指定するとエラーになります。

Object.definePropertyの戻り値

変更されたオブジェクト(obj)が返されます。

Object.definePropertyメソッドを使ってみる

実際にObject.definePropertyを使って、動作を確認していきます。

例1:基本的なデータプロパティの定義

オブジェクトに、値と属性を細かく制御したプロパティを追加します。

"use strict"; // 厳格モードを有効にする

const user = {};

// name プロパティを定義: 値は'Alice'、書き込み可能、列挙可能、設定変更可能
Object.defineProperty(user, 'name', {
  value: 'Alice',
  writable: true,
  enumerable: true,
  configurable: true
});

console.log(user.name); // 出力: Alice
user.name = 'Bob';      // writable: true なので変更可能
console.log(user.name); // 出力: Bob

console.log(Object.keys(user)); // 出力: [ 'name' ]

userオブジェクトを作成して、Object.definePropertyを使用してプロパティの設定をしています。

第2引数に設定したnameがプロパティ名です。
value(値)にはAliceを設定し、それぞれのプロパティの設定を行なっています。
writableenumerableconfigurabletrueに設定することで、書き込み可能で、列挙可能で、設定変更可能なプロパティとなっています。

結果としてnameプロパティは、通常のプロパティ代入で作成したかのように振る舞いますが、ここでは属性を明示的に指定しています。

例2:読み取り専用プロパティの作成

プロパティのwritable属性をfalseに設定することで、値を変更できないようにします。

"use strict";

const config = {};

// version プロパティを定義: 値は'1.0.0'、書き込み不可、列挙可能、設定変更不可
Object.defineProperty(config, 'version', {
  value: '1.0.0',
  writable: false, // 変更不可
  enumerable: true,
  configurable: false // 削除不可、属性変更不可
});

console.log(config.version); // 出力: 1.0.0

try {
  config.version = '1.0.1'; // writable: false なのでTypeErrorが発生
} catch (e) {
  console.error("エラー: versionは変更できません。", e.message); // 出力: エラー: Cannot assign to read only property 'version' of object '#<Object>'
}

// configurable: false なので、後から属性を変更したり削除したりすることもできない
try {
  delete config.version; // configurable: false なのでTypeErrorが発生
} catch (e) {
  console.error("エラー: versionは削除できません。", e.message); // 出力: エラー: Cannot delete property 'version' of object '#<Object>'
}

configオブジェクトを作成して、Object.definePropertyを使用してプロパティの設定をしています。
先ほどと違って、今回はwritablefalseconfigurableにもfalseを設定しています。

これでversionプロパティは、一度設定されると値を変更できず、削除もできない「定数」のような振る舞いをします。

例3:列挙されない(隠された)プロパティの作成

enumerablefalseに設定してみます。
これでfor...inループやObject.keysなどでプロパティが列挙されないようにします。

"use strict";

const userProfile = {
  username: 'Alice'
};

// userId プロパティを定義: 列挙不可
Object.defineProperty(userProfile, 'userId', {
  value: 'u_12345',
  writable: true,
  enumerable: false, // 列挙されない
  configurable: true
});

console.log(userProfile.userId); // 出力: u_12345 (直接アクセスは可能)

console.log(Object.keys(userProfile)); // 出力: [ 'username' ] (userIdは含まれない)
for (let key in userProfile) {
  console.log(`for...in: ${key}`); // 出力: for...in: username (userIdは含まれない)
}

// Object.getOwnPropertyNames() や Object.getOwnPropertyDescriptors() では取得可能
console.log(Object.getOwnPropertyNames(userProfile)); // 出力: [ 'username', 'userId' ]

usernameプロパティを最初から持っている、userProfileオブジェクトを作成しています。

作成したuserProfileオブジェクトに対して、userIdプロパティを追加しています。
追加時にenumerablefalseにして、列挙されないようにしました。

出力してみると、直接アクセスはできますが、Object.keysfor...inループで対象が取れなくなっています。

このようにuserIdプロパティはオブジェクトに存在しますが、一般的なプロパティの列挙方法では見えません。
これは、内部的なIDやメタデータなど、ユーザーに直接表示する必要のない情報を格納するのに役立ちます。

例4:ゲッターとセッターを持つプロパティを定義する

プロパティの読み書き時に特定の処理を実行したい場合にアクセサディスクリプタを使用します。

"use strict";

const circle = {
  _radius: 0 // 実際の半径を保持するプライベートなプロパティ
};

// radius プロパティを定義: ゲッターとセッターを持つ
Object.defineProperty(circle, 'radius', {
  enumerable: true, // 列挙可能
  configurable: true, // 設定変更可能
  get: function() {
    console.log("radiusを読み込みました。");
    return this._radius;
  },
  set: function(newRadius) {
    if (newRadius < 0) {
      console.warn("半径は負の値に設定できません。");
      return;
    }
    console.log(`radiusを${newRadius}に設定しました。`);
    this._radius = newRadius;
  }
});

// area プロパティを定義: ゲッターのみを持つ (読み取り専用の計算プロパティ)
Object.defineProperty(circle, 'area', {
  enumerable: true,
  configurable: true,
  get: function() {
    console.log("areaを計算しました。");
    return Math.PI * this._radius * this._radius;
  }
});

circle.radius = 10; // セッターが呼び出される (出力: radiusを10に設定しました。)
console.log(circle.radius); // ゲッターが呼び出される (出力: radiusを読み込みました。 10)

circle.radius = -5; // セッター内の警告が表示され、値は変更されない
console.log(circle.radius); // 出力: 10

console.log(circle.area); // areaのゲッターが呼び出される (出力: areaを計算しました。 314.159...)

circleオブジェクトを作成して、オブジェクトに対してradiusareaプロパティを追加しました。
radiusプロパティにはゲッターとセッター、areaプロパティにはゲッタ―を追加しています。

radiusプロパティは、読み書き時にログを出力し、負の値を設定できないようにバリデーションを行っています。
areaプロパティはゲッターのみを持つため、radiusに基づいて計算された読み取り専用の値を提供します。

出力を確認すると、それぞれのプロパティ、読み込んだり書き込んだりするとゲッター・セッターメソッドが動作していることが確認できました。

Object.definePropertyを使う際の注意点

Object.definePropertyを使う際の注意点についてです。

厳格モードでのエラー

Object.definePropertyを使用する際は、通常厳格モード ("use strict")で開発することをお勧めします。
非厳格モードでは、プロパティの定義に失敗してもサイレントに失敗する(エラーが発生しない)ことがあり、デバッグが困難になるためです。

既存プロパティの変更

既に存在するプロパティに対してdefinePropertyを使用すると、既存の属性が上書きされます。
特にconfigurable: falseに設定すると、そのプロパティは削除できなくなり、その属性(writable, enumerable, configurable 自体)も二度と変更できなくなるため、非常に強力かつ不可逆な操作となります。

データディスクリプタとアクセサディスクリプタの排他性

valueまたはwritableと、getまたはsetを同じプロパティディスクリプタ内で同時に指定することはできません。
どちらか一方のタイプのみを使用する必要があります。

デフォルト値

プロパティディスクリプタで明示的に指定しなかった属性(例: enumerableconfigurable)は、デフォルト値が適用されます。
これらのデフォルト値は、通常のプロパティ代入(obj.prop = value)で作成されるプロパティのデフォルト値とは異なる(通常は false)ため、注意が必要です。

プロトタイプチェーン上のプロパティ

Object.definePropertyは、オブジェクト自身のプロパティのみを操作します。
プロトタイプチェーンから継承されたプロパティは直接変更できません。

まとめ

JavaScriptのObject.definePropertyメソッドは、オブジェクトに単一のプロパティを定義したり、既存のプロパティの属性を詳細に制御したりするための非常に強力なツールです。
読み取り専用プロパティの作成、列挙されないプロパティの定義、ゲッター/セッターによるカスタムロジックの実装など、オブジェクトの振る舞いを細かくカスタマイズしたい場合に便利です。

しかし、その強力さゆえに、プロパティディスクリプタの各属性の意味、厳格モードでのエラー挙動、そして一度設定したconfigurable: falseが不可逆であることなど、重要な注意点を深く理解して使用することが不可欠です。
これらのポイントを踏まえ、Object.definePropertyを効果的に活用し、JavaScriptアプリケーションの堅牢性と柔軟性を高めましょう。

コメント