JavaScriptのObject.definePropertiesの使い方!複数のプロパティを定義する

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

これは、プロパティのだけでなく、そのプロパティが書き込み可能か列挙可能か設定変更可能か、あるいはゲッター/セッターを持つかといった詳細な振る舞いを一度に制御したい場合に非常に強力です。

Object.definePropertyが単一のプロパティを定義するのに対し、Object.definePropertiesは複数のプロパティを効率的に定義できるため、オブジェクトの初期化やメタプログラミングにおいて特に役立ちます。
この記事では、Object.definePropertiesメソッドの基本的な使い方、引数、戻り値、主要な使用例、そして使用する際の重要な注意点について解説します。

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

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

Object.defineProperties(obj, props)
  • obj: 必須。プロパティを定義または変更したいオブジェクトです。
  • props: 必須。プロパティ名と、それに対応するプロパティディスクリプタを含むオブジェクトです。

下記は簡単な例です。

Object.defineProperties(obj, {
  prop1: { value: 10, writable: true },
  prop2: { get() { return 20; } }
});

第1引数(obj)

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

第2引数(props)

  • 必須です。
  • このオブジェクトの各プロパティは、ターゲットオブジェクトに追加/変更したいプロパティの名前を表します。
  • 各プロパティの値は、そのプロパティの属性を定義するプロパティディスクリプタオブジェクトです。

プロパティディスクリプタについて

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

  1. データディスクリプタ (Data Descriptor): プロパティの値を直接持つ場合。
  • value: プロパティの実際の値。デフォルトは undefined
  • writable: true の場合、value を変更できる。デフォルトは false
  • enumerable: true の場合、for...in ループや Object.keys() で列挙できる。デフォルトは false
  • configurable: true の場合、プロパティのディスクリプタを変更したり、プロパティを削除したりできる。デフォルトは false
  1. アクセサディスクリプタ (Accessor Descriptor): ゲッター/セッター関数を持つ場合。
  • get: ゲッター関数。プロパティが読み取られたときに呼び出される。デフォルトは undefined
  • set: セッター関数。プロパティが設定されたときに呼び出される。デフォルトは undefined
  • enumerable: true の場合、列挙できる。デフォルトは false
  • configurable: true の場合、設定変更・削除できる。デフォルトは false

注意

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

Object.definePropertiesの戻り値

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

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

例1:複数のデータプロパティを定義する

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

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

const user = {};

// userオブジェクトに複数のプロパティを定義
Object.defineProperties(user, {
  // name プロパティ: 値は'Alice'、書き込み可能、列挙可能、設定変更可能
  name: {
    value: 'Alice',
    writable: true,
    enumerable: true,
    configurable: true
  },
  // id プロパティ: 値は101、書き込み不可、列挙可能、設定変更不可
  id: {
    value: 101,
    writable: false, // 変更不可
    enumerable: true,
    configurable: false // 削除不可、属性変更不可
  },
  // secretKey プロパティ: 値は'xyz123'、書き込み可能、列挙不可、設定変更可能
  secretKey: {
    value: 'xyz123',
    writable: true,
    enumerable: false, // for...in や Object.keys() で表示されない
    configurable: true
  }
});

console.log(user); // 出力: { name: 'Alice', id: 101 } (secretKeyは列挙不可なので表示されない)

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

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

console.log(Object.keys(user)); // 出力: [ 'name', 'id' ] (secretKeyは含まれない)

// idプロパティの削除を試みる
try {
  delete user.id; // configurable: false なのでエラー
} catch (e) {
  console.error("エラー: idは削除できません。", e.message); // 出力: エラー: Cannot delete property 'id' of object '#<Object>'
}

userオブジェクトを作成して、Object.definePropertiesを使用して、複数のプロパティを定義しています。
writableが「true」になっているnameプロパティは変更することが可能で、writableが「false」のidプロパティは変更や削除ができないことが確認できます。
また、enumerablefalseに設定しているためsecretKeyプロパティは、Object.keysでキーが含まれないことが確認できました。(※ブラウザの開発ツールに依存する)

このようにnameは通常のプロパティのように振る舞い、idは読み取り専用で削除もできないプロパティになって
secretKeyは値は設定できるものの、オブジェクトのキーとして列挙されない(隠された)プロパティになりました。

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

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

"use strict";

const product = {
  _price: 0 // 実際の価格を保持するプライベートなプロパティ
};

Object.defineProperties(product, {
  // price プロパティ: ゲッターとセッターを持つ
  price: {
    enumerable: true,
    configurable: true,
    get: function() {
      console.log("priceを読み込みました。");
      return this._price;
    },
    set: function(newPrice) {
      if (newPrice < 0) {
        console.warn("価格は負の値に設定できません。");
        return;
      }
      console.log(`priceを${newPrice}に設定しました。`);
      this._price = newPrice;
    }
  },
  // discountPrice プロパティ: ゲッターのみを持つ (読み取り専用の計算プロパティ)
  discountPrice: {
    enumerable: true,
    get: function() {
      // priceプロパティのゲッターを介して_priceにアクセス
      return this.price * 0.9; // 10%割引
    }
  }
});

product.price = 100; // セッターが呼び出される
console.log(product.price); // ゲッターが呼び出される (出力: priceを読み込みました。 100)

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

console.log(product.discountPrice); // ゲッターが呼び出される (出力: 90)

productオブジェクトを作成して、priceプロパティとdiscountPriceプロパティを定義しています。

priceプロパティにset/getを書くことで、値の書き込み時と読み込み時に処理をすることができます。
discountPriceプロパティではgetのみ用意しました。

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

結果をみると、それぞれプロパティが呼び出された時にログが出力されていることが確認できます。

priceを100に設定しました。
priceを読み込みました。
100
価格は負の値に設定できません。
priceを読み込みました。
100
priceを読み込みました。
90

例3:既存のプロパティの属性を変更する

既に存在するプロパティの属性を変更することもできます。
ただし、configurable: falseに設定されたプロパティの属性は、後から変更できません。

"use strict";

const config = {
  version: '1.0.0',
  debugMode: true
};

console.log(Object.getOwnPropertyDescriptor(config, 'version'));
/*
出力例:
{
  value: '1.0.0',
  writable: true,
  enumerable: true,
  configurable: true
}
*/

// versionを読み取り専用にする
Object.defineProperties(config, {
  version: {
    writable: false,
    configurable: false // これを設定すると、後からwritableをtrueに戻したり、プロパティを削除したりできなくなる
  }
});

console.log(Object.getOwnPropertyDescriptor(config, 'version'));
/*
出力例:
{
  value: '1.0.0',
  writable: false,
  enumerable: true, // 変更なし
  configurable: false // 変更された
}
*/

try {
  config.version = '1.0.1'; // writable: false なのでエラー
} catch (e) {
  console.error("エラー: versionは変更できません。", e.message);
}

try {
  // configurable: false になったので、このプロパティの属性を再度変更しようとするとエラー
  Object.defineProperties(config, {
    version: {
      writable: true // エラーになる
    }
  });
} catch (e) {
  console.error("エラー: versionの属性は変更できません。", e.message); // 出力: エラー: Cannot redefine property: version
}

この例では、versionプロパティを読み取り専用にし、さらにconfigurable: falseに設定することで、そのプロパティの属性を二度と変更できないようにしています。

Object.definePropertiesを使うときの注意点

Object.definePropertiesを使うときの注意点です。

厳格モードでのエラー

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

既存プロパティの変更

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

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

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

デフォルト値

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

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

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

まとめ

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

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

コメント