definePropertyメソッドでプロパティを定義するには?[JavaScript].NET TIPS

JavaScriptではdefinePropertyメソッドによって、オブジェクトのプロパティを定義し、その属性を細やかに制御できる。

» 2016年05月27日 05時00分 公開
[かわさきしんじInsider.NET編集部]
.NET TIPS
Insider.NET

 

「.NET TIPS」のインデックス

連載目次

 「.NET TIPS: ゲッターとセッター(プロパティ)を定義するには?[JavaScript]」では、JavaScriptでゲッター/セッターを定義する方法を幾つか紹介した。本稿では、そのうちのObject.definePropertyメソッドについて、さらに見ていくことにする。

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

 Object.definePropertyメソッドは以下の構文で、指定したオブジェクトにプロパティを定義する。

Object.defineProperty(
  オブジェクト,
  プロパティ名,
  プロパティディスクリプタ
)

definePropertyメソッドの構文

 第1引数に指定したオブジェクトに、第2引数に指定した名前のプロパティが定義される。第3引数には、そのプロパティディスクリプタ(プロパティの属性)を指定する。プロパティディスクリプタにはデータプロパティディスクリプタとアクセサプロパティディスクリプタの2種類がある(後述)。

 以下に簡単なプロパティの定義例を示す。

var obj = {};
Object.defineProperty(obj, 'prop', { value: 'foo' });
// alert(obj.prop);
console.log(obj.prop);

objオブジェクトにpropプロパティを定義(データプロパティディスクリプタを使用)

 ここではプロパティディスクリプタに「{ value: 'foo' }」と記述している。このようにvalueという名前のフィールド(あるいはwritableフィールド)を持つディスクリプタのことをデータプロパティディスクリプタ(以下、データディスクリプタ)と呼ぶ。

 以下にプロパティ定義をもう1つ示す。

var obj = {};
Object.defineProperty(obj, 'prop',
  { get: function() { return 'foo'} }
);
console.log(obj.prop);

objオブジェクトにpropプロパティを定義(アクセサプロパティディスクリプタを使用)

 この例では、プロパティディスクリプタにgetフィールドがある。get/setフィールドを持つプロパティディスクリプタのことをアクセサプロパティディスクリプタ(以下、アクセサディスクリプタ)と呼ぶ。データディスクリプタとアクセサディスクリプタは相互排他的、つまり、value/writableフィールドを持つディスクリプタには、get/setフィールドを記述できないし、その逆も同様だ。

var obj = {};
Object.defineProperty(obj, 'prop',
  {
    value: 'foo',
    get: function() { return 'foo'}
  }
);
console.log(obj.prop);

valueフィールドとgetフィールドを同時に記述はできない
このコードはエラーとなる。

 つまり、Object.definePropertyメソッドを使って、ゲッター/セッターを必要としない単純なプロパティを定義するにはデータディスクリプタを、これらを必要とするプロパティを定義するにはアクセサディスクリプタを使用する。

 なお、実際にアクセサディスクリプタを指定して、ゲッター/セッターを持つプロパティを実装するには、「.NET TIPS: ゲッターとセッター(プロパティ)を定義するには?[JavaScript]」でも行っているように、ファクトリ関数やコンストラクタ関数の内部にプライベートなローカル変数を用意してクロージャを形成することになるだろう(後述のコード例も参考のこと)。

プロパティディスクリプタのフィールド

 プロパティディスクリプタには上で見たvalueフィールド、get/setフィールド以外にも幾つかのフィールドを記述できる。これらを使うことで、定義するプロパティの属性や挙動を制御可能だ。

 データディスクリプタには、valueフィールドに加えて、上述のwritableフィールドとconfigurableフィールド、enumerableフィールドを記述できる。アクセサディスクリプタにはget/setフィールドに加えて、configurableフィールドとenumerableフィールドを記述できる。configurableフィールドとenumerableフィールドは2つのディスクリプタに共通のフィールドで、プロパティの属性変更やプロパティの削除が可能か(configurableフィールド)、for-inによる列挙の対象とするか(enumerableフィールド)を指定する。データディスクリプタのwritableフィールドは、valueフィールドに記述したプロパティの値が変更可能かを指定する。

データディスクリプタを使用したプロパティ定義

 以下にデータディスクリプタに記述可能なフィールドをまとめる。

フィールド 説明
value 任意の型 プロパティの値。デフォルト値はundefined
writable boolean trueならvalueフィールドの値を変更可能。デフォルト値はfalse(変更不可能)
enumerable boolean trueならfor-inループでこのプロパティが列挙される。デフォルト値はfalse(列挙されない)
configurable boolean falseならこのプロパティの削除、アクセサを持つプロパティへの変更、valueフィールド以外の値の変更が不可能。デフォルト値はfalse(フィールド値の変更やプロパティの削除が不可能)
データディスクリプタに記述可能なフィールド

 以下にこれらのフィールドの使用例を示す。

function makePersonData1(name, tel) {
  var p = {};
  Object.defineProperty(p, 'name', { value: name, enumerable: true });
  Object.defineProperty(p, 'tel', { value: tel, writable: true });
  return p;
}

