Function overloading in Javascript - Best practices

后端 未结 30 1521
难免孤独
难免孤独 2020-11-22 03:33

What is the best way(s) to fake function overloading in Javascript?

I know it is not possible to overload functions in Javascript as in other languages. If I neede

相关标签:
30条回答
  • 2020-11-22 04:16

    Function Overloading via Dynamic Polymorphism in 100 lines of JS

    • VanillaJS, no external dependencies
    • Full Browser Support - Array.prototype.slice, Object.prototype.toString
    • 1114 bytes uglify'd / 744 bytes g-zipped

    This is from a larger body of code which includes the isFn, isArr, etc. type checking functions. The VanillaJS version below has been reworked to remove all external dependencies, however you will have to define you're own type checking functions for use in the .add() calls.

    Note: This is a self-executing function (so we can have a closure/closed scope), hence the assignment to window.overload rather than function overload() {...}.

    window.overload = function () {
        "use strict"
    
        var a_fnOverloads = [],
            _Object_prototype_toString = Object.prototype.toString
        ;
    
        function isFn(f) {
            return (_Object_prototype_toString.call(f) === '[object Function]');
        } //# isFn
    
        function isObj(o) {
            return !!(o && o === Object(o));
        } //# isObj
    
        function isArr(a) {
            return (_Object_prototype_toString.call(a) === '[object Array]');
        } //# isArr
    
        function mkArr(a) {
            return Array.prototype.slice.call(a);
        } //# mkArr
    
        function fnCall(fn, vContext, vArguments) {
            //# <ES5 Support for array-like objects
            //#     See: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/apply#Browser_compatibility
            vArguments = (isArr(vArguments) ? vArguments : mkArr(vArguments));
    
            if (isFn(fn)) {
                return fn.apply(vContext || this, vArguments);
            }
        } //# fnCall
    
        //# 
        function registerAlias(fnOverload, fn, sAlias) {
            //# 
            if (sAlias && !fnOverload[sAlias]) {
                fnOverload[sAlias] = fn;
            }
        } //# registerAlias
    
        //# 
        function overload(vOptions) {
            var oData = (isFn(vOptions) ?
                    { default: vOptions } :
                    (isObj(vOptions) ?
                        vOptions :
                        {
                            default: function (/*arguments*/) {
                                throw "Overload not found for arguments: [" + mkArr(arguments) + "]";
                            }
                        }
                    )
                ),
                fnOverload = function (/*arguments*/) {
                    var oEntry, i, j,
                        a = arguments,
                        oArgumentTests = oData[a.length] || []
                    ;
    
                    //# Traverse the oArgumentTests for the number of passed a(rguments), defaulting the oEntry at the beginning of each loop
                    for (i = 0; i < oArgumentTests.length; i++) {
                        oEntry = oArgumentTests[i];
    
                        //# Traverse the passed a(rguments), if a .test for the current oArgumentTests fails, reset oEntry and fall from the a(rgument)s loop
                        for (j = 0; j < a.length; j++) {
                            if (!oArgumentTests[i].tests[j](a[j])) {
                                oEntry = undefined;
                                break;
                            }
                        }
    
                        //# If all of the a(rgument)s passed the .tests we found our oEntry, so break from the oArgumentTests loop
                        if (oEntry) {
                            break;
                        }
                    }
    
                    //# If we found our oEntry above, .fn.call its .fn
                    if (oEntry) {
                        oEntry.calls++;
                        return fnCall(oEntry.fn, this, a);
                    }
                    //# Else we were unable to find a matching oArgumentTests oEntry, so .fn.call our .default
                    else {
                        return fnCall(oData.default, this, a);
                    }
                } //# fnOverload
            ;
    
            //# 
            fnOverload.add = function (fn, a_vArgumentTests, sAlias) {
                var i,
                    bValid = isFn(fn),
                    iLen = (isArr(a_vArgumentTests) ? a_vArgumentTests.length : 0)
                ;
    
                //# 
                if (bValid) {
                    //# Traverse the a_vArgumentTests, processinge each to ensure they are functions (or references to )
                    for (i = 0; i < iLen; i++) {
                        if (!isFn(a_vArgumentTests[i])) {
                            bValid = _false;
                        }
                    }
                }
    
                //# If the a_vArgumentTests are bValid, set the info into oData under the a_vArgumentTests's iLen
                if (bValid) {
                    oData[iLen] = oData[iLen] || [];
                    oData[iLen].push({
                        fn: fn,
                        tests: a_vArgumentTests,
                        calls: 0
                    });
    
                    //# 
                    registerAlias(fnOverload, fn, sAlias);
    
                    return fnOverload;
                }
                //# Else one of the passed arguments was not bValid, so throw the error
                else {
                    throw "poly.overload: All tests must be functions or strings referencing `is.*`.";
                }
            }; //# overload*.add
    
            //# 
            fnOverload.list = function (iArgumentCount) {
                return (arguments.length > 0 ? oData[iArgumentCount] || [] : oData);
            }; //# overload*.list
    
            //# 
            a_fnOverloads.push(fnOverload);
            registerAlias(fnOverload, oData.default, "default");
    
            return fnOverload;
        } //# overload
    
        //# 
        overload.is = function (fnTarget) {
            return (a_fnOverloads.indexOf(fnTarget) > -1);
        } //# overload.is
    
        return overload;
    }();
    

    Usage:

    The caller defines their overloaded functions by assigning a variable to the return of overload(). Thanks to chaining, the additional overloads can be defined in series:

    var myOverloadedFn = overload(function(){ console.log("default", arguments) })
        .add(function(){ console.log("noArgs", arguments) }, [], "noArgs")
        .add(function(){ console.log("str", arguments) }, [function(s){ return typeof s === 'string' }], "str")
    ;
    

    The single optional argument to overload() defines the "default" function to call if the signature cannot be identified. The arguments to .add() are:

    1. fn: function defining the overload;
    2. a_vArgumentTests: Array of functions defining the tests to run on the arguments. Each function accepts a single argument and returns truethy based on if the argument is valid;
    3. sAlias (Optional): string defining the alias to directly access the overload function (fn), e.g. myOverloadedFn.noArgs() will call that function directly, avoiding the dynamic polymorphism tests of the arguments.

    This implementation actually allows for more than just traditional function overloads as the second a_vArgumentTests argument to .add() in practice defines custom types. So, you could gate arguments not only based on type, but on ranges, values or collections of values!

    If you look through the 145 lines of code for overload() you'll see that each signature is categorized by the number of arguments passed to it. This is done so that we're limiting the number of tests we are running. I also keep track of a call count. With some additional code, the arrays of overloaded functions could be re-sorted so that more commonly called functions are tested first, again adding some measure of performance enhancement.

    Now, there are some caveats... As Javascript is loosely typed, you will have to be careful with your vArgumentTests as an integer could be validated as a float, etc.

    JSCompress.com version (1114 bytes, 744 bytes g-zipped):

    window.overload=function(){'use strict';function b(n){return'[object Function]'===m.call(n)}function c(n){return!!(n&&n===Object(n))}function d(n){return'[object Array]'===m.call(n)}function e(n){return Array.prototype.slice.call(n)}function g(n,p,q){if(q=d(q)?q:e(q),b(n))return n.apply(p||this,q)}function h(n,p,q){q&&!n[q]&&(n[q]=p)}function k(n){var p=b(n)?{default:n}:c(n)?n:{default:function(){throw'Overload not found for arguments: ['+e(arguments)+']'}},q=function(){var r,s,t,u=arguments,v=p[u.length]||[];for(s=0;s<v.length;s++){for(r=v[s],t=0;t<u.length;t++)if(!v[s].tests[t](u[t])){r=void 0;break}if(r)break}return r?(r.calls++,g(r.fn,this,u)):g(p.default,this,u)};return q.add=function(r,s,t){var u,v=b(r),w=d(s)?s.length:0;if(v)for(u=0;u<w;u++)b(s[u])||(v=_false);if(v)return p[w]=p[w]||[],p[w].push({fn:r,tests:s,calls:0}),h(q,r,t),q;throw'poly.overload: All tests must be functions or strings referencing `is.*`.'},q.list=function(r){return 0<arguments.length?p[r]||[]:p},l.push(q),h(q,p.default,'default'),q}var l=[],m=Object.prototype.toString;return k.is=function(n){return-1<l.indexOf(n)},k}();
    
    0 讨论(0)
  • 2020-11-22 04:16

    While Default parameters is not overloading, it might solve some of the issues that developers face in this area. The input is strictly decided by the order, you can not re-order as you wish as in classical overloading:

    function transformer(
        firstNumber = 1,
        secondNumber = new Date().getFullYear(),
        transform = function multiply(firstNumber, secondNumber) {
            return firstNumber * secondNumber;
        }
    ) {
        return transform(firstNumber, secondNumber);
    }
    
    console.info(transformer());
    console.info(transformer(8));
    console.info(transformer(2, 6));
    console.info(transformer(undefined, 65));
    
    function add(firstNumber, secondNumber) {
        return firstNumber + secondNumber;
    }
    console.info(transformer(undefined, undefined, add));
    console.info(transformer(3, undefined, add));
    

    Results in (for year 2020):

    2020
    16160
    12
    65
    2021
    2023
    

    More info: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Default_parameters

    0 讨论(0)
  • 2020-11-22 04:16

    there is no actual overloading in JS, anyway we still can simulate method overloading in several ways:

    method #1: use object

    function test(x,options){
      if("a" in options)doSomething();
      else if("b" in options)doSomethingElse();
    }
    test("ok",{a:1});
    test("ok",{b:"string"});
    

    method #2: use rest (spread) parameters

    function test(x,...p){
     if(p[2])console.log("3 params passed"); //or if(typeof p[2]=="string")
    else if (p[1])console.log("2 params passed");
    else console.log("1 param passed");
    }
    

    method #3: use undefined

    function test(x, y, z){
     if(typeof(z)=="undefined")doSomething();
    }
    

    method #4: type checking

    function test(x){
     if(typeof(x)=="string")console.log("a string passed")
     else ...
    }
    
    0 讨论(0)
  • 2020-11-22 04:18

    JavaScript is untyped language, and I only think that makes sense to overload a method/function with regards to the number of params. Hence, I would recommend to check if the parameter has been defined:

    myFunction = function(a, b, c) {
         if (b === undefined && c === undefined ){
              // do x...
         }
         else {
              // do y...
         }
    };
    
    0 讨论(0)
  • 2020-11-22 04:19

    #Forwarding Pattern => the best practice on JS overloading Forward to another function which name is built from the 3rd & 4th points :

    1. Using number of arguments
    2. Checking types of arguments
    window['foo_'+arguments.length+'_'+Array.from(arguments).map((arg)=>typeof arg).join('_')](...arguments)
    

    #Application on your case :

     function foo(...args){
              return window['foo_' + args.length+'_'+Array.from(args).map((arg)=>typeof arg).join('_')](...args);
    
      }
       //------Assuming that `x` , `y` and `z` are String when calling `foo` . 
      
      /**-- for :  foo(x)*/
      function foo_1_string(){
      }
      /**-- for : foo(x,y,z) ---*/
      function foo_3_string_string_string(){
          
      }
    

    #Other Complex Sample :

          function foo(...args){
              return window['foo_'+args.length+'_'+Array.from(args).map((arg)=>typeof arg).join('_')](...args);
           }
    
            /** one argument & this argument is string */
          function foo_1_string(){
    
          }
           //------------
           /** one argument & this argument is object */
          function foo_1_object(){
    
          }
          //----------
          /** two arguments & those arguments are both string */
          function foo_2_string_string(){
    
          }
           //--------
          /** Three arguments & those arguments are : id(number),name(string), callback(function) */
          function foo_3_number_string_function(){
                    let args=arguments;
                      new Person(args[0],args[1]).onReady(args[3]);
          }
         
           //--- And so on ....   
    
    0 讨论(0)
  • 2020-11-22 04:20

    I'm not sure about best practice, but here is how I do it:

    /*
     * Object Constructor
     */
    var foo = function(x) {
        this.x = x;
    };
    
    /*
     * Object Protoype
     */
    foo.prototype = {
        /*
         * f is the name that is going to be used to call the various overloaded versions
         */
        f: function() {
    
            /*
             * Save 'this' in order to use it inside the overloaded functions
             * because there 'this' has a different meaning.
             */   
            var that = this;  
    
            /* 
             * Define three overloaded functions
             */
            var f1 = function(arg1) {
                console.log("f1 called with " + arg1);
                return arg1 + that.x;
            }
    
            var f2 = function(arg1, arg2) {
                 console.log("f2 called with " + arg1 + " and " + arg2);
                 return arg1 + arg2 + that.x;
            }
    
            var f3 = function(arg1) {
                 console.log("f3 called with [" + arg1[0] + ", " + arg1[1] + "]");
                 return arg1[0] + arg1[1];
            }
    
            /*
             * Use the arguments array-like object to decide which function to execute when calling f(...)
             */
            if (arguments.length === 1 && !Array.isArray(arguments[0])) {
                return f1(arguments[0]);
            } else if (arguments.length === 2) {
                return f2(arguments[0], arguments[1]);
            } else if (arguments.length === 1 && Array.isArray(arguments[0])) {
                return f3(arguments[0]);
            }
        } 
    }
    
    /* 
     * Instantiate an object
     */
    var obj = new foo("z");
    
    /*
     * Call the overloaded functions using f(...)
     */
    console.log(obj.f("x"));         // executes f1, returns "xz"
    console.log(obj.f("x", "y"));    // executes f2, returns "xyz"
    console.log(obj.f(["x", "y"]));  // executes f3, returns "xy"
    
    0 讨论(0)
提交回复
热议问题