Object.defineProperty()

静的メソッドの Object.defineProperty() は、あるオブジェクトに新しいプロパティを直接定義したり、オブジェクトの既存のプロパティを変更したりして、そのオブジェクトを返します。

メモ: このメソッドは Object コンストラクターで直接呼び出すものであって、Object 型のインスタンスで呼ぶものではありません。

構文

Object.defineProperty(obj, prop, descriptor)

引数

obj
プロパティを定義したいオブジェクト。
prop
定義または変更するプロパティの名前または Symbol
descriptor
定義または変更されるプロパティの記述子。

返値

渡されたオブジェクトをそのまま返します。

解説

このメソッドで、あるオブジェクトのプロパティを明示的に追加または変更することができます。代入による通常のプロパティ追加では、プロパティ列挙 (for...in ループや Object.keys メソッド) に現れ、値は変更可能で、また削除も可能なプロパティが生成されます。このメソッドでは、これらの詳細事項を既定値から変えることが可能です。既定では、Object.defineProperty() を使って追加された値は不変になります。

プロパティの記述子は、データ記述子とアクセサー記述子の二つに分かれます。データ記述子は値を持つプロパティで、その値は書き換え可能にも不可能にもできます。アクセサー記述子は、関数のゲッターとセッターの組で表されるプロパティです。記述子はこれら二種類のどちらかでなければならず、両方になることはできません。

どちらの形でも記述子はオブジェクトで表現します。共通して以下のキーを持つことができます (既定値は Object.defineProperty() を使ってプロパティを定義する場合)。

configurable
true である場合のみ、この種の記述子を変更することや、対応するオブジェクトからプロパティを削除することができます。
既定値は false です。
enumerable
true である場合のみ、このプロパティは対応するオブジェクトでのプロパティ列挙に現れます。
既定値は false です。

データ記述子の場合はオプションとして次のキーを持つことができます:

value
プロパティに関連づけられた値です。有効な JavaScript の値 (number, object, function など) である必要があります。
既定値は undefined です。
writable
true である場合のみ、プロパティに関連づけられた値は代入演算子で変更することができます。
既定値は false です。

アクセサー記述子の場合はオプションとして次のキーを持つことができます。

get
プロパティのゲッターとなる関数で、ゲッターを設けない場合は undefined です。プロパティにアクセスするとこの関数が引数なしでコールされます。この関数内で this はアクセスしようとしたプロパティを持つオブジェクトになります(プロパティを定義するために作成した記述子オブジェクトではありません)。戻り値はこのプロパティの値として使われます。
既定値は undefined です。
set
プロパティのセッターとなる関数で、セッターがない場合は undefined です。プロパティに値が割り当てられたとき、その値を引数としてこの関数がコールされます。この関数内で this は割り当てようとしたプロパティを持つオブジェクトになります。
既定値は undefined です。

記述子に value, writable, get, set のいずれのキーもない場合、データ記述子として扱われます。記述子に value または writable と、get または set のキーの両方がある場合は、例外が投げられます。

これらのキーは必ずしも記述子が直接所有しているとは限らないことに留意してください。継承されたプロパティも同様です。これらの既定値が存在することを保証するには、先行して Object.prototype を freeze しておくか、すべてのオプションを明示的に指定するか、Object.create(null)null に設定するかします。

// __proto__ を使うやり方
var obj = {};
var descriptor = Object.create(null); // 意図しないキーの継承を防止します。
descriptor.value = 'static';

// 既定で継承不可、変更不可、書換不可のプロパティとなります。
Object.defineProperty(obj, 'key', descriptor);

// 明示的な指定
Object.defineProperty(obj, 'key', {
  enumerable: false,
  configurable: false,
  writable: false,
  value: 'static'
});

// 同じオブジェクトを再利用
function withValue(value) {
  var d = withValue.d || (
    withValue.d = {
      enumerable: false,
      writable: false,
      configurable: false,
      value: value
    }
  );

  // 値の代入で重複操作を防ぐ
  if (d.value !== value) d.value = value;

  return d;
}
// このように使います。
Object.defineProperty(obj, 'key', withValue('static'));

