はじめに
JavaScript ではメソッドを解決するために、プロトタイプチェーンの仕組みにより親クラスを辿る。
node の REPL で確認すると、生成したオブジェクトは (デフォルトの親クラスである) Object にあるインスタンスメソッドの toString() を実行できる。
> class Klass {}
undefined
> const o = new Klass
undefined
> o.toString()
'[object Object]'
と、いった一般的なことは理解していたが、静的メソッドの Object.keys() はどうなっているのかと疑問になった。
> Object.keys({key:'value'})
[ 'key' ]
> Klass.keys({key:'value'})
Uncaught TypeError: Klass.keys is not a function
この「親クラスなのに静的メソッドは呼び出せない」という状況が説明できていなかったので調査してみた。
前提となること
改めてプロトタイプについて MDN で調べると __proto__ prototype [[Prototype]] と 3 つの単語が出てくる。
MDN によると __proto__ と [[Prototype]] は (違いがあるが)同義で、prototype はクラスの中にあるインスタンスメソッドが配置される連想配列となっている。
> class Klass { static static_f() {}
... instance_f() {} }
undefined
上記のクラスでは以下のようにメソッドが配置される。
また [[Prototype]] はクラスとオブジェクトの中に作成され、どこかへのリンクになっている。
さらに prototype の中にも [[Prototype]] が作成されている。
[[Prototype]] のリンク先はインスタンスメソッドと静的メソッドの場合により考え方が違ってくる。
インスタンスメソッドの場合
new により生成されたオブジェクトの [[Prototype]] は生成元のクラスの prototype へのリンクになっている。
> class Parent {}
undefined
> class Child extends Parent {}
undefined
> const o = new Child
undefined
> o.__proto__ === Child.prototype
true
それ以外は親クラスの prototype にリンクし、最後は null になる。
> Child.prototype.__proto__ === Parent.prototype
true
> Parent.prototype.__proto__ === Object.prototype
true
> Object.prototype.__proto__ === null
true
この繋がりを図示すると以下のようになる。
オブジェクトに対しメソッドが実行されたとき、指定されたメソッドが存在しない場合は [[Prototype]] のリンク先に移動して探す、ということを null になるまで繰り返す。
これはイメージしていたプロトタイプチェーンと同じものであり、わかりやすい。
REPL で確認するとオブジェクトから親クラスのメソッドを呼び出せていることが確認できる。
> class Parent { func() { return 'parent' } }
undefined
> class Child extends Parent { }
undefined
> const o = new Child
undefined
> o.func()
'parent'
> o.valueOf()
Child {}
静的メソッドの場合
インスタンスメソッドと違い、静的メソッドではクラスによって [[Prototype]] のリンク先が違ってくる。
ここで新しく Function クラスが登場し、その [[Prototype]] は自身の prototype へリンクする。
また Object クラスの [[Prototype]] も上記にリンクしている。
> Function.__proto__ === Function.prototype
true
> Object.__proto__ === Function.prototype
true
そして、ユーザークラスの場合は派生元の違いにより [[Prototype]] のリンク先が変わってくる。
親クラスが Object であるクラス(Parent) の [[Prototype]] のリンク先は Function の prototype になる。
派生したクラス(Child) の [[Prototype]] のリンク先は、親クラス(Parent) の コンストラクタ になる。
> Parent.__proto__ === Function.prototype
true
> Child.__proto__ === Parent
true
全体図
ここまでに現れたオブジェクトとクラスの関係を一枚の図にすると以下になる。
図の矢印に沿って考えると、一番最初に疑問だった以下のコードに納得ができる。
> class Klass {}
undefined
> Klass.keys({key:'value'})
Uncaught TypeError: Klass.keys is not a function
静的メソッドの場合は [[Prototype]] を辿っても Klass.[[Prototype]] → Function.prototype → Object.prototype → null
となり、Object.keys は見つからない。
メソッドの探索
図を見ると Child の [[Prototype]] だけはリンク先が どこかの prototype ではなく、親クラスの コンストラクタ になっている。
一貫性がないようにも見えるが、このことによりメソッドの探索ロジックがインスタンスメソッドと静的メソッドの場合で共通になることができている。
function call_method(t, name, ...args)
{
if (!t)
{
console.log(`--> NOT FOUND: '${name}'`)
return
}
if (t.hasOwnProperty('constructor'))
{
console.log(`CLASS(prototype): ${t.constructor.name}`)
}
else if (t.hasOwnProperty('name'))
{
console.log(`CLASS: ${t.name}`)
}
else
{
console.log('OBJECT')
}
if (t.hasOwnProperty(name))
{
const f = t[name]
if (typeof f === 'function')
{
console.log(`--> FOUND: '${name}'`)
try {
const res = f.apply(t, args)
console.log(` CALL SUCCESS: ${res}`)
} catch (error) {
console.log(` CALL ERROR: ${error.message}`)
}
}
else
{
console.log(`--> NOT FOUND: '${name}'`)
}
}
else
{
call_method(Object.getPrototypeOf(t), name, ...args)
//call_method(t.__proto__, name, ...args)
}
}
上記は 対象と関数名、引数を元にメソッドを実行する関数となっている。
この関数を使い、new により生成したオブジェクトから親クラスのインスタンスメソッドを呼び出すと、通常のメソッドの呼び出しと同じように実行できる。
> .load a.js
...
[Function (anonymous)]
> class Parent { static parent_static() { return 'parent static' }
... parent_inst() { return 'parent inst' } }
undefined
> class Child extends Parent {}
undefined
> const child = new Child
undefined
> child.parent_inst()
'parent inst'
> call_method(child, 'parent_inst')
OBJECT
CLASS(prototype): Child
CLASS(prototype): Parent
--> FOUND: 'parent_inst'
CALL SUCCESS: parent inst
undefined
クラスを対象とすると、同じ関数で静的メソッドの呼び出しも行える。
> Child.parent_static()
'parent static'
> call_method(Child, 'parent_static')
CLASS: Child
CLASS: Parent
--> FOUND: 'parent_static'
CALL SUCCESS: parent static
undefined






