0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

プロトタイプチェーンを辿ってメソッドを探索する

0
Last updated at Posted at 2026-03-14

はじめに

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

上記のクラスでは以下のようにメソッドが配置される。

image.png

また [[Prototype]] はクラスとオブジェクトの中に作成され、どこかへのリンクになっている。

image.png

さらに prototype の中にも [[Prototype]] が作成されている。

image.png

[[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

この繋がりを図示すると以下のようになる。

image.png

オブジェクトに対しメソッドが実行されたとき、指定されたメソッドが存在しない場合は [[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

image.png

そして、ユーザークラスの場合は派生元の違いにより [[Prototype]] のリンク先が変わってくる。

親クラスが Object であるクラス(Parent) の [[Prototype]] のリンク先は Functionprototype になる。

派生したクラス(Child) の [[Prototype]] のリンク先は、親クラス(Parent) の コンストラクタ になる。

> Parent.__proto__ === Function.prototype
true
> Child.__proto__ === Parent
true

image.png

全体図

ここまでに現れたオブジェクトとクラスの関係を一枚の図にすると以下になる。

image.png

図の矢印に沿って考えると、一番最初に疑問だった以下のコードに納得ができる。

> class Klass {}
undefined
> Klass.keys({key:'value'})
Uncaught TypeError: Klass.keys is not a function

静的メソッドの場合は [[Prototype]] を辿っても Klass.[[Prototype]]Function.prototypeObject.prototype → null
となり、Object.keys は見つからない。

メソッドの探索

図を見ると Child[[Prototype]] だけはリンク先が どこかの prototype ではなく、親クラスの コンストラクタ になっている。

一貫性がないようにも見えるが、このことによりメソッドの探索ロジックがインスタンスメソッドと静的メソッドの場合で共通になることができている。

a.js
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
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?