// freeze が利用できるなら、オブジェクトのプロトタイプのプロパティ
// (value, get, set, enumerable, writable, configurable) を
// 追加・削除することを防ぐことができます。
(Object.freeze || Object)(Object.prototype);

バイナリーフラグを使って Object.defineProperty を利用したい場合は 追加の例 を見てください。

プロパティの生成

オブジェクトに指定されたプロパティが存在しないとき、Object.defineProperty() は指定された形で新たなプロパティを生成します。記述子のキーは省略することができ、そのようなキーには既定値が適用されます。

var o = {}; // 新しいオブジェクトの生成

// データ記述子により、defineProperty を用いて
// オブジェクトプロパティを追加する例
Object.defineProperty(o, 'a', {
  value: 37,
  writable: true,
  enumerable: true,
  configurable: true
});
// o オブジェクトに 'a' プロパティが存在するようになり、その値は 37 となります

// アクセサー記述子により、defineProperty を用いて
// オブジェクトプロパティを追加する例
var bValue = 38;
Object.defineProperty(o, 'b', {
  // メソッド名ショートハンドを利用しています(ES2015 の機能)。
  // 次のように書いているのと同じことです:
  // get: function() { return bValue; },
  // set: function(newValue) { bValue = newValue; },
  get() { return bValue; },
  set(newValue) { bValue = newValue; },
  enumerable: true,
  configurable: true
});
o.b; // 38
// o オブジェクトに 'b' プロパティが存在するようになり、
// その値は 38 となります
// o.b は再定義されない限り、その値は常に bValue と同じです。

// (訳注:データとアクセサーを)両方を混在させることはできません:
Object.defineProperty(o, 'conflict', {
  value: 0x9f91102,
  get() { return 0xdeadbeef; }
});
// TypeError が発生します。value はデータ記述子にのみ、
// get はアクセサー記述子にのみ存在していなければなりません。

プロパティの変更

プロパティが既に存在している場合、Object.defineProperty() は記述子の値および現在のオブジェクトの設定に基づいて、プロパティの変更を試みます。元の記述子で configurable 属性が false なら、そのプロパティは「変更不可」です。変更不可のプロパティは記述子の属性を変更することができません。データプロパティで writable なら、値を変更することができますし、writable 属性を true から false に変更することが出来ます。変更不可のプロパティはデータとアクセサーの種別を切り替えることはできません。

変更不可なプロパティに変更を加えようとすると、新旧の値が同じでない限り TypeError が投げられます(可能な場合の valuewritable の変更は除きます)。

Writable 属性

writable プロパティ属性が false に設定されているとき、そのプロパティは書換不可になります。代入が出来なくなります。

var o = {}; // 新しいオブジェクトの生成

Object.defineProperty(o, 'a', {
  value: 37,
  writable: false
});

console.log(o.a); // 37 がログ出力されます
o.a = 25; // エラーは発生しません
// (strict モードでは発生します。同じ値を代入したとしても。)
console.log(o.a); // 37 がログ出力されます。代入文は動作しません。

// strict mode
(function() {
  'use strict';
  var o = {};
  Object.defineProperty(o, 'b', {
    value: 2,
    writable: false
  });
  o.b = 3; // TypeError がスローされます: "b" is read-only
  return o.b; // 上の行は動作せず 2 が返ります(訳注:正しくは「ここに制御は来ません」)
}());

例で見たように、書き込み不可のプロパティに書き込もうとしても変更されず、またエラーは発生しません。

Enumerable 属性

enumerable プロパティ属性は、プロパティが for...in ループや Object.keys() に現れるか否かを定義します。

var o = {};
Object.defineProperty(o, 'a', {
  value: 1,
  enumerable: true
});
Object.defineProperty(o, 'b', {
  value: 2,
  enumerable: false
});
Object.defineProperty(o, 'c', {
  value: 3
}); // enumerable の既定値は false
o.d = 4; // このようにプロパティを生成するとき、
         // enumerable の既定値は true
