【JavaScript】実行中の関数自身の関数名やクラス自身のメソッド名を取得する

Code

はじまり

リサちゃん
リサちゃん

ぐああ〜、関数の名前が欲しいんだが、thisで取得できないのか? Pythonのselfみたく出来ると思ったんだが・・・

135ml
135ml

あ〜、テストクラスとか作ると欲しくなるよね。JavaScriptは、thisを使ったときの挙動が、呼び出し元でどう呼び出すかで変わるんだよねえ。

リサちゃん
リサちゃん

なんてこった・・・。解説頼む!

今回の問題点

今回の問題点は、以下のように関数を実行すると、thisが関数自身ではなく他の関数を指してしまう事象です。(thisがundefinedになってしまうこともありますよね・・・。)

function getThisFuncName(func){
  const funcValue = func.toString();
  const initialOfFuncStatement = "function ";
  let funcName = "";
  if(funcValue.indexOf(initialOfFuncStatement, 0) !== 0){
    funcName = funcValue.substring(0, funcValue.indexOf("(", 0));
  }else{
    funcName = funcValue.substring(initialOfFuncStatement.length, funcValue.indexOf("(", 0));
  }
  return funcName;
}

function myFunction(){
  console.log(this);
  console.log("=============================");
  console.log(this.toString());
  console.log("///////////////////////////");
  console.log(getThisFuncName(this));
}

function main(){
  myFunction();
}

ミスった出力

{ myTestFunc: [Function: myTestFunc],
  ...
  (略)
  ... }
=============================
[object Object]
///////////////////////////

今回のソース①

そんなthisが指すところが定まらない問題を解決するために、今回使用したソースはこんな感じになります。

function getThisFuncName(func){
  const funcValue = func.toString();
  const initialOfFuncStatement = "function ";
  let funcName = "";
  if(funcValue.indexOf(initialOfFuncStatement, 0) !== 0){
    funcName = funcValue.substring(0, funcValue.indexOf("(", 0));
  }else{
    funcName = funcValue.substring(initialOfFuncStatement.length, funcValue.indexOf("(", 0));
  }
  return funcName;
}

function myFunction(){
  console.log(this);
  console.log("=============================");
  console.log(this.toString());
  console.log("///////////////////////////");
  console.log(getThisFuncName(this));
}

function main(){
  const bindFunc = myFunction.bind(myFunction);
  bindFunc();
}

出力

[Function: myFunction]
=============================
function myFunction(){
  console.log(this);
  console.log("=============================");
  console.log(this.toString());
  console.log("///////////////////////////");
  console.log(getThisFuncName(this));
}
///////////////////////////
myFunction

まず、main()処理を実行します。

func.bind(obj)で、funcのthisにobjをbindさせるというイメージです。

そのため、今回は、myFunctionのthisに自身をバインドさせたいので、以下のように書くことで、thisがmyFunctionになった関数bindFuncを宣言できます。

function main(){
  const bindFunc = myFunction.bind(myFunction);
  bindFunc();
}

そして、bindFuncを実行すると、myFunctionを実行しながらも、thisがundefinedとかにならずにmyFunctionが入っています。

function myFunction(){
  console.log(this);
  console.log("=============================");
  console.log(this.toString());
  console.log("///////////////////////////");
  console.log(getThisFuncName(this));
}

補足:bind以外のバインド方法

bind以外にもthisにオブジェクトをバインドさせる方法が2つあります。

callapplyなのですが、引数を渡さないのであれば、関数名以外の書き方は変わりません。引数を渡さない場合は、callがスプレッド形式で、applyがリスト形式で引数を渡します。

call

function getThisFuncName(func){
  const funcValue = func.toString();
  const initialOfFuncStatement = "function ";
  let funcName = "";
  if(funcValue.indexOf(initialOfFuncStatement, 0) !== 0){
    funcName = funcValue.substring(0, funcValue.indexOf("(", 0));
  }else{
    funcName = funcValue.substring(initialOfFuncStatement.length, funcValue.indexOf("(", 0));
  }
  return funcName;
}

function myFunction(){
  console.log(this);
  console.log("=============================");
  console.log(this.toString());
  console.log("///////////////////////////");
  console.log(getThisFuncName(this));
}

function main(){
  myFunction.call(myFunction);
}

apply

function getThisFuncName(func){
  const funcValue = func.toString();
  const initialOfFuncStatement = "function ";
  let funcName = "";
  if(funcValue.indexOf(initialOfFuncStatement, 0) !== 0){
    funcName = funcValue.substring(0, funcValue.indexOf("(", 0));
  }else{
    funcName = funcValue.substring(initialOfFuncStatement.length, funcValue.indexOf("(", 0));
  }
  return funcName;
}

function myFunction(){
  console.log(this);
  console.log("=============================");
  console.log(this.toString());
  console.log("///////////////////////////");
  console.log(getThisFuncName(this));
}

function main(){
  myFunction.apply(myFunction);
}

今回使用したソース②:classから取得

少し余談を話したところで、次に、class内の関数(メソッド)から取得してみます。

書き方はクラスを宣言するところ以外は特に変わりません。

class MyClass{
  myFunction(){
    console.log(this);
    console.log("=============================");
    console.log(this.toString());
    console.log("///////////////////////////");
    console.log(getThisFuncName(this));
  }
} 

function main(){
  const myClass = new MyClass();
  const bindFunc = myClass.myFunction.bind(myClass.myFunction);
  bindFunc();
}

出力

[Function: myFunction]
=============================
function myFunction(){
  console.log(this);
  console.log("=============================");
  console.log(this.toString());
  console.log("///////////////////////////");
  console.log(getThisFuncName(this));
}
///////////////////////////
myFunction

今回使用したソース③:プロパティディスクリプタから取得

僕が今回の記事を書こうとした時にハマったところがここで、クラスオブジェクトが持っているメソッドを一気に取得して、それらのメソッドを実行した都度、メソッド名を取得してみたいと思います。

最終形はこれです。メソッド名を一式取得できています。

function removeItemsByValues(array, values){
  if(!Array.isArray(array)){
    throw new TypeError("array must be array type.");
  }
  if(!Array.isArray(values)){
    throw new TypeError("values must be array type.");
  }
  let index = -1;
  for(let i = 0; i < values.length; i++){
    index = array.indexOf(values[i]);
    if(index !== -1){
      array.splice(index, 1);
    }
  }
  return array;
}

function getThisFuncName(func){
  const funcValue = func.toString();
  const initialOfFuncStatement = "function ";
  let funcName = "";
  if(funcValue.indexOf(initialOfFuncStatement, 0) !== 0){
    funcName = funcValue.substring(0, funcValue.indexOf("(", 0));
  }else{
    funcName = funcValue.substring(initialOfFuncStatement.length, funcValue.indexOf("(", 0));
  }
  return funcName;
}

class MyClass{
  myFunction1(){
    console.log(this);
    console.log("=============================1");
    console.log(this.toString());
    console.log("///////////////////////////1");
    console.log(getThisFuncName(this.value));
  }
  myFunction2(){
    console.log(this);
    console.log("=============================2");
    console.log(this.toString());
    console.log("///////////////////////////2");
    console.log(getThisFuncName(this.value));
  }
} 

function descriptExec(execClass){
  let descriptorObj = Object.getOwnPropertyDescriptors(execClass.prototype);
  let descriptorKeys = Object.keys(descriptorObj);
  descriptorKeys = removeItemsByValues(descriptorKeys, ["constructor"]);
  for(let i = 0; i < descriptorKeys.length; i++){
    descriptorObj[descriptorKeys[i]].value();
  }
}

function main(){
  descriptExec(MyClass);
}

出力

{ value: [Function: myFunction1],
  writable: true,
  enumerable: false,
  configurable: true }
=============================1
myFunction1(){
    console.log(this);
    console.log("=============================1");
    console.log(this.value.toString());
    console.log("///////////////////////////1");
    console.log(getThisFuncName(this.value));
  }
///////////////////////////1
myFunction1
{ value: [Function: myFunction2],
  writable: true,
  enumerable: false,
  configurable: true }
=============================2
myFunction2(){
    console.log(this);
    console.log("=============================2");
    console.log(this.value.toString());
    console.log("///////////////////////////2");
    console.log(getThisFuncName(this.value));
  }
///////////////////////////2
myFunction2

descriptorObjに入っているのが、プロパティディスクリプタです。ー①

そして、descriptorKeysにconstructorを含めたメソッドを全て取得します。今回は、MyClassが持っているメソッドを全て取得します。ー②

この処理で、MyClassはnewとかでインスタンス化しておらず、constructorを呼び出すのはマズいのでconstructorは呼び出す処理からremoveItemsByValues()で除外します。ー③

そうしたら、for文で1つずつ関数を実行していきます。ー④

function removeItemsByValues(array, values){
  if(!Array.isArray(array)){
    throw new TypeError("array must be array type.");
  }
  if(!Array.isArray(values)){
    throw new TypeError("values must be array type.");
  }
  let index = -1;
  for(let i = 0; i < values.length; i++){
    index = array.indexOf(values[i]);
    if(index !== -1){
      array.splice(index, 1);
    }
  }
  return array;
}

class MyClass{
  myFunction1(){
    console.log(this);
    console.log("=============================1");
    console.log(this.value.toString());
    console.log("///////////////////////////1");
    console.log(getThisFuncName(this.value));
  }
  myFunction2(){
    console.log(this);
    console.log("=============================2");
    console.log(this.value.toString());
    console.log("///////////////////////////2");
    console.log(getThisFuncName(this.value));
  }
} 

function descriptExec(execClass){
  let descriptorObj = Object.getOwnPropertyDescriptors(execClass.prototype); // ー①
  let descriptorKeys = Object.keys(descriptorObj); // ー②
  descriptorKeys = removeItemsByValues(descriptorKeys, ["constructor"]); // ー③
  for(let i = 0; i < descriptorKeys.length; i++){
    descriptorObj[descriptorKeys[i]].value(); // ー④
  }
}

function main(){
  descriptExec(MyClass);
}

すると、注目して欲しい点が2つありまして、

1つ目は、呼び出し場所でthisにメソッドをバインディングする必要がない点です。

今まで、main()処理(今回だと実行場所はdescriptExec())でbindしていたのですが、今回はバインドしていません。

2つ目は、メソッド名をするための記述です。今まで、this.toString()で関数・メソッドを取得していましたが、今回はthis.value.toString()で取得しています。

このthisがどうバインディングされたのかは正直のところ分かりませんが、プロパティディスクリプタから関数を呼び出すと挙動が何か変わっています。

class MyClass{
  myFunction1(){
    console.log(this);
    console.log("=============================1");
    console.log(this.value.toString()); // ← this.toString()じゃない。
    console.log("///////////////////////////1");
    console.log(getThisFuncName(this.value));
  }
  myFunction2(){
    console.log(this);
    console.log("=============================2");
    console.log(this.value.toString()); // ← this.toString()じゃない。
    console.log("///////////////////////////2");
    console.log(getThisFuncName(this.value));
  }
} 

function descriptExec(execClass){
  let descriptorObj = Object.getOwnPropertyDescriptors(execClass.prototype);
  let descriptorKeys = Object.keys(descriptorObj);
  descriptorKeys = removeItemsByValues(descriptorKeys, ["constructor"]);
  for(let i = 0; i < descriptorKeys.length; i++){
    descriptorObj[descriptorKeys[i]].value();
  }
}

function main(){
  descriptExec(MyClass);
}

おしまい

リサちゃん
リサちゃん

ふう・・・、何とか取得できたな・・・

135ml
135ml

実行中の関数名って地味に使うけど、取得するのは少し面倒いよな。

少しでも、この記事が助けになれば嬉しいです。

リサちゃん
リサちゃん

嬉しいぞ!

参考

ペンギン
ペンギン

この方の記事に、更に詳しく”this”のことについて記載されていて、参考になりました。

【JS】thisについて

以上になります!

コメント

タイトルとURLをコピーしました