文字列操作

【JavaScript】関数やクラスから動的に内部コードと引数リストを取得する方法

更新日:2024/02/27

全く使用する機会がないと思うが、JavaScriptの関数オブジェクトから関数内部のソースコードと、定義時の引数リストを取得する方法をお伝えします。

 

関数のコードを取得する方法

プログラムコード上で定義された関数はJavaScriptのシステムに読み込まれると、関数オブジェクトに変換されます。
この関数オブジェクトはtoString()というメソッドを持っていて、実行すると関数定義を文字列で返します。

関数定義で生成した関数のコードを出力してみます。

関数定義コードを出力

     function       func( a , b , c = 1 /* 省略可 */ )       {
    console.log( a , b , c);
}
console.log( func.toString() );
 // 結果:
 // "function       func( a , b , c = 1 /* 省略可 */ )       {
 //    console.log( a , b , c);
 // }"

意図的にスペースを多用していたり、引数リスト内にコメントが入っていたりしますが、そのまま出力されていますね。
ただし、functionの前のスペースは削除されます。

関数式でも同様です。

const func2 =      ( a , b ,c = 1 /* 省略可 */) => {
    console.log( a , b , c);
}
console.log( func2.toString() );
 // 結果:
 // "( a , b ,c = 1 /* 省略可 */) => {
 //    console.log( a , b , c);
 // }"

 

クラスのコードを取得する方法

JavaScriptのclass構文はシステムに読み込まれると、関数オブジェクトに変換されます。
こちらも前項と同様にtoString()というメソッドでコードを出力できます。

class class1 {
    #a;#b;#c;
    constructor(a,b,c){
        this.#a = a;this.#b = b;this.#c = c;
    }
}
console.log( class1.toString() );
 // 結果:
 // "class class1 {
 //     #a;#b;#c;
 //     constructor(a,b,c){
 //         this.#a = a;this.#b = b;this.#c = c;
 //     }
 // }"

式での定義も同様です。

const class2 = class {
    #a;#b;#c;
    constructor(a,b,c){
        this.#a = a;this.#b = b;this.#c = c;
    }
}
console.log( class12.toString() );
 // 結果:
 // "class {
 //     #a;#b;#c;
 //     constructor(a,b,c){
 //         this.#a = a;this.#b = b;this.#c = c;
 //     }
 // }"

 

引数リストを取得する方法

取得したコードから引数リストを抽出してみます。
基本的に正規表現を使用した文字列解析です。

完成したコード

完成したコードを、最初に掲載しておきます。