Object.defineProperty(o, Symbol.for('e'), {
  value: 5,
  enumerable: true
});
Object.defineProperty(o, Symbol.for('f'), {
  value: 6,
  enumerable: false
});

for (var i in o) {
  console.log(i);
}
// 'a' と 'd' がログされます(順不同)

Object.keys(o); // ['a', 'd']

o.propertyIsEnumerable('a'); // true
o.propertyIsEnumerable('b'); // false
o.propertyIsEnumerable('c'); // false
o.propertyIsEnumerable('d'); // true
o.propertyIsEnumerable(Symbol.for('e')); // true
o.propertyIsEnumerable(Symbol.for('f')); // false

var p = { ...o }
p.a // 1
p.b // undefined
p.c // undefined
p.d // 4
p[Symbol.for('e')] // 5
p[Symbol.for('f')] // undefined

Configurable 属性

configurable 属性は、プロパティをオブジェクトから削除できるかとプロパティの属性 (valuewritable 以外) を変更できるかを同時に制御します。

var o = {};
Object.defineProperty(o, 'a', {
  get() { return 1; },
  configurable: false
});

Object.defineProperty(o, 'a', {
  configurable: true
}); // TypeError が発生
Object.defineProperty(o, 'a', {
  enumerable: true
}); // TypeError が発生
Object.defineProperty(o, 'a', {
  set() {}
}); // TypeError が発生 (set は未定義であった)
Object.defineProperty(o, 'a', {
  get() { return 1; }
}); // TypeError が発生 (新たな get は全く同じであるにもかかわらず)
Object.defineProperty(o, 'a', {
  value: 12
}); // TypeError が発生 ('configurable' が false でも 'value' は変更できますが、ここでは 'get' アクセサーがあるため変更できません)

console.log(o.a); // logs 1
delete o.a; // 何も起きません
console.log(o.a); // logs 1

o.aconfigurable 属性が true である場合、エラーが発生することなく最終的にプロパティが削除されます。

プロパティおよび既定値の追加

属性の既定値がどう適用されるかを考えることは重要です。値の割り当てにドット表記を用いた場合と Object.defineProperty() を用いた場合とでは、以下の例で示したとおりに違いがあります。

var o = {};

o.a = 1;
// これは以下と同じです:
Object.defineProperty(o, 'a', {
  value: 1,
  writable: true,
  configurable: true,
  enumerable: true
});

// その一方で、
Object.defineProperty(o, 'a', { value: 1 });
// これは以下と同じです:
Object.defineProperty(o, 'a', {
  value: 1,
  writable: false,
  configurable: false,
  enumerable: false
});

独自のゲッターおよびセッター

例として自律的に記録を行うオブジェクトを作成してみます。temperature プロパティに値が代入されると、配列 archive に要素が一つ追加されます。

function Archiver() {
  var temperature = null;
  var archive = [];

  Object.defineProperty(this, 'temperature', {
    get() {
      console.log('get!');
      return temperature;
    },
    set(value) {
      temperature = value;
      archive.push({ val: temperature });
    }
  });

  this.getArchive = function() { return archive; };
}

var arc = new Archiver();
arc.temperature; // 'get!'
arc.temperature = 11;
arc.temperature = 13;
arc.getArchive(); // [{ val: 11 }, { val: 13 }]

次の例では、ゲッターが常に同じ値を返すようにしています。

var pattern = {
    get() {
        return 'I always return this string, ' +
               'whatever you have assigned';
    },
    set() {
        this.myname = 'this is my name string';
    }
};

function TestDefineSetAndGet() {
    Object.defineProperty(this, 'myproperty', pattern);
}

var instance = new TestDefineSetAndGet();
instance.myproperty = 'test';
console.log(instance.myproperty);
// I always return this string, whatever you have assigned

console.log(instance.myname); // this is my name string

プロパティの継承

アクセサープロパティを継承されると、その派生クラスでもプロパティがアクセスされたり書き換えられるときに getset が呼ばれます。これらのメソッドが値を保持するために変数を使っていると、すべてのオブジェクトがその値を共有することになります。

function myclass() {
}

