概要
プロトタイプチェーンはプロパティの読み込みだけじゃなくて代入するときも影響するという話。値を代入しようとしたときには以下の内部メソッドたちが呼ばれる、これは意外と複雑。
- [[DefineOwnProperty]]
- [[GetOwnProperty]]
- [[GetProperty]]
- [[CanPut]]
- [[Put]]
この記事では、これらの内部メソッドの詳細と、プロパティに値を代入するときに起こりえる挙動について説明する。
内部メソッドたちの挙動
仕様上での挙動とJavaScriptでのサンプルコードで解説(一部省略)。
[[DefineOwnProperty]](P, Desc, Throw)
オブジェクト O 自身のプロパティ名 P にプロパティディスクリプタ Desc を指定して定義する。 Throw はフラグで、strict modeのとき true となる。Object.defineProperty
と同じようなもの。
[[GetOwnProperty]](P)
オブジェクト O 自身のプロパティ名 P であるプロパティのプロパティディスクリプタを返す。Object.getOwnPropertyDescriptor
と同じようなもの。返り値は内部的に使われる。
[[GetProperty]](P)
オブジェクト O のプロパティ名 P である(プロトタイプチェーンから最初に見つかった)プロパティのプロパティディスクリプタを返す。返り値は内部的に使われる。
- prop を プロパティ名を P として O の[[GetOwnPropery]]内部メソッドを呼んだときの返り値とする
- prop が undefined でない場合、 prop を返す
- proto を O の[[Prototype]]内部プロパティ(プロトタイプのこと)とする
- proto が null の場合、 undefined を返す
- プロパティ名を P として proto の[[GetProperty]]内部メソッドを呼んだときに返り値を返す
function getProperty(O, P) {
var prop, proto;
prop = Object.getOwnPropertyDescriptor(O, P);
if (prop !== undefined) return prop;
proto = Object.getPrototypeOf(O);
if (proto === null) return;
return getProperty(proto, P);
}
[[CanPut]](P)
オブジェクト O のプロパティ名 P のプロパティに値を代入できるかどうかを真偽値で返す。返り値は内部的に使われる。
- desc をプロパティ名を P として O の[[GetOwnPropery]]内部メソッドを呼んだときの返り値とする
-
desc が undefined でない場合、
-
desc がアクセサディスクリプタな場合、
- desc.[[Set]]が undefined な場合、 false を返す
- 違う場合、 true を返す
- 違う場合は必ずデータディスクリプタになるので、 desc.[[Writable]]を返す
-
desc がアクセサディスクリプタな場合、
- proto を O の[[Prototype]]内部プロパティとする
- proto が null な場合、 O の[[Extensible]]内部プロパティの値を返す
- inherited をプロパティ名を P として proto の[[GetProperty]]内部メソッドを呼んだときに返り値とする
- inherited が undefined な場合、O の[[Extensible]]内部プロパティの値を返す
-
inherited がアクセサディスクリプタな場合、
- inherited.[[Set]] が undefined な場合、 false を返す
- 違う場合、 true を返す
-
inherited はデータディスクリプタになり、
- O の内部プロパティ[[Extensible]]内部プロパティが falseの場合、 false を返す
- 違う場合、 inherited.[[Writable]]を返す
Note
- アクセサディスクリプタとはアクセサプロパティのプロパティディスクリプタのこと
- データディスクリプタとはデータプロパティのプロパティディスクリプタのこと
-
hoge.[[Set]]、 hoge.[[Writable]]等は実際のコード(
Object.getOwnPropertyDescriptor
で取得できるオブジェクト)ではhoge.set
、hoge.writable
と読み替える -
Oの[[Extensible]]内部プロパティは
Object.isExtensible(O)
で得られる
function isAccessorDescriptor(desc) {
var hasOwn = Object.prototype.hasOwnProperty;
return hasOwn.call(desc, 'get') && hasOwn.call(desc, 'set');
}
function isDataDescriptor(desc) {
var hasOwn = Object.prototype.hasOwnProperty;
return hasOwn.call(desc, 'value') && hasOwn.call(desc, 'writable');
}
function canPut(O, P) {
var desc, proto, inherited;
desc = getOwnProperty(O, P);
if (desc !== undefined) {
if (isAccessorProperty(desc)) return desc.set !== undefined;
return desc.writable;
}
proto = Object.getPrototypeOf(O);
if (proto === null) return Object.isExtensible(O);
inherited = getProperty(proto);
if (inherited === undefined) return Object.isExtendible(O);
if (isAccessorProperty(inherited)) return inherited.set !== undefined;
if (Object.isExtensible(O)) return false;
return inherited.writable;
}
[[Put]](P, V, Throw)
オブジェクト O のプロパティ名 P のプロパティに値 V を代入するときに呼ばれる。 Throw はフラグでstrict modeのときに true となる。
- プロパティ名を P として O の[[CanPut]]内部メソッドを呼んだときの返り値が false の場合、
- Throw が true な場合、 TypeError 例外を投げる
- 違う場合、終了
- ownDesc をプロパティ名を P として O の[[GetOwnPropery]]内部メソッドを呼んだときの返り値とする
-
ownDesc がデータプロパティの場合、
- valueDesc をプロパティディスクリプタ {[[Value]]: V}とする
- 引数を P 、 valueDesc 、 Throw として O の[[DefineOwnProperty]]を呼ぶ([[Value]]だけ上書きする)
- 終了
- desc をプロパティ名を P として proto の[[GetProperty]]内部メソッドを呼んだときに返り値とする
-
desc がアクセサディスクリプタな場合、
- setter を desc.[[Set]]とする(undefined の場合は終了?)
- this を O 引数を V として setter の [[Call]]内部プロパティを呼ぶ
- 違う場合、
- newDesc をプロパティディスクリプタ{[[Value]]: V, [[Writable]]: true, [[Enumerable]]: true, [[Configurable]]: true}とする
- 引数を P 、 newDesc 、 Throw として O の[[DefineOwnProperty]]を呼ぶ
- 終了
Note
[[Call]]内部プロパティはFunction.prototype.call
みたいなもの(厳密には違う)
// strict modeは割愛
function put(O, P, V) {
if (!canPut(O, P)) return;
var ownDesc, valueDesc, desc, setter, newDesc;
ownDesc = Object.getOwnPropertyDescriptor(O, P);
if (isDataDescriptor(ownDesc)) {
valueDesc = { value: V };
Object.defineProperty(O, P, valueDesc);
return;
}
desc = getProperty(O, P);
if (isAccessorDescriptor(desc)) {
setter = desc.set;
if (settter === undefined) return;
setter.call(O, V);
} else {
newDesc = {
value: V,
writable: true,
enumerable: true,
configurable: true
};
Object.defineProperty(O, P, newDesc);
}
}
オブジェクト自身にプロパティがないときに新しいプロパティが作られない状況
オブジェクトにプロパティを代入しようとしたとき(foo.bar = baz
)に新しくプロパティが定義されない状況は大きく分けて3パターンある
Object.isExtensible(foo)がfalse
これは単純に新しくプロパティを追加できないという状態。でも、こういう状況はあまりない気がする。
プロトタイプチェーン上から得たプロパティがデータプロパティで書き込み不可能
プロトタイプチェーン上のプロパティであっても[[Writable]]が false であれば新しくプロパティは作られない。
var proto = Object.create(null, {
bar: { value: 1, writable: false }
});
var foo = Object.create(proto);
foo.bar = 10;
console.log(Object.prototype.hasOwnProperty.call(foo, 'bar')); // false
console.log(foo.bar); // 1
プロトタイプチェーン上から得られたプロパティがアクセサプロパティ
プロトタイプチェーン上から得られたプロパティがアクセサプロパティな場合、セッターがある場合はセッターが呼ばれ、ない場合は何もしない。
var proto = {
_bar: 1,
set bar(x) { this._bar = x }
}
var foo = Object.create(proto);
foo.bar = 10;
console.log(Object.prototype.hasOwnProperty.call(foo, 'bar')); // false
console.log(Object.prototype.hasOwnProperty.call(foo, '_bar')); // true
console.log(foo._bar); // 10
プロトタイプにアクセサプロパティを定義するのはクラス風の書き方ではけっこう使えそう。
function BitReader(bytes) {
this.bytes = bytes;
this.bitOffset = 0;
}
Object.defineProperty(BitReader.prototype, 'byteOffset', {
get: function() { return this.bitOffset >>> 3; },
set: function(byteOffset) { this.bitOffset = byteOffset << 3; }
});
var bitReader = new BitReader(new Uint8Array(1024));
bitReader.byteOffset = 512;
console.log(bitReader.bitOffset); // 4096
ちなみに、TypeScriptではこのテクニックが使われている。サンプルコード
それでも新しくプロパティを定義したいは
Object.defineProperty
を使おう。終わり。