ES6からclass
構文が投入されて、普通のオブジェクト指向言語っぽく書けるようになったJavaScriptだが、クラスの継承の面では他の言語と結構毛色が違う。
JavaScriptの継承の基本はプロトタイプ継承で、class
構文はこれをうまいこと隠してくれるが、見えていた方が幸せなんじゃないかと思うこともある。
なお、今日日のclass
構文はシンタックシュガーの域を超えていて、class
構文じゃないとできないこともある。
いつしかあきらめてclass
構文を使わなきゃいけない日が来るんだろうなぁ。
JavaScriptのクラスの基本は、クラス名を冠したコンストラクタ関数を作り、そのprototype
プロパティにインスタンスメソッドを詰め込む、である。
インスタンスプロパティはすべてコンストラクタの中で初期化する。
これは慣れるまでは「すごく変な」感じがする。
まず、関数にプロパティがある。
C()
という関数を作ると、C.x
みたいなプロパティを作って代入できてしまう。
もちろん、関数としてのC
の定義は残ったままである。
function C() { } C.x = 1; C(); // I can call it!クラスの静的プロパティ・静的メソッドはすべてこの形で定義する。
逆にインスタンスにくっつく方のプロパティはすべてコンストラクタの中で定義する。
コンストラクタの中で定義するので、この時に必ず初期値を設定することになる。
メソッドはコンストラクタで同様に設定してもいいが、普通はprotytpoe
プロパティに設定する。
両者の違いは継承した時に出る。
前者は派生クラスから基底クラスの関数を呼び出せない。
function C () { this.x = 1; this.y = 2; this.foo = function () { console.log("this.foo"); } } C.prototype.bar = function () { console.log("this.bar"); }
prototype
プロパティの中身は、new
を実行したときにそのままそっくり新しく生成したオブジェクトのプロトタイプに設定される。
ややこしいが、前者は普通のプロパティでC.x
なんかと同じ扱いである。
後者は言語が定める特別な属性であり、オブジェクトならば必ず持っている。
前者はprototype
プロパティ、後者はオブジェクトのプロトタイプと呼んで区別することにする。
まとめると、「関数を定義するとprototype
プロパティが自動生成され、new
でオプジェクトを生成するとコンストラクタ関数のprototype
プロパティがオブジェクトのプロパティに自動的に設定される」ようになっている。
const c = new C(); c.foo(); // this.foo c.bar(); // this.bar console.log(Object.getPrototypeOf(c)); // {bar: f, ...
JavaScriptはオブジェクトに対して何かを要求すると、まず自身のプロパティ一覧から探す。
なければオブジェクトのプロトタイプから探す。
それでもない場合はどうするか。
オブジェクトのプロトタイプもオブジェクトの一種なので、プロトタイプオブジェクトにさらにプロトタイプをくっつけることができる。
そこで、さらにプロトタイプオブジェクトのプロトタイプを検索する。
これをプロトタイプオブジェクトのプロトタイプがnull
になるまで続ける。
通常、すべてのオブジェクトはObject
クラスまでプロトタイプをたどることができるが、Object
オブジェクトのプロトタイプがnull
になっており、ここで検索は終了する。
これをプロトタイプチェーンと呼び、JavaScriptではプロトタイプチェーンを使って継承を実現する。
正直、文字だけで説明するのはプロトタイプが氾濫してしまって無理がある。 「現代のJavaScriptチュートリアル」のプロトタイプ継承あたりとか、「JavaScript入門」のプロトタイプでオブジェクトの継承を実装するあたりを見ると図入りで分かりやすい。
オブジェクトのプロトタイプはObject.setPrototypeOf
静的メソッドを使って明示的に設定できる。
function D() { this.y = 3; this.z = 4; } Object.setPrototypeOf(D.prototype, C.prototype);
ここではD.prototype
には何も設定していないが、関数を定義した時点でprototype
プロパティは自動的に作成されるので、Object.setPrototypeOf
は何事もなかったように成功する。
const d = new D(); console.log(d); // D {y: 2, z: 3 } // d.foo(); → TypeError: d.foo is not a function d.bar(); // prototype.bar
ここでプロトタイプに設定した関数と、そうではない関数の差が出る。
bar
はC
のprototype
プロパティにあり、それをD
のproperty
プロパティのオブジェクトプロパティに設定している。
new
はD.prototype
を生成したオブジェクトのプロトタイプに設定するので、プロトタイプチェーンの中にC.prototype
が入っている。
このため、D
でbar
を定義していなくてもプロトタイプチェーン経由でC.bar
が見つかる。
一方、foo
はC
のプロパティとしては登録されているが、プロトタイプには登録されていないので、プロトタイプチェーン経由では見つからずエラーになる。
あらためて実行結果を見てみると、基底クラスC
のインスタンスプロパティx
が出力されていない。
これは基底クラスのコンストラクタC()
を呼んでいないからである。
明示的に呼び出す必要がある。
当然、派生クラスの初期化の前に呼び出す必要がある。
このとき単純にC()
と呼び出してもダメである。
this
が思った値にならない。
C()
はグローバル関数なのでそのまま呼び出すとthis
はグローバルなthisになる。
つまりWebブラウザの環境ならwindow
だし、厳格モードならば未定義である。
それに対し、D()
はnew
が呼び出しており、this
をきちんと設定してくれている。
一方、this.C()
と書いてもプロトタイプチェーンからC()
を見つけられないのでエラーになる。
this
からではなく直接C
を探しつつ、D
のthis
をC()
に渡す必要がある。
そのためにはcall
を使って以下のように書く。
function D() { C.call(this) this.y = 3; this.z = 4; } const d = new D(); console.log(d); // D {x: 1, y: 2, z: 3, foo: f} d.foo(); // this.foo d.bar(); // prototype.bar
これで基底クラスのインスタンスプロパティも初期化され、同時にfoo
もプロパティとして見えるようになっている。
まとめれば以下のようになる。
これをシンタックスシュガーとしてまとめたのがclass
キーワードによるクラス定義である。
C(...) { // constructor B.call(this, ...); // base class constructor, if you needed this.x = 10; // instnace property } Object.assign(C, { foo() { /* class (static) method */ }, a: 20 // class (static) property }); Object.assign(C.prototype, { bar() { /* instance method */ } }); // set base class prototype, if you needed Object.setPrototypeOf(C.prototype, B.prototype);
ここでObject.assign
はオプジェクトにプロパティを上書きしてくれるメソッドである。
もちろん、ちまちまと代入していっても、スプレッド構文を使ってもいい。
通常、JavaScriptでオブジェクトを生成する場合はnew
を使うが、もうひとつオブジェクトを作る方法がある。
それがObject.create
である。
この関数は第1引数に生成するオブジェクトのプロトタイプとなるオブジェクトを指定する。
第2引数もあるのだが、とりあえず使わないのでここでは解説しない。
Object.create
を使うと先ほどのクラスC
は以下のように書ける。
const C = { create() { const me = Object.create(C.prototype); me.x = 1; me.y = 2; me.foo = function () { console.log("C.foo"); }; return me; }, prototype: { bar() { console.log("C.bar"); } } };
プロトタイプはprototype
という名前である必要はなく、また、プロトタイプの内容を直接C
のメンバとしてC.bar
のように書いても動かせるが、コンストラクタ関数型クラスとの相互運用を考えるとprototype
に収めておいた方がよい。
継承にはやはりObject.setPrototypeOf
を使う。
ややこしいので実例を先に見た方がいいだろう。
const D = { create() { const me = Object.create(D.prototype); me.y = 3; me.z = 4; return me; }, prototype: { } } Object.setPrototypeOf(D.prototype, C.prototype);
D.prototpye
プロパティのオブジェクトプロトタイプに、C.prototype
そのものを設定している。
C.prototype
はたぶんプロトタイプの体を為したオブジェクトであろうから、これでプロトタイプチェーンがつながる。
ただし、まだ完全ではない。
基底クラスのコンストラクタを呼んでいない。
今、基底クラスの初期化はC.create
で行っているが、これをそのまま呼び出すともう一度オブジェクトが作られてしまうので、オブジェクトを作る部分と初期化する部分を分ける。
初期化する部分がコンストラクタに相当するが、規格とバッティングするのを避けてinit
という名前にしてみる。
const C = { create() { const me = Object.create(C.prototype); me.init(); return me; }, prototype: { init() { this.x = 1; this.y = 2; this.foo = function () { console.log("C.foo"); }; }, bar() { console.log("C.bar"); } } }; const D = { create() { const me = Object.create(D.prototype); this.init(); return me; }, prototype: { init() { super.init(); // means C.init.call(this); this.y = 3; this.z = 4; } } }; Object.setPrototypeOf(D.prototype, C.prototype);
コード中に書いたが、C.init.call(this)
とすることで、クラスC
の初期化関数を呼び出せるが、代わりにsuper
が使える。
ただし、super
が使えるのはこの書き方だけで、コンストラクタ呼び出しとしての使用はできず、また、以下の方法では使えない。
const D = { prototype: { // CANNOT do this. foo: function () { super.foo(); } } }; // CANNOT do this. D.prototype.bar = function () { super.bar(); }
さて、C
とD
のcreate
がほとんど同じである。
何度も書くのは面倒なのでどうにかしたい。
ざっくり、プロトタイプをもらって、上のC
やD
のようなオブジェクトを返す関数を作ればよさそうだ。
prototype
プロパティをつなげ、それを保持するprototype
プロパティ
init
を呼び出すcreate
関数
function buildClass(base, proto) { if (base) Object.setPrototypeOf(proto, base.prototype); return { prototype: proto, create(...args) { const me = Object.create(proto); me.init(...args); return me; } }; } const C = buildClass(null, { init() { this.x = 1; this.y = 2; this.foo = function () { console.log("C.foo"); }; }, bar() { console.log("C.bar"); } }); const D = buildClass(C, { init() { super.init(); this.y = 3; this.z = 4; } });
クラスのスタティックメンバは返ってきたC
やD
に直接書き足してしまえばよい。
ただ、このままだと親クラスのスタティックメンバは見えないので、クラスそれ自身のオブジェクトプロトタイプに、親クラスそのものを設定しておく。 これで親クラスのスタティックメンバがプロトタイプ経由で見えるようになる、とりあえず。09 Aug 2024
まとめるとこんな感じになる。
const C = buildClass(base, { init(...arg) { super.init(...); // base class constructor, if you needed // or if base is ordinal constructor function, do: // base.call(this, ...) this.x = 10; // instnace property }, bar() { } // instance method }); Object.assign(C, { foo() { }, // class (static) method a: 20 // class (static) property }); Object.setPrototypeOf(C, base);
これでだいたい使えるようになっているが、コンストラクタ関数型では関数のprototype
プロパティにconstructor
というプロパティがあり、自分自身を指している。
現状、コンストラクタ関数がないので、より厳密には定義してやる必要がある。
function buildClass(base, proto) { if (base) Object.setPrototypeOf(proto, base.prototype); proto.constructor = function (...args) { this.init(...args); }; proto.constructor.prototype = proto; return { prototype: proto, create(...args) { const me = Object.create(proto); me.init(...args); return me; } }; }
proto.constructor
はこの形で書く必要がある。
function
を使って関数を定義しないとprototype
プロパティが生成されず、コンストラクタとして使えない。
この関数のprototype.constructor
はデフォルトでこの関数自身となる。
このコンストラクタの呼び出しでインスタンスを生成できなければならないから、コンストラクタ関数のprototype
プロパティには、今作っているクラスのプロトタイプオブジェクトを設定しておけばよい。
ただ、コンストラクタを使わないでオブジェクトを作れるようにしたのに、今更コンストラクタなんて、という気もする。
new obj.constructor()
を使っているコードのことは忘れて、C.create(...args)
をプロトタイプに入れておけばそれでいい気もする。
function buildClass(base, proto) { if (base) Object.setPrototypeOf(proto, base.prototype); proto.create = function (...args) { const me = Object.create(proto); me.init(...args); return me; }; return { prototype: proto, create: proto.create }; }
もうひとつ問題があり、instanceof
演算子が動かない。
const d = D.create(); console.log(d instanceof D); // TypeError: Right-hand side of 'instanceof' is not callable
instanceof
の右辺は関数じゃないといけないらしい。
D.prototype.isPrototypeOf(d)
で代用できるのだが、やはり不便である。
これはSymbol.hasInstance
というクラスメソッドを定義することで解決できる。
ただ、この名前は普通の関数名ではないので、JSON読んでたらJavaScriptの識別子として使えないキーに遭遇しちゃった場合に使う、[ ]
記法を使う。
つまり、この場合はD[Symbol.hasInstance] = ...
という形で定義すればよい。
この関数の引数にはinstanceof
の左辺が入ってくるので、instanceof
の結果として返したい値を返せばよい。
判断にはObject.isPrototypeOf
が使える。
分かりやすく書けば以下のようになる。
なお、先ほどのprototype.create
は削除している。
function buildClass(base, proto) { if (base) Object.setPrototypeOf(proto, base.prototype); const cls = { prototype: proto }; cls.create = function (...args) { const me = Object.create(proto); me.init(...args); return me; }; cls[Symbol.hasInstance] = function (obj) { return proto.isPrototypeOf(obj); }; return cls; }
アロー関数等使ってえいやっとまとめてしまえば、以下のようになる。
function buildClass(base, proto) { if (base) Object.setPrototypeOf(proto, base.prototype); return { prototype: proto, create(...args) { const me = Object.create(proto); me.init(...args); return me; }, [Symbol.hasInstance]: obj => proto.isPrototypeOf(obj) }; }
このくらいがバランスがいいかな、と思う。
これで実は相互運用が可能になっている。
肝はObject.create
型のクラス定義にprototype
を持たせたことで、関数型も見た目がほぼ一緒になるため、基底クラス・派生クラスとも混ぜて使える。
function C() { } Object.assign(C.prototype, { }); const D = buildClass(C, { init() { } }); function F() { } Object.assign(F.prototype, { }); Object.setPrototypeOf(F.prototype, D.prototype); const f = new F(); console.log(f instanceof F); // true console.log(f instanceof C); // true console.log(f instanceof D); // true
組み込み型を基底クラスとすることもできるが、一部のクラスは特殊な動きをするので気をつける必要がある。
Array
はコンストラクタを呼び出すと必ずオブジェクトを作ってしまう。
const A = buildClass(Array, { init() { } }); A.create(...args) = { const me = Array(...args); Object.setPrototypeOf(me, A.prototype); me.init(); return me; };
仕方がないのでObject.create
の呼び出しはあきらめて、buildClass
が作ってくれたコンストラクタを置き換え、Array
オブジェクトを作ってからObject.setPrototypeOf
でプロトタイプを入れ替える。
buildClass
がArray
のプロトタイプをA.prototype
にぶら下げているはずなので、これでA
はArray
としても動く。
String
なんかも同類。
WebGLRenderingContext
・CanvasRenderingContext2D
などはcanvas.getContext
を呼び出すことで得られ、コンストラクタは使わない。
これらもArray
と同様に実装できる。
const G = buildClass(WebGLRenderingContext, { init() { } }); G.create(canvas, ...args) = { const me = canvas.getContext("webgl", ...args); Object.setPrototypeOf(me, G.prototype); me.init(...args); return me; };
オリジナルのオブジェクトを複製せず、そのままプロトタイプを入れ替えるパターンは意外と有用なことが多いので、buildClass
でクラスメソッドを定義してもいいだろう。関数名はtakeover
にしてある。09 Aug 2024
function buildClass(base, proto) { ... return { ... takeover(me, ...args) { Object.setPrototypeOf(me, proto).init(...args); return me; }, ... }; } G.create(canvas, ...args) = { return G.takeover(canvas.getContext("webgl", ...args));; };
コンストラクタ関数には普通にプロパティがあり、関数も設定できる。また、new
が生成した「生」のオブジェクトを受け取るが、このオブエクトのプロトタイプには既にコンストラクタ関数のprototype
プロパティが設定されている。
ということは、以下のような半ばデタラメのようなことができる。
function F(...args) { this.init(...args); } F.prototype = { init(...args) { ... } }; F.create = function (...args) { const me = Object.create(F.prototype); me.init(...args); return me; }
これでnew F()
でもF.create()
でも動いてしまう。
create
はもっと簡単に、
F.create = function (...args) { return new this(...args); }
でいい。
ここまでするなら普通にコンストラクタ関数型でいいじゃないか、という気もする。
create型のクラスの何がメリットだったかを考えると、buildClass
が全部やってくれることだった。
ならば、コンストラクタ関数型でも全部やってくれる関数を用意すればよいのではないか?
function functionClass(base, proto) { function F(...args) { this.init(...args); } if (base) { Object.setPrototypeOf(F, base); Object.setPrototypeOf(proto, base.prototype); } Object.assign(F, { prototype: proto, ...proto.staticMembers }); F.prototype.constructor = F; return F; } const C = functionClass(null, { ... });
これで一応動くのだが、コンストラクタ関数の内容が固定になってしまうのと、コンストラクタ関数の場合、関数名、つまりクラス名をname
プロパティで得られるが、関数名が必ずF
になってしまうので生成したクラスのクラス名がすべてF
になってしまう。
そこで、コンストラクタ関数だけは外で定義することにすると、
function functionClass(func, base, proto) { if (base) { Object.setPrototypeOf(func, base); Object.setPrototypeOf(proto, base.prototype); } Object.assign(func, { prototype: proto, ...proto.staticMembers }); func.prototype.constructor = func; } function C(...args) { this.init(...args); } functionClass(C, null, { ... });
こんな感じに使える。
create型と比べると、クラス作成が関数定義とクラス化の2段階になってしまうが、クラス名を得られるのがコンストラクタ型クラス宣言のメリットである。当然、instanceof
も小細工なしで普通に動く。
実はクラスの中から例外を投げるときにクラス名をメッセージに入れようと思ったのがこの記事を書くことになった発端。
create型だとbuildClass
はクラス名を決定する情報を一切受け取っていないので、そのままではどうやってもユーザーが決めた名前が分からない。
ユーザーが明示的にクラス名を渡す必要があるが、buildClass
の結果を保持する変数名と二重に書く必要があるし、どちらか一方だけを修正して他方の修正を忘れると、要らぬ混乱を招く元になる。
で、今のところの成果をまとめるとこうなる。
function funclass(func, base, proto) { func.prototype = proto; proto.constructor = func; proto.factory = { create(...args) { return new func(...args); }, takeover(me, ...args) { Object.setPrototypeOf(me, proto).init(...args); return me; }, ...proto.factory }; Object.assign(func, proto.factory); if (base) { Object.setPrototypeOf(func, base); Object.setPrototypeOf(proto, base.prototype); } } function buildClass(base, proto) { if (proto.constructor == null) { proto.constructor = function (...args) { this.init(...args); }; proto.constructor.prototype = proto; } proto.factory = { prototype: proto, create(...args) { const me = Object.create(proto); me.init(...args); return me; }, takeover(me, ...args) { Object.setPrototypeOf(me, proto).init(...args); return me; }, ...proto.factory }; Object.defineProperty(proto.factory, Symbol.hasInstance, { value: obj => proto.isPrototypeOf(obj) }); if (base) { Object.setPrototypeOf(proto.factory, base); Object.setPrototypeOf(proto, base.prototype); } return proto.factory; }
functionClass
は動詞・目的語ルールから外れてるし、classの頭文字cがfunctionのいいところに出てくるので、funclass
という名前にしてみた。
静的メンバはproto.factory
の中に収めている。
これは静的メンバには必ず create
が含まれるからで、本来はproto.static
みたいな名前の方がいいのだろうが、JavaScriptの構文要素として使われている単語だし、メンバー名として用いることに問題はないが避けることにした。
funclass
・buildClass
とも、引数proto
のfactory
はシャローコピーされる。
create
などの関数はユーザーが定義した内容で上書きされるようになっている。
つまり、proto
の書き方次第でcreate
・constructor
・takeover
はカスタマイズ可能である。
ちょっと前の更新ではproto
それ自体もシャローコピーしていたのだが、そうするとsuper
キーワードが効かなくなる。
もしかするとちゃんとsuper
が効くコピーの方法もあるのかもしれないが、とりあえず今は与えられたオブジェクトをそのまま使うしかない。
呼び出し側でプロトタイプを使いまわさないように注意する必要がある。
Symbol.hasInstance
メンバーがdefineProperty
による設定になっているが、これはこの記事を作成中にcreate型の静的メンバをコンストラクタ関数にごっそりコピーしようとしたら、Symbol.hasInstance
メンバーが上書きできない、と怒られたためである。
defineProperty
を使うとデフォルトで列挙不可(かつ設定不可、変更不可)になる。
このふたつのクラスでできることをまとめると以下のようになる。
コンストラクタ関数型 | Object.create型 | |
---|---|---|
クラス定義 | funclass | buildClass
|
new | ◯ | × |
new this.constructor | ◯ | ◯ |
create | ◯ | ◯ |
this.factory.create | ◯ | ◯ |
ワンライナー | × | ◯ |
つまり、コンストラクタ関数型の方ができることが多いのだが、create型にもひとつだけメリットがあり、それは「new
によるオブジェクト生成を避けられる」ことである。
配列を継承したい場合、どうしてもArray()
を呼んだ時に配列ができてしまうので、それをtakeover
するとnew
が作ったオブジェクトは捨てるしかない。
静的解析で「new
は不要だ」と判断することはある程度は可能なので、JavaScriptエンジンで最適化される可能性はあるが、動的にObject.create
する場合とtakeover
する場合が分かれる場合はエンジン側の最適化はできない。
この手のオブジェクトが大量に生成されると、その倍のオブジェクトが生成されて、半分は生まれてすぐにガベージコレクションの対象になるだけである。
create型はこれを避けることができる。
const C = buildClass(Array, { factory: { create(...args) { if (args[0] instanceof D) return args[0].clone(); return this.takeover(Array(...args)); } }, init() { ... }, clone() { ... } });
C++言語からJavaScriptのクラス、特に継承まわりを見たときに、大きく違うのは以下の4点だと思う。
this
が何にでもできる
例えば、「石橋で叩いて渡るGL」では、まずWebGLRenderingContext
を便利に使うコード集としてglUtil
を作り、さらにそれをオブジェクト指向っぽくしてVanillaGL
クラスを作っている。
glUtil
はクラスというより名前空間に近い。
この中にlinkProgram
という関数がある。
この関数は3つのクラス全部に実装されているが、それぞれ若干仕様が違っていて、そのために引数の型が違う。
WebGLRenderingContext
では最初の引数はGLProgram
である。
glUtil
ではWebGLRenderingContext
である。
VanillaGL
ではWebGLShaer
である。
void WebGLRenderingContext.linkProgram (WebGLProgram prog); Object glUtil.linkProgram (WebGLRenderingContext gl, WebGLShader vs, WebGLShader, fs); VanillaGLProgram VanillaGL.linkProgram (WebGLShader vs, WebGLShader, fs);
glUtil
には基底クラスはなく、そのためメソッドを呼び出す際は多くの場合WebGLRenderingContext
を指定してやる必要がある。
linkProgram
の場合は戻り値もその場で適当に作ったオブジェクトで、その中にWebGLProgram
が入っている。
一方、VanillaGL
はWebGLRenderingContext
を基底クラスとしている。
先に作ったglUtil.linkProgram
はWebGLRenderingContext
型の引数gl
を使って、gl.linkProgram
を呼び出す。
後から作ったVanillaGL.linkProgram
はこれは便利とばかりにglUtil.linkProgram
を呼び出す。
glUtil.linkProgram
の第1引数gl
はWebGLRenderingContext
だが、VanillaGL
はWebGLRenderingContext
を継承しているのでthis
をそのまま渡せる。
ここまではC++もJavaScriptも似たように進む。
違うのはこの先である。
glUtil
は後からVanillaGL
ができたことを知らないから、何も考えずにgl.linkProgram
を呼び出す。
ところがgl
引数は実はVanillaGL
なので、JavaScriptの場合はVanillaGL.linkProgram
を呼び出してしまう。
無限再帰のできあがりである。
C++の場合、glUtil.linkProgram
呼び出し時にVanillaGL
のインスタンスはWebGLRenderingContext
にキャストされ、linkProgram
が仮想関数でなければglUtil
は必ず引数に書かれたクラスであるWebGLRenderingContext.linkProgram
を呼び出す。
仮想関数であった場合でも、引数型が違えば違う関数として認識されるので、オーバーロードで解決することが可能である。
一方、JavaScriptの場合はそもそもキャストの概念がない。
キャストできたところで常に仮想関数のように振る舞うので、必ずVanillaGL.linkProgram
を呼び出してしまう。
しかも、引数型が宣言になく、言語としてオーバーロードの仕様がないので、クラス内に同じ名前の関数はひとつしか存在しない。派生クラス側の関数が呼ばれると決まった時点で再帰呼び出し確定である。
引数型がWebGLRenderingContext
とVanillaGL
で違うのならば、受け取った側で型を調べることができるので、VanillaGL
側で引数の数と型を調べて分岐し、第1引数がWebGLProgram
であればWebGLRenderingContext
の関数を呼び出せばよい。
そしてこれがJavaScriptでのオーバーロードの実現方法である。
VanillaGL.linkProgram
もこの方法で実装している。
呼び出し方はsuper.linkProgram(vs);
のようになる。
先に書いたように、オブジェクトリテラル形式でも書き方によってはsuper
が使える。
もしsuper
を使わないのなら、プロトタイプから引っ張ってくればいいので、
WebGLRenderingContext.prototype.linkProgram.call(this, vs)
のようになる。
WebGLRenderingContext
自体はコンストラクタ関数であり、静的メンバしか持っていない。
インスタンスメソッドはコンストラクタ関数のprototype
プロパティを見る必要がある。
そしてcall
でthis
を強制的に置き換えて呼び出す。
JavaScriptのthis
はかなりいい加減で、関数呼び出しの前に何かオブジェクトがくっついている場合は、原則としてそのオブジェクトがthis
である(例外はアロー関数)。
この場合は放っておくと
WebGLRenderingContext.prototype
がthis
になる。
これはほとんどの場合で意図したものではないため、
WebGLRenderingContext.linkProgram
を呼び出すときに、call
で強制的にVanillaGL
オブジェクトをthis
に設定して呼び出す。
VanillaGL
クラスはWebGLRenderingContext
クラスを継承しているため、これで問題なく動作する。
call
はFunction
クラスのメソッドで、関数ならば必ずこのメソッドを持っている。
この関数を使えばどんなオブジェクトでもthis
に設定できてしまう。
ある意味これがC++のキャストに相当するが、キャストもへったくれもなく、好きなように呼び出し先のthis
を設定できる。
いい具合にいい加減である。
言い方を変えると、super.f(...args)
は、基底クラスがB
ならば、B.prototype.f.call(this, ...args)
のシンタックスシュガーであるとも言えそうだ。
C++でも、linkProgram
が仮想関数で、派生クラス・基底クラスで引数型が同じだった場合は、基底クラスにキャストしても派生クラス側のlinkProgram
が再帰呼び出しされる(仮想関数だからね)。
glUtil
側に修正を加えていいのならば、gl.WebGLRenderingContext::linkProgram
と書けば、linkProgram
が仮想関数であったとしてもWebGLRenderingContext
のlinkProgram
が呼び出される。
glUtil
側を修正できない場合は、JavaScript同様、派生クラスであるVanillaGL
側で対策をする必要がある。
考えられるのは、VanillaGL
に「linkProgram
実行中」というフラグを設けておいて、フラグが立っていなければフラグを立てた上でglUtil
のlinkProgram
を呼ぶ。立っていればWebGLRenderingContext
のlinkProgram
を呼び出してフラグを落とす、だろう。
これはJavaScriptでもC++でも同様に実装できる。
15 Jul 2024: 初稿
Copyright (C) 2024 akamoz.jp