ドワンゴ Advent Calendar の 5 日目の記事です。ドワンゴといえばカドカワ。カドカワといえば角川ゲームスの RPG ツクール MV。 RPG ツクール MV といえば JavaScript なので、今回は JavaScript に関する記事です。
RPG ツクール MV をいじって遊んでいたら、 prototype について案外勘違いしてしまっていたところがあったので、復習を兼ねて JavaScript の prototype についてメモします。ついでに RPG ツクール MV で使用されている継承イディオムについてもメモしています。なお、 ES6 以降では無論こういったイディオムを使わずに class
を使うべきですが、その場合でも裏方の仕組みである prototype の仕組みについて知っておいて損はないでしょう。
__proto__
と prototype
- オブジェクトには
[[Prototype]]
というプロトタイプチェーンに使われる内部スロットがある (9.1)。- プロトタイプチェーンは、オブジェクトのプロパティを探索する仕組みである (本稿では説明を割愛する)。
-
__proto__
は内部メソッド[[GetPrototypeOf]]
および[[SetPrototypeOf]]
を通じて[[Prototype]]
を暴露するプロパティである (9.1, B.2.2.1)。 -
__proto__
は ES6 で標準化されたものの、表立って使用することは望ましくない。__proto__
に何かを突っ込みたい場合は、以下に説明するnew
演算子やObject.create
を使用することが望ましい。 -
new
演算子のオブジェクト生成時に__proto__
にセットされる値には、コンストラクタ関数のprototype
が使用される (6.1.7.2, 12.3.3)。
new
演算子
function Foo() {}
var foo = new Foo();
は基本的に以下と等価。
function Foo() {}
var foo = {};
foo.__proto__ = Foo.prototype; // プロトタイプオブジェクトの設定
Foo.apply(foo); // foo を this として Foo 関数を呼ぶ
constructor
プロパティ
function Foo() {}
var foo = new Foo();
// foo.constructor という呼び出しは foo.__proto__.constructor の呼び出しになる。
// foo 自身は constructor というプロパティを持っていない。
console.log(foo.hasOwnProperty('constructor')); // false
console.log(foo.__proto__.hasOwnProperty('constructor')); // true
console.log(Foo.prototype.hasOwnProperty('constructor')); // true
// 以下はすべて等価
console.log(foo.constructor === Foo) // true
console.log(foo.__proto__.constructor === Foo) // true
console.log(Foo.prototype.constructor === Foo) // true
Object.create
関数
Object.create
は、引数をプロトタイプ (__proto__
) に持つオブジェクトを生成する (4.3.5, 19.1.2.2)。
function Foo {};
var foo = Object.create(Foo.prototype);
は基本的に以下と等価。
function Foo {};
var foo = {};
foo.__proto__ = Foo.prototype;
誤解を恐れずに言うと、「コンストラクタを実行しない new
」である。
継承
古いやり方
function Foo() {}
function Bar() {}
Bar.prototype = new Foo();
一見よさそうに見えるが、これには問題がある。 Bar
のインスタンスごとに Foo
のインスタンス (Foo.prototype
を __proto__
に持つオブジェクト) がほしいが、この継承の時点ではコンストラクタ Foo
を実行してほしくない。そのために Object.create
を使う。
Object.create
を使うやり方
function Foo() {}
function Bar() {}
Bar.prototype = Object.create(Foo.prototype);
// この時点だと Bar.prototype.constuctor は Foo なので、 Bar に直す必要がある。
Bar.prototype.constructor = Bar;
サイの表紙で有名な「サイ本」やいま巷で話題の RPG ツクール MV はこの方式。 Bar
を以下のようにすることで親クラスコンストラクタを呼ぶことができる。
function Bar() {
Foo.apply(this, arguments);
}