関数・メソッド

【JavaScript】 関数オーバーロードを実装してみる

更新日:2020/06/09

JavaScriptはオーバーロードという機能を持っていません。
しかし先達たちは、様々な方法で疑似的なオーバーロードを実現しているようです。

そこで僕も、自分なりに疑似的なオーバーロードを実装してみます。

 

やりたいこと

引数の数と型が異なる同名の関数を複数定義して、それぞれ実行できるようにする。

オーバーロードの例


     // 同名の関数を複数定義
const a = function( b ){ } // bは数値
const a = function( c ){ } // cはテキスト
const a = function( b , c ){ } // bは数値 cはテキスト

   // 引数の数や型で実行する関数を使い分ける
a( "abc " ); // 2番目の関数aを実行
a( 10 , "abc " ); // 3番目の関数aを実行

 

作成したJavaScriptコード

const overload = (  ) => {

            const funcs=[];
            let name=null;

            return (  f   => {
                f.add = ( argTypes , func ) => funcs.push(
                    { length:argTypes.length ,
                        type : argTypes.map( e => e.trim().toUpperCase() ).join( "," ) ,
                        func : func }
                );
                f.setName = newName => name = newName;
                return f;

            })( function ( ...arg ){
                let retVal = null;
                const argType = arg.map( e => (typeof e).toUpperCase() ).join( "," );

                if( funcs.every(
                    e   => {
                        if( arg.length !== e.length ) return true;
                        if( e.type === argType ) {
                            retVal = e.func.apply( this , arg); return false;
                        }
                        return true;
                    }
                ) ) {
                    if( name !== null ) {
                        let parent = typeof Object.getPrototypeOf(this) === "object" ?
                            Object.getPrototypeOf(Object.getPrototypeOf(this)) : undefined;

                        if( typeof parent === "object" &&  typeof parent[name] === "function" ){
                            retVal = parent[name].apply( this , arg); return false;
                        }
                    }
                    throw new Error("overload:No matching function");
                }
                return retVal;
            }
        );

};

 

解説

overload関数を実行すると、無名関数function ( ...arg ){}がオブジェクト化されて、即時関数 ( f  => {})()に引数として渡されます。

即時関数では、無名関数にadd()とsetName()というメソッドをセットして、無名関数をリターンしています。

このコードは、次のように書いた方がわかりやすいかもしれません。

const overloadfunc = function ( ...arg ){ ...省略  };

overloadfunc.add =  ( argTypes , func ) =>funcs.push( ...省略 );
overloadfunc.setName  = newName => name = newName;

return overloadfunc;

しかし、無名関数に名前を付けたくなかったので、このような形になっています。

オーバーロードする関数は、add()メソッドを使用して登録します。

add()の形式

const oFunc = overload();
oFunc.add( argTypes , func )

argTypesはオーバーロードする引数の型を順番に格納した配列です。
typeofで判定しているため、"number"や"boolean"、"string"、"function"、"object"等が指定できます。

funcは、呼び出される関数です。

add()メソッド内では、argTypesを大文字にして","で接続した文字列に変換しています。

setName()メソッドは、関数の名前をセットします。
クラスで継承元のメソッドを呼び出すときに使用します。

オーバーロード関数の呼び出しは、次のようにおこないます。

オーバーロード関数の呼び出し

const oFunc = overload();
oFunc.add( argTypes , func );

oFunc( 引数 );

このとき実行されるのは、overload関数内の無名関数function ( ...arg ){}です。

ここでは、typeofで与えられた引数の型を取得。
大文字に変換して","で接続しています。

この文字列と、add()メソッドで登録した文字列が同じなら、対応する関数を実行します。

一致する文字列がない場合は、setName()メソッドで関数名を登録している場合に限り、次の処理をおこなっています。

(1) thisがプロトタイプチェーンを持っているなら(2)へ
(2) プロトタイプチェーンの中に、さらにプロトタイプチェーンがあるなら(3)へ
(3) プロトタイプチェーンの中のプロトタイプチェーンから、setName()メソッドで登録した名前と同じ関数をたどれるなら、実行する。

これはClassで継承をおこなっているとき、親のメソッドを呼び出すケースを想定しています。

継承をおこなったclassオブジェクトは、つぎのようなイメージになっていて親classで定義したメソッドを直接呼び出しています。

object {
   プロパティ
   ・・・
   <prototype>{ // プロトタイプチェーン
       classで定義したメソッド
       ・・・
        <prototype>{ // プロトタイプチェーン
           親classで定義したメソッド ←これを呼び出す
            ・・・
        }
    }
}

参考記事:【JavaScript】 クラスとは?正体についても調べてみた
参考記事:【JavaScript】 プロトタイプとは?prototypeプロパティはプロトタイプではない件について

 

使い方

overload()関数から返された関数オブジェクトの持っているadd()メソッドで、オーバーロードする関数を登録します。

登録が終わったら、関数オブジェクトを実行します。

const a1 = overload();
a1.add( ["text","number"] , function( ...arg ){ console.log( "f1" , arg )} );
a1.add( ["number","number"] , function( ...arg ){ console.log( "f2" , arg )} );

a1( "aaa" , "bbb" ); // "f1" , Array [ "aaa" , "bbb" ]
a1( 100 , "bbb" );   // "f2" , Array [ 100 , "bbb" ]

次はクラスで継承をおこない、オーバーロードを設定してみます。

class a {
            abc(){
                console.log( "parent class" );
            }
}
class b extends a{
            constructor() {
                super();
            }
}

b.prototype.abc = overload();
b.prototype.abc.setName("abc");
b.prototype.abc.add( ["number","string"],function( ...arg ){ console.log( "f1" , arg )} );
b.prototype.abc.add( ["number","number"],function( ...arg ){ console.log( "f2" , arg )} );
b.prototype.abc.add( ["boolean","number","string"],function( ...arg ){ console.log( "f3" , arg)} );
b.prototype.abc.add( ["boolean","number","string","object"],function( ...arg ){ console.log( "f4" , arg)} );

const c = new b();
c.abc( 1 , 2 );                                              // f2 Array [ 1, 2 ]
c.abc( Number(1) , "abc" );                          // f1 Array [ 1, "abc" ]
c.abc( "xyz" , "abc"  );                               // parent class
c.abc( false , 100.2 , "あいうえお" );               // f3 Array [ false, 100.2, "あいうえお" ]
c.abc( false , 100.2 , "あいうえお" ,{x:10});       // f4 Array [ false, 100.2, "あいうえお", { x:10 } ]

残念ながら、class構文の中でオーバーロードをセットすることができませんでした。
class定義の段階では式を実行できないからです。

 

このコードの問題点

親classと子classで同名の関数をオーバーロードした状況で、引数と実行する関数が一致しない場合、無限ループします。

その他にも問題が盛りだくさん。
汎用的に使用するのは厳しいです…

更新日:2020/06/09

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

スポンサーリンク

記事の内容について

null

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

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

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

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

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

 

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