Memo:

アイデアや気づきとかが雑に書き殴られる

JavaScript でクラスのメソッド名を取得する方法 - プロトタイプチェーンを利用して列挙不可能プロパティを取得する

TypeScript でクラスをインスタンス化して、そのメソッド名をループで取得したかったが簡単にできなくてハマったので残しておく。

class MyClass {
    property = 24;
    method(a: number, b: number): number {
        return a + b;
    }
}

const obj = new MyClass();
console.log(Object.keys(obj));

for (const key in obj) {
    console.log(key);
}
/*
'property'  // method がない!
*/

JavaScript ではプロトタイプ継承という仕組みがある。ザックリいうと、MyClass のインスタンス objmethod は持っていないのだが、obj.method とアクセスしたときにチェーンされている prototype をさかのぼり実体の MyClass.prototype.method が検索されアクセスできるという感じの機能。

継承とプロトタイプチェーン - JavaScript | MDN

for...in 文は、キーが文字列であるオブジェクトの列挙可能プロパティすべてに対して、継承された列挙可能プロパティも含めて反復処理を行います (Symbol がキーになったものは無視します)。 for...in - JavaScript | MDN

プロトタイプチェーンをふと思い出したので、これが原因か...?と思ったが key..in 文はプロトタイプチェーンをたどってくれるらしい。

「列挙可能プロパティ」というのが気になる Object.getOwnPropertyDescriptor を使えば確認できるようなので見てみる。

console.log(Object.getOwnPropertyDescriptor(obj, "property"));
// { value: 24, writable: true, enumerable: true, configurable: true }

console.log(Object.getOwnPropertyDescriptor(obj, "method")); // --> undefined
console.log(Object.getOwnPropertyDescriptor(MyClass.prototype, "method")); // 実体を見る必要がある
/*
{
  value: [Function: method],
  writable: true,
  enumerable: false,  // 列挙不可能プロパティに設定されていた!
  configurable: true
}
*/

obj.property は列挙可能プロパティだが、obj.method は列挙不可能なので、出てこなかったみたいだ。

結論

Object.getPrototypeOf() でプロトタイプにアクセスし、そこから Object.getOwnPropertyNames() で得られる。 Object.getOwnPropertyNames()は列挙不可能プロパティも取得できるという特性を持っているので取得が可能になる。

class MyClass {
    property = 24;
    method(a: number, b: number): number {
        return a + b;
    }
}

const obj = new MyClass();

console.log(Object.getOwnPropertyNames(obj)); // [ 'property' ]
console.log(Object.getOwnPropertyNames(Object.getPrototypeOf(obj))); // [ 'constructor', 'method' ]

継承している場合

また、何重にも継承をしていると、プロトタイプチェーンが長くなるので再帰的にアクセスする必要があったりする。これで親の親クラスにあるメソッド名 'methodOfSuperParent' を得られた。色々関係ないメソッド名まで出てきてしまうので、もう少し工夫は必要そうではあるけれど。

class SuperParentClass {
    methodOfSuperParent(a: string, b: string): string {
        return `${a} * ${b}`;
    }
}

class ParentClass extends SuperParentClass {
    methodOfParent(a: string, b: string): string {
        return `${a} + ${b}`;
    }
}

class MyClass extends ParentClass {
    property = 24;
    method(a: number, b: number): number {
        return a + b;
    }
}

const obj = new MyClass();

let keys = Object.getOwnPropertyNames(obj);
let prototype = Object.getPrototypeOf(obj);
while (true) {
    keys = keys.concat(Object.getOwnPropertyNames(prototype));
    prototype = Object.getPrototypeOf(prototype);
    if (!prototype) {
        break;
    }
}

console.log(keys);
/*
[
  'property',             'constructor',
  'method',               'constructor',
  'methodOfParent',       'constructor',
  'methodOfSuperParent',  'constructor',
  '__defineGetter__',     '__defineSetter__',
  'hasOwnProperty',       '__lookupGetter__',
  '__lookupSetter__',     'isPrototypeOf',
  'propertyIsEnumerable', 'toString',
  'valueOf',              '__proto__',
  'toLocaleString'
]
*/