var p1 = makePersonData1('Insider.NET', '2222-2222');
// enumerableフィールドの値がtrueなのはnameプロパティのみ
// 出力結果は「name: Insider.NET」のみ
for (var item in p1) {
  console.log(item + ': ' + p1[item]);
}
// nameプロパティはwritableフィールドの値がfalse
p1.name = 'Build Insider'; // strictモードではエラーになる
p1.tel = '3333-3333';
console.log(p1.name + ': ' + p1.tel); // Insider.NET: 3333-3333

データディスクリプタを利用したプロパティ定義の例

 ここではnameとtelの2つのプロパティを定義している。nameプロパティはwritableフィールドの値がfalse(デフォルト値)で、enumerableフィールドの値はtrueだ。telプロパティはwritableフィールドの値がtrueとなっている(enumerableフィールドの値はデフォルト値のfalse)。

 そのため、for-inループでのプロパティ名の列挙では、nameプロパティだけが列挙される。また、nameプロパティの値を書き換えようとしても変更はできない。コメントにも書いた通り、strictモードではエラーが発生する。telプロパティは書き換えが可能だ。

アクセサディスクリプタを使用したプロパティ定義

 以下はアクセサディスクリプタに記述可能なフィールドだ。

フィールド 説明
get Function/undefined プロパティの値の読み出し時にこの関数が呼び出される。デフォルト値はundefined(ゲッターが存在しない)
set Function/undefined プロパティの値の設定時にこの関数が呼び出される。デフォルト値はundefined(セッターが存在しない)
enumerable boolean trueならfor-inループでこのプロパティが列挙される。デフォルト値はfalse(列挙されない)
configurable boolean falseならこのプロパティの削除、データプロパティへの変更、フィールド値の変更が不可能。デフォルト値はfalse(フィールド値の変更やプロパティの削除が不可能)
アクセサディスクリプタに記述可能なフィールド

 以下にこれらのフィールドの使用例を示す。上のデータディスクリプタを使用したプロパティ定義と同じ意味を持つコードだ。

function makePersonData2(name, tel) {
  var _name = name;
  var _tel = tel;

  var p = {};
  Object.defineProperty(p, 'name',
    {
      get: () => _name,
      enumerable: true
    }
  );
  Object.defineProperty(p, 'tel',
    {
      get: () => _tel,
      set: newtel => _tel = newtel
    }
  );
  return p;
}

var p2 = makePersonData2('Insider.NET', '2222-2222');

// enumerableフィールドの値がtrueなのはnameプロパティのみ
// 出力結果は「name: Insider.NET」のみ
for (var item in p2) {
  console.log(item + ': ' + p2[item]);
}

// nameプロパティのsetフィールドの値はundefinedなので読み出し専用
p2.name = 'Build Insider'; // strictモードではエラーとなる
p2.tel = '3333-3333';
console.log(p2.name + ': ' + p2.tel);

アクセサディスクリプタの利用例
nameプロパティでは、setフィールドの記述がないので、これは読み出し専用のプロパティとなる。
なお、記述が簡潔になるようにget/setフィールドにはアロー関数を記述している。

 ここではnameとtelの2つのプロパティを定義している。nameプロパティのsetフィールドは記述していない(デフォルト値はundefined)なので、これは読み出し専用のプロパティとなる。また、enumerableフィールドの値はtrueになっている。telプロパティはget/setフィールドを記述しているので読み書きが行える。enumerableフィールドは記述していないので、その値はデフォルト値のfalseとなっている。

 そのため、for-inループでのプロパティ名の列挙では、nameプロパティだけが列挙される。また、nameプロパティの値を書き換えようとしても変更はできない。コメントにも書いた通り、strictモードではエラーが発生する。telプロパティは書き換えが可能だ。

configurableフィールド

 configurableフィールドは、プロパティディスクリプタの各フィールドの値を変更可能か、プロパティを削除できるかを示すものだ。以下に例を示す。

var v = {};
Object.defineProperty(v, 'foo',
  {
    configurable: true,
    enumerable: false,
    writable: false,
    value: 'foo'
  }
);

console.log('1st for-in loop');
for (var item in v) {
  console.log(item + ': ' + v[item]);
}

Object.defineProperty(v, 'foo', { enumerable: true, writable: true });
v.foo = 'bar';

console.log('2nd for-in loop');
for (var item in v) {
  console.log(item + ': ' + v[item]);
}

configurableフィールドの利用例

 ここでは、オブジェクトvにプロパティfooを定義している。その値(valueフィールド)は'foo'であり、configuableフィールドはtrue(データディスクリプタのフィールドを変更可能/プロパティを削除可能)、writable/enumerableフィールドはfalseを明示的に指定している。

 そのため、最初のfor-inループではfooプロパティは列挙されない。しかし、その後のObject.definePropertyメソッド呼び出しでデータディスクリプタに「{ enumerable: true, writable: true }」を指定している。これにより、fooプロパティの値を更新できるようになっている。また、次のfor-inループではfooプロパティが列挙されるようになる。

まとめ

 Object.definePropertyメソッドを使うと、オブジェクトにプロパティを定義できる。記述は煩雑になるが(特にアクセサディスクリプタを使用した場合)、読み出し専用のプロパティの定義や、プロパティの属性を設定が可能だ。本稿では単純な例を示したのみだが、プロパティの読み出しや設定の際にゲッター/セッターを使って何らかの処理を挟み込みたい場合に、このメソッドの利用価値があるだろう。

「.NET TIPS」のインデックス

.NET TIPS

Copyright© Digital Advantage Corp. All Rights Reserved.

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。