Object.defineProperty()
Object.defineProperty()
は静的メソッドで、あるオブジェクトに新しいプロパティを直接定義したり、オブジェクトの既存のプロパティを変更したりして、そのオブジェクトを返します。
試してみましょう
構文
Object.defineProperty(obj, prop, descriptor)
引数
obj
-
プロパティを定義するオブジェクトです。
prop
-
文字列または
Symbol
で、定義または変更するプロパティのキーを指定します。 descriptor
-
定義または変更するプロパティの記述子です。
返値
指定したプロパティが追加または変更された、関数に渡されたオブジェクト。
解説
Object.defineProperty()
で、あるオブジェクトのプロパティを明示的に追加または変更することができます。代入による通常のプロパティ追加では、プロパティ列挙 (for...in
ループや Object.keys()
メソッドなど) に現れ、値は変更可能で、また削除も可能なプロパティが生成されます。このメソッドでは、これらの詳細事項を既定値から変えることが可能です。既定では、 Object.defineProperty()
を使って追加されたプロパティは書き込み不可、列挙不可、構成不可になります。加えて、 Object.defineProperty()
は内部メソッドの [[DefineOwnProperty]]
を [[Set]]
の代わりに使用しますので、プロパティが既に存在する場合でもセッターを呼び出しません。
プロパティの記述子は、データ記述子とアクセサー記述子の二つに分かれます。データ記述子は値を持つプロパティで、その値は書き換え可能にも不可能にもできます。アクセサー記述子は、関数のゲッターとセッターの組で表されるプロパティです。記述子はこれら二種類のどちらかでなければならず、両方になることはできません。
どちらの形でも記述子はオブジェクトで表現します。共通して以下のオプションのキーを持つことができます (注: ここでいう既定値とは、Object.defineProperty()
を使ってプロパティを定義する場合です)。
configurable
-
これが
false
に設定されていた場合、- このプロパティの種類をデータプロパティとアクセサープロパティの間で変更することができません。
- このプロパティを削除することができません。
- 記述子の他の属性は変更できません(しかし、
writable: true
のデータ記述子であれば、value
を変更し、writable
をfalse
に変更することができます)。
既定値は
false
です。 enumerable
-
true
である場合のみ、このプロパティは対応するオブジェクトでのプロパティ列挙に現れます。既定値はfalse
です。
データ記述子は以下のオプションキーも持ちます。
value
-
プロパティに関連づけられた値です。有効な JavaScript の値(数値、オブジェクト、関数など)である必要があります。既定値は
undefined
です。 writable
-
true
である場合のみ、プロパティに関連づけられた値は代入演算子で変更することができます。
既定値はfalse
です。
アクセサー記述子の場合はオプションとして次のキーも持つことができます。
get
-
プロパティのゲッターとなる関数で、ゲッターを設けない場合は
undefined
です。プロパティにアクセスするとこの関数が引数なしでコールされます。この関数内でthis
はアクセスしようとしたプロパティを持つオブジェクトになります (プロパティを定義するために作成した記述子オブジェクトではありません)。返値はこのプロパティの値として使われます。 既定値はundefined
です。 set
-
プロパティのセッターとなる関数で、セッターがない場合は
undefined
です。プロパティに値が割り当てられたとき、その値を引数としてこの関数がコールされます。この関数内でthis
は割り当てようとしたプロパティを持つオブジェクトになります。 既定値はundefined
です。
記述子に value
, writable
, get
, set
のいずれのキーもない場合、データ記述子として扱われます。記述子に value
または writable
と、get
または set
のキーの両方がある場合は、例外が発生します。
これらのキーは必ずしも記述子が直接所有しているとは限らないことに留意してください。継承されたプロパティも同様です。これらの既定を確実に保持するためには、記述子オブジェクトのプロトタイプチェーンにある既存のオブジェクトを前もって凍結するか、すべてのオプションを明示的に指定するか、 null
プロトタイプオブジェクトを作成します。
const obj = {};
// 1. null プロトタイプの使用: 継承しているプロパティなし
const descriptor = Object.create(null);
descriptor.value = "static";
// 既定で継承不可、変更不可、書換不可
Object.defineProperty(obj, "key", descriptor);
// 明示的な指定
Object.defineProperty(obj, "key2", {
enumerable: false,
configurable: false,
writable: false,
value: "static",
});
// 同じオブジェクトを再利用
function withValue(value) {
const d =
withValue.d ||
(withValue.d = {
enumerable: false,
writable: false,
configurable: false,
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()
は記述子の値とプロパティの現在の構成に従ってプロパティを変更しようとします。
古い記述子の configurable
属性が false
に設定されていた場合、そのプロパティは 構成不可 と言います。設定不可能なアクセサープロパティの属性を変更することはできませんし、データプロパティとアクセサープロパティの種類を切り替えることもできません。 writable: true
のデータプロパティでは、値を変更して writable
属性を true
から false
に変更することが可能です。データプロパティで元の値と同じ値を定義する場合を除き、設定不可能なプロパティ属性(許可されている場合は value
と writable
を除く)を変更しようとすると TypeError
が発生します。
現在のプロパティが構成可能な場合、属性を undefined
に定義すると、その属性は効果的に削除されます。例えば、 o.k
がアクセサープロパティである場合、 Object.defineProperty(o, "k", { set: undefined })
とするとセッターを除去し、k
はゲッターのみを持つことになるので、、読み取り専用になります。新しい記述子に属性がない場合、古い記述子の属性値は保持されます(暗黙的に undefined
に再定義されることはありません)。異なる「風味」の記述子を与えることで、データとアクセサープロパティを切り替えることが可能です。例えば、新しい記述子が(value
または writable
を持つ)データ記述子の場合、元の記述子の get
属性と set
属性は両方とも削除されます。
例
プロパティの作成
オブジェクトに指定されたプロパティが存在しないとき、Object.defineProperty()
は指定された形で新たなプロパティを生成します。記述子のキーは省略することができ、そのようなキーには既定値が適用されます。
const o = {}; // 新しいオブジェクトの生成
// データ記述子により、defineProperty を用いて
// オブジェクトプロパティを追加する例
Object.defineProperty(o, "a", {
value: 37,
writable: true,
enumerable: true,
configurable: true,
});
// o オブジェクトに 'a' プロパティが存在するようになり、その値は 37 となります
// アクセサー記述子により、defineProperty を用いて
// オブジェクトプロパティを追加する例
let bValue = 38;
Object.defineProperty(o, "b", {
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 はアクセサー記述子にのみ現れます。
プロパティの変更
既存のプロパティを変更するとき、操作が成功するか、何もしないか、TypeError
が発生するかは、現在のプロパティ構成によって決まります。
writable 属性
writable
プロパティ属性が false
の場合、そのプロパティは「書き込み不可」になります。代入ができなくなります。書き込み不可能なプロパティに書き込もうとすると、変更されず、厳格モードではエラーになります。
const o = {}; // 新しいオブジェクトの生成
Object.defineProperty(o, "a", {
value: 37,
writable: false,
});
console.log(o.a); // 37 がログ出力されます
o.a = 25; // エラーは発生しません
// (厳格モードでは、同じ値を代入したとしても、エラーが発生します。)
console.log(o.a); // 37 がログ出力されます。代入文は動作しません。
// 厳格モード
(() => {
"use strict";
const o = {};
Object.defineProperty(o, "b", {
value: 2,
writable: false,
});
o.b = 3; // TypeError が発生: "b" is read-only
return o.b; // 上の行は動作せず 2 が返ります(訳注:正しくは「ここに制御は来ません」)
})();
enumerable 属性
enumerable
プロパティ属性は、そのプロパティが Object.assign()
や スプレッド演算子で認識されるかどうかを定義します。 Symbol
以外のプロパティでは、 for...in
ループや Object.keys()
に現れるかどうかも定義します。詳細情報については、プロパティの列挙可能性と所有権を参照してください。
const 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 (const 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
const 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
属性は、プロパティをオブジェクトから削除できるかとプロパティの属性 (value
と writable
以外) を変更できるかを制御します。
この例は、構成可能でないアクセサープロパティを示しています。
const 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.a
の configurable
属性が true
である場合、エラーが発生することなく最終的にプロパティが削除されます。
この例は、構成可能ではないが書き込み可能なデータプロパティを示しています。プロパティの value
は変更可能で、 writable
は true
から false
に切り替えることができます。
const o = {};
Object.defineProperty(o, "b", {
writable: true,
configurable: false,
});
console.log(o.b); // undefined
Object.defineProperty(o, "b", {
value: 1,
}); // 構成可能な値が false の場合でも、オブジェクトは書き込み可能なので、値を置き換えることができる
console.log(o.b); // 1
o.b = 2; // 割り当てる演算子を使って値を変更することもできる
console.log(o.b); // 2
// プロパティの書き込み可能属性を切り替える
Object.defineProperty(o, "b", {
writable: false,
});
Object.defineProperty(o, "b", {
value: 1,
}); // TypeError: プロパティは書き込みも構成も可能でないため、変更することができない
// この時点で、 'b' をさらに変更したり、書き込み可能な状態に
// 戻したりする方法はありません
この例は構成可能な、しかし書き込み不可能なデータプロパティを示しています。プロパティの value
は defineProperty
で置き換えることができ(代入演算子ではなく)、writable
は切り替えることができます。
const o = {};
Object.defineProperty(o, "b", {
writable: false,
configurable: true,
});
Object.defineProperty(o, "b", {
value: 1,
}); // defineProperty で値を置き換えることができる
console.log(o.b); // 1
o.b = 2; // 厳格モードでは TypeError が発生: 書き込み不可能なプロパティの値を割り当てることで変更することはできる
この例は構成可能でなく、書き込み不可のデータプロパティを示しています。プロパティの値
を含め、プロパティの属性を更新する方法はありません。
const o = {};
Object.defineProperty(o, "b", {
writable: false,
configurable: false,
});
Object.defineProperty(o, "b", {
value: 1,
}); // TypeError: プロパティは書き込みも構成も可能ではないため、変更することはできない
プロパティおよび既定値の追加
属性の既定値がどう適用されるかを考えることは重要です。値の割り当てにプロパティアクセサーを用いた場合と Object.defineProperty()
を用いた場合とでは、以下の例で示したとおりに違いがあります。
const 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() {
let temperature = null;
const archive = [];
Object.defineProperty(this, "temperature", {
get() {
console.log("get!");
return temperature;
},
set(value) {
temperature = value;
archive.push({ val: temperature });
},
});
this.getArchive = () => archive;
}
const arc = new Archiver();
arc.temperature; // 'get!'
arc.temperature = 11;
arc.temperature = 13;
arc.getArchive(); // [{ val: 11 }, { val: 13 }]
次の例では、ゲッターが常に同じ値を返すようにしています。
const 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);
}
const 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
プロパティの継承
アクセサープロパティを継承されると、その派生クラスでもプロパティがアクセスされたり書き換えられるときに get
と set
が呼ばれます。これらのメソッドが値を保持するために変数を使っていると、すべてのオブジェクトがその値を共有することになります。
function MyClass() {}
let value;
Object.defineProperty(MyClass.prototype, "x", {
get() {
return value;
},
set(x) {
value = x;
},
});
const a = new MyClass();
const b = new MyClass();
a.x = 1;
console.log(b.x); // 1
この問題を回避する方法は値を別のプロパティで保持することです。get
と set
メソッド内で this
はアクセス/変更されようとしているプロパティを納めるオブジェクトを指しています。
function MyClass() {}
Object.defineProperty(MyClass.prototype, "x", {
get() {
return this.storedX;
},
set(x) {
this.storedX = x;
},
});
const a = new MyClass();
const b = new MyClass();
a.x = 1;
console.log(b.x); // undefined
アクセサープロパティとは違い、データプロパティは常にオブジェクト自身に格納されるのであって、プロトタイプに格納されるわけではありません。しかし、書き込み不可能なデータプロパティを継承している場合、継承先オブジェクトでも書き換えは阻止されます。
function MyClass() {}
MyClass.prototype.x = 1;
Object.defineProperty(MyClass.prototype, "y", {
writable: false,
value: 1,
});
const a = new MyClass();
a.x = 2;
console.log(a.x); // 2
console.log(MyClass.prototype.x); // 1
a.y = 2; // 無視されます。厳格モードではエラーが発生します
console.log(a.y); // 1
console.log(MyClass.prototype.y); // 1
仕様書
Specification |
---|
ECMAScript Language Specification # sec-object.defineproperty |
ブラウザーの互換性
BCD tables only load in the browser