var value;
Object.defineProperty(myclass.prototype, "x", {
  get() {
    return value;
  },
  set(x) {
    value = x;
  }
});

var a = new myclass();
var b = new myclass();
a.x = 1;
console.log(b.x); // 1

この問題を回避する方法は値を別のプロパティで保持することです。getset メソッド内で this はアクセス/書き換えされようとしているプロパティを納めるオブジェクトを指しています。

function myclass() {
}

Object.defineProperty(myclass.prototype, "x", {
  get() {
    return this.stored_x;
  },
  set(x) {
    this.stored_x = x;
  }
});

var a = new myclass();
var b = new myclass();
a.x = 1;
console.log(b.x); // undefined

アクセサープロパティとは違い、データプロパティは常にオブジェクト自身に格納されるのであって、prototype に格納されるわけではありません。しかし、書き込み不可能なデータプロパティを継承している場合、継承先オブジェクトでも書き換えは阻止されます。

function myclass() {
}

myclass.prototype.x = 1;
Object.defineProperty(myclass.prototype, "y", {
  writable: false,
  value: 1
});

var a = new myclass();
a.x = 2;
console.log(a.x); // 2
console.log(myclass.prototype.x); // 1
a.y = 2; // 無視されます。strict モードではエラースローされます。
console.log(a.y); // 1
console.log(myclass.prototype.y); // 1

仕様書

仕様書
ECMAScript (ECMA-262)
Object.defineProperty の定義

ブラウザーの互換性

BCD tables only load in the browser

互換性のメモ

Array オブジェクトの length プロパティの再定義

配列の length プロパティを再定義することは、通常の再定義の制限に照らせば可能です。(length プロパティは初期状態で構成不可、列挙不可、書き込み可能です。つまり、変更されていない配列では、length プロパティの値を変更したり書き込み不可にしたりすることが可能です。列挙可否や構成可否、また書き込み不可に変更した後は値や書き込み可否も、変更することはできません。) しかし、すべてのブラウザーがこの再定義を許可しているとは限りません。

Firefox 4 から 22 までの間では、配列の length プロパティを再定義しようとすると、無条件に (許可の有無にかかわらず) TypeError が発生します。

Object.defineProperty() を実装している Chrome のバージョンでは、状況によっては配列の現在の length プロパティとは異なる length の値を無視することがあります。状況によっては書き込み可否が暗黙に動作しない (そして例外を発生させない) こともあります。また、関連して、Array.prototype.push のような配列を変更する一部のメソッドが、書き込み不可であることを尊重しないことがあります。

Object.defineProperty() を実装する Safari のバージョンでは配列の現在の length プロパティと異なる値の length を無視し、また書き込み許可を変更する試みはエラーなしに実行されますが、実際はプロパティの書き込み許可が変更されません。

Internet Explorer 9 以降と Firefox 23 以降のみが、完全かつ正確に配列の length プロパティの再定義を実装しているようです。現時点では、配列の length プロパティの再定義はどのブラウザーでも動作する、あるいは特定のルールに則って動作するとは考えないようにしてください。そして、もしこれが実行できたとしても、これを行う本当に良い理由はありません

Internet Explorer 8 の特記事項

Internet Explorer 8 は Object.defineProperty() メソッドを DOM オブジェクトでのみ使用できるものとして実装しました。以下 2 点に注意が必要です:

  • ネイティブオブジェクトに対して Object.defineProperty() を用いようとするとエラーが発生します。
  • プロパティの属性には特定の値を設定しなければなりません。configurable, enumerable, writable の各属性に対して、データ記述子ではすべて true に、アクセサー記述子では configurabletrueenumerablefalse にです。(?)ほかの値(?)を与えようとすると、エラーが発生します。
  • プロパティの再設定には、始めにプロパティの削除が必要です。プロパティが削除されていない場合、再設定を試みる前の状態のままになります。

Chrome 37 以下の特記事項

Chrome 37 以下には、writable: false 指定を行なった "prototype" プロパティを関数に定義する場合に、想定通りに動かない バグ があります。

関連情報