const getFunctionParameter = f =>{
    if( typeof f !== "function" ) return false;

        // コメント、改行をスペースに変換
        // 文字列リテラルを退避
    const textBuf = [];
    const code = f.toString()
                .replace( /(['"])(.*?)(?<!\\)\1/ // 文字列リテラルを退避
                    , m=>{
                        const text = Math.random().toString(36).substring(2)
                                    + textBuf.length.toString()
                                    + Math.random().toString(36).substring(2);
                        textBuf.push( {replaceText : text, originalText : m} );
                        return text;
                    })
                .replace( /\/\/.*$/ , " " ) // コメント "//" をスペースに変換
                .replace( /\/\*.*?\*\// , " " ) // コメント /* */ をスペースに変換
                .replace( /\r?\n|\r/g , " " ); // 改行をスペースに変換

        // 引数の抽出
    const funcProc = m => m[1];
        // 引数の抽出(クラス用)
    const classProc = m => {
        const text = m[1];
        let param = "" , depth = 0;

        const regexp = /(.*?)([{}])/g;
        let result;
        while( (result  = regexp.exec( text )) !== null ){
            if( depth === 1 ) {
                const match = result[1].match( /constructor *?\((.*?)\)/ );
                if( match !== null ) param = match[1];
            }
            if( result[2] === "{" ) depth ++;
            else if( result[2] === "}" ) depth --;
        } 
        return param;
    }
        // 解析開始
    const paramString = 
        [ 
            { regexp: /^function .*?\((.*?)\)/ , proc : funcProc } , // function name(){}
            { regexp: /^function *?\((.*?)\)/ , proc : funcProc } , // function (){}
            { regexp: /^\((.*?)\) *?=>/ , proc : funcProc } , // (a,b,c)=> {}
            { regexp: /^(.*?)=>/ , proc : funcProc } , // a => {}
            { regexp: /^class(([ {]).*$)/ , proc : classProc } , // class
        ]
        .reduce( (result,e)=>{
            if( result.skip ) return result;
            const m = code.match( e.regexp );
            return m === null ? result
                     : { skip:true , param : e.proc( m )};
        },{skip:false,param:""}).param.split(",");

    return paramString.map(e=>{ // 退避した文字列リテラルを戻す
        textBuf.forEach( ({replaceText , originalText}) =>{
            e = e.replace( replaceText , originalText );
        });
        return e.trim();
    });
};

思ったよりも長いコードになりましたが、やっていることは単純です。

関数を使用して、パラメータを取得してみます。。

function funcA( a ,b , c = ")"){}
const funcB = function( a ,b , c = ")"){}
const ArrowFuncA = (a ,b , c = "=>") => {};
const ArrowFuncB=  a => {};

class A extends class extends class {constructor(){}} {constructor(){}} {
    constructor(a ,b , c = "=>"){}
    func(){
        class B{ constructor(){} }
        return new B();
    }
};

console.log( getFunctionParameter( funcA ) );      // [ "a", "b", 'c = ")"' ]
console.log( getFunctionParameter( funcB ) );      // [ "a", "b", 'c = ")"' ]
console.log( getFunctionParameter( ArrowFuncA ) ); // [ "a", "b", 'c = "=>"' ]
console.log( getFunctionParameter( ArrowFuncB ) ); // [ "a" ]
console.log( getFunctionParameter( A ) );          // [ "a", "b", 'c = "=>"' ]

クラス定義の例が一見意味不明ですが、エラーにはなりません。
対応に苦労しました。

コードの解説

コードの解説です。

コメント等を削除

まずは文字リテラルが削除されないように、ランダムな文字列(記号なし)に置き換え、置き換えた情報を配列にセットしています。

次にコメントをスペースに置き換えます。

空文字ではなくスペースなのは、次のようなケースに対応するためです。
function/* コメント */abc( )

最後にパラメータ内などに改行などが入っていると面倒なので、改行をスペースに置き換えます。

正規表現でパラメータを取得

次に正規表現でパラメータを取得します。

関数は次のような正規表現を作成しました。

function name(p) のとき: /^function .*?\((.*?)\)/
function (p) のとき      : /^function *?\((.*?)\)/
(p) => のとき            : /^\((.*?)\) *?=>/
p => のとき              : /^(.*?)=>/

実際には一つの正規表現で4つのパターンに対応できそうですね。
でも見落としパターンがある可能性があるので、対応しやすいようにわけました。

次にclassからのパラメータ取得です。

class内のconstructor()メソッドを参照できれば簡単だったのですが、このメソッドはオブジェクト内部のプライベートな領域に保存されているため、参照できません。
そのため、class定義の文字列から抽出する必要があります。

しかし、次のように"constructor()"が乱立するような定義が可能なので、単純にはいきません。

class A extends class extends class {constructor(){}} {constructor(){}} {
    constructor(a ,b , c = "=>"){}
    func(){
        class B{ constructor(){} }
        return new B();
    }
};

そこで "{" と "}" の出現を監視して階層管理をおこなっています。(classProc()関数)
そして最初の階層で、最後に出現した"constructor()"からパラメータを抽出しています。

文字列リテラルを戻す

最後にランダムな文字列に置き換えた文字列リテラルを、元に戻して終了です。

更新日:2024/02/27

書いた人(管理人):けーちゃん

スポンサーリンク

記事の内容について

null

こんにちはけーちゃんです。
説明するのって難しいですね。

「なんか言ってることおかしくない?」
たぶん、こんなご意見あると思います。

裏付けを取りながら記事を作成していますが、僕の勘違いだったり、そもそも情報源の内容が間違えていたりで、正確でないことが多いと思います。
そんなときは、ご意見もらえたら嬉しいです。

掲載コードについては事前に動作確認をしていますが、貼り付け後に体裁を整えるなどをした結果動作しないものになっていることがあります。
生暖かい視線でスルーするか、ご指摘ください。

ご意見、ご指摘はこちら。
https://note.affi-sapo-sv.com/info.php

 

このサイトは、リンクフリーです。大歓迎です。