Como sobrecarregar funções em javascript?

103

Abordagem clássica (não js) para sobrecarregar:

function myFunc(){
 //code
}

function myFunc(overloaded){
 //other code
}

Javascript não permite que mais de uma função seja definida com o mesmo nome. Como tal, coisas como esta aparecem:

function myFunc(options){
 if(options["overloaded"]){
  //code
 }
}

Existe uma solução melhor para sobrecarga de função em javascript do que passar um objeto com as sobrecargas nele?

Transmitir sobrecargas pode rapidamente fazer com que uma função se torne muito detalhada, pois cada sobrecarga possível precisaria de uma instrução condicional. Usar funções para realizar o //codeinterior dessas instruções condicionais pode causar situações complicadas com escopos.

Travis J
fonte
1
Então você está pensando em javascript errado; se você tiver problemas com sobrecargas e escopo, provavelmente deve ter um nome de método diferente, pois parece que ele está fazendo outro trabalho
joshcomley 01 de
@joshcomley - Sobrecargas e escopo requerem código para manipular. Estou apenas tentando garantir que o código que uso seja o mais eficiente possível. Lidar com escopos com soluções alternativas é bom, mas eu prefiro pelo menos tentar usar os métodos de práticas recomendadas.
Travis J
2
possível duplicata de sobrecarga
cdeszaq

Respostas:

126

Existem vários aspectos para sobrecarga de argumento em Javascript:

  1. Argumentos variáveis - você pode passar diferentes conjuntos de argumentos (em tipo e quantidade) e a função se comportará de uma maneira que corresponda aos argumentos passados ​​a ela.

  2. Argumentos padrão - você pode definir um valor padrão para um argumento se ele não for passado.

  3. Argumentos nomeados - a ordem dos argumentos torna-se irrelevante e você apenas nomeia quais argumentos deseja passar para a função.

Abaixo está uma seção sobre cada uma dessas categorias de tratamento de argumentos.

Argumentos Variáveis

Como o javascript não tem verificação de tipo em argumentos ou quantidade necessária de argumentos, você pode ter apenas uma implementação myFunc()que pode se adaptar aos argumentos que foram passados ​​a ele verificando o tipo, presença ou quantidade de argumentos.

jQuery faz isso o tempo todo. Você pode tornar alguns dos argumentos opcionais ou pode ramificar em sua função, dependendo de quais argumentos são passados ​​para ela.

Ao implementar esses tipos de sobrecargas, você tem várias técnicas diferentes que pode usar:

  1. Você pode verificar a presença de qualquer argumento, verificando se o valor do nome do argumento declarado é undefined.
  2. Você pode verificar a quantidade total ou argumentos com arguments.length.
  3. Você pode verificar o tipo de qualquer argumento.
  4. Para números variáveis ​​de argumentos, você pode usar a argumentspseudo-matriz para acessar qualquer argumento com arguments[i].

aqui estão alguns exemplos:

Vejamos o obj.data()método jQuery . Ele suporta quatro formas diferentes de uso:

obj.data("key");
obj.data("key", value);
obj.data();
obj.data(object);

Cada um aciona um comportamento diferente e, sem usar essa forma dinâmica de sobrecarga, exigiria quatro funções distintas.

Veja como se pode discernir entre todas essas opções em inglês e, em seguida, combinarei todas no código:

// get the data element associated with a particular key value
obj.data("key");

Se o primeiro argumento passado para .data()for uma string e o segundo argumento for undefined, então o chamador deve usar este formulário.


// set the value associated with a particular key
obj.data("key", value);

Se o segundo argumento não for indefinido, defina o valor de uma chave específica.


// get all keys/values
obj.data();

Se nenhum argumento for passado, retorne todas as chaves / valores em um objeto retornado.


// set all keys/values from the passed in object
obj.data(object);

Se o tipo do primeiro argumento for um objeto simples, defina todas as chaves / valores desse objeto.


Veja como você pode combinar tudo isso em um conjunto de lógica javascript:

 // method declaration for .data()
 data: function(key, value) {
     if (arguments.length === 0) {
         // .data()
         // no args passed, return all keys/values in an object
     } else if (typeof key === "string") {
         // first arg is a string, look at type of second arg
         if (typeof value !== "undefined") {
             // .data("key", value)
             // set the value for a particular key
         } else {
             // .data("key")
             // retrieve a value for a key
         }
     } else if (typeof key === "object") {
         // .data(object)
         // set all key/value pairs from this object
     } else {
         // unsupported arguments passed
     }
 },

A chave para essa técnica é certificar-se de que todas as formas de argumentos que você deseja aceitar sejam identificáveis ​​de forma única e que nunca haja confusão sobre qual formulário o chamador está usando. Isso geralmente requer ordenar os argumentos de forma adequada e certificar-se de que haja exclusividade suficiente no tipo e na posição dos argumentos para que você sempre possa saber qual forma está sendo usada.

Por exemplo, se você tem uma função que recebe três argumentos de string:

obj.query("firstArg", "secondArg", "thirdArg");

Você pode facilmente tornar o terceiro argumento opcional e detectar facilmente essa condição, mas você não pode tornar apenas o segundo argumento opcional porque você não pode dizer qual deles o chamador pretende passar porque não há como identificar se o segundo argumento deve ser o segundo argumento ou o segundo argumento foi omitido, então o que está no lugar do segundo argumento é na verdade o terceiro argumento:

obj.query("firstArg", "secondArg");
obj.query("firstArg", "thirdArg");

Como todos os três argumentos são do mesmo tipo, você não pode dizer a diferença entre os diferentes argumentos, portanto não sabe o que o chamador pretendia. Com este estilo de chamada, apenas o terceiro argumento pode ser opcional. Se você quisesse omitir o segundo argumento, ele teria que ser passado como null(ou algum outro valor detectável) e seu código detectaria que:

obj.query("firstArg", null, "thirdArg");

Aqui está um exemplo jQuery de argumentos opcionais. ambos os argumentos são opcionais e assumem os valores padrão se não forem passados:

clone: function( dataAndEvents, deepDataAndEvents ) {
    dataAndEvents = dataAndEvents == null ? false : dataAndEvents;
    deepDataAndEvents = deepDataAndEvents == null ? dataAndEvents : deepDataAndEvents;

    return this.map( function () {
        return jQuery.clone( this, dataAndEvents, deepDataAndEvents );
    });
},

Aqui está um exemplo de jQuery em que o argumento pode estar ausente ou qualquer um dos três tipos diferentes que fornecem quatro sobrecargas diferentes:

html: function( value ) {
    if ( value === undefined ) {
        return this[0] && this[0].nodeType === 1 ?
            this[0].innerHTML.replace(rinlinejQuery, "") :
            null;

    // See if we can take a shortcut and just use innerHTML
    } else if ( typeof value === "string" && !rnoInnerhtml.test( value ) &&
        (jQuery.support.leadingWhitespace || !rleadingWhitespace.test( value )) &&
        !wrapMap[ (rtagName.exec( value ) || ["", ""])[1].toLowerCase() ] ) {

        value = value.replace(rxhtmlTag, "<$1></$2>");

        try {
            for ( var i = 0, l = this.length; i < l; i++ ) {
                // Remove element nodes and prevent memory leaks
                if ( this[i].nodeType === 1 ) {
                    jQuery.cleanData( this[i].getElementsByTagName("*") );
                    this[i].innerHTML = value;
                }
            }

        // If using innerHTML throws an exception, use the fallback method
        } catch(e) {
            this.empty().append( value );
        }

    } else if ( jQuery.isFunction( value ) ) {
        this.each(function(i){
            var self = jQuery( this );

            self.html( value.call(this, i, self.html()) );
        });

    } else {
        this.empty().append( value );
    }

    return this;
},

Argumentos Nomeados

Outras linguagens (como Python) permitem que se passe argumentos nomeados como um meio de passar apenas alguns argumentos e tornar os argumentos independentes da ordem em que são passados. Javascript não suporta diretamente o recurso de argumentos nomeados. Um padrão de design comumente usado em seu lugar é passar um mapa de propriedades / valores. Isso pode ser feito passando um objeto com propriedades e valores ou no ES6 e acima, você pode realmente passar um objeto Mapa em si.

Aqui está um exemplo ES5 simples:

jQuery's $.ajax() aceita uma forma de uso em que você apenas passa um único parâmetro que é um objeto Javascript regular com propriedades e valores. Quais propriedades você passa determinam quais argumentos / opções estão sendo passados ​​para a chamada ajax. Alguns podem ser obrigatórios, muitos são opcionais. Como são propriedades de um objeto, não existe uma ordem específica. Na verdade, existem mais de 30 propriedades diferentes que podem ser passadas para aquele objeto, apenas uma (o url) é necessária.

Aqui está um exemplo:

$.ajax({url: "http://www.example.com/somepath", data: myArgs, dataType: "json"}).then(function(result) {
    // process result here
});

Dentro da $.ajax()implementação, ele pode apenas interrogar quais propriedades foram passadas no objeto de entrada e usá-las como argumentos nomeados. Isso pode ser feito com for (prop in obj)ou obtendo todas as propriedades em uma matriz Object.keys(obj)e, em seguida, iterando essa matriz.

Esta técnica é usada muito comumente em Javascript quando há um grande número de argumentos e / ou muitos argumentos são opcionais. Observação: isso coloca um ônus sobre a função de implementação para garantir que um conjunto mínimo válido de argumentos esteja presente e dar ao chamador algum feedback de depuração o que está faltando se argumentos insuficientes forem passados ​​(provavelmente lançando uma exceção com uma mensagem de erro útil) .

Em um ambiente ES6, é possível usar a desestruturação para criar propriedades / valores padrão para o objeto passado acima. Isso é discutido com mais detalhes neste artigo de referência .

Aqui está um exemplo desse artigo:

function selectEntries({ start=0, end=-1, step=1 } = {}) {
    ···
};

Isto cria propriedades padrão e os valores para a start, ende stepas propriedades de um objecto transmitido para a selectEntries()função.

Valores padrão para argumentos de função

No ES6, o Javascript adiciona suporte de idioma integrado para valores padrão de argumentos.

Por exemplo:

function multiply(a, b = 1) {
  return a*b;
}

multiply(5); // 5

Uma descrição mais detalhada das maneiras como isso pode ser usado aqui no MDN .

jfriend00
fonte
Portanto, devo apenas ficar com uma função adaptável e aceitar isso como a melhor prática para implementar a fachada de sobrecarga de função?
Travis J
2
@TravisJ - sim, função adaptável é a forma como as sobrecargas são feitas em Javascript. Sua outra opção é usar uma função nomeada separadamente com argumentos diferentes. Se as duas implementações não tiverem nada em comum, então qualquer uma delas é uma prática aceitável. Se as duas implementações estão essencialmente fazendo a mesma coisa, mas apenas começando com argumentos diferentes, então acho que torna sua interface mais compacta para usar a função adaptável e dá a você a oportunidade de compartilhar código entre as duas implementações relacionadas.
jfriend00
Adicionados alguns exemplos de código, explicados em inglês e no código real.
jfriend00
Estou recebendo um erro, token inesperado '=' na versão mais recente do Safari quando tento fazer isso resetProgressBar: function(display_errors, lockout = false).
Nick
@Nick - De acordo com esta tabela de compatibilidade ES6 , os valores de parâmetro padrão devem funcionar no Safari 10 e posterior. Por esta página MDN , diz "sem suporte" no Safari. Não tenho um Mac, então não posso testá-lo sozinho. Geralmente, parece ser um dos recursos ES6 implementados posteriormente em muitos navegadores. Por exemplo, ainda não parece que está no iOS9.
jfriend00
33

Sobrecarregar uma função em JavaScript pode ser feito de várias maneiras. Todos eles envolvem uma única função mestre que executa todos os processos ou delega a subfunções / processos.

Uma das técnicas simples mais comuns envolve uma troca simples:

function foo(a, b) {
    switch (arguments.length) {
    case 0:
        //do basic code
        break;
    case 1:
        //do code with `a`
        break;
    case 2:
    default:
        //do code with `a` & `b`
        break;
    }
}

Uma técnica mais elegante seria usar uma matriz (ou objeto, se você não estiver fazendo sobrecargas para cada contagem de argumento):

fooArr = [
    function () {
    },
    function (a) {
    },
    function (a,b) {
    }
];
function foo(a, b) {
    return fooArr[arguments.length](a, b);
}

O exemplo anterior não é muito elegante, qualquer pessoa pode modificar fooArre falhará se alguém passar mais de 2 argumentos para foo, então uma forma melhor seria usar um padrão de módulo e algumas verificações:

var foo = (function () {
    var fns;
    fns = [
        function () {
        },
        function (a) {
        },
        function (a, b) {
        }
    ];
    function foo(a, b) {
        var fnIndex;
        fnIndex = arguments.length;
        if (fnIndex > foo.length) {
            fnIndex = foo.length;
        }
        return fns[fnIndex].call(this, a, b);
    }
    return foo;
}());

Claro que suas sobrecargas podem querer usar um número dinâmico de parâmetros, então você pode usar um objeto para a fnscoleção.

var foo = (function () {
    var fns;
    fns = {};
    fns[0] = function () {
    };
    fns[1] = function (a) {
    };
    fns[2] = function (a, b) {
    };
    fns.params = function (a, b /*, params */) {
    };
    function foo(a, b) {
        var fnIndex;
        fnIndex = arguments.length;
        if (fnIndex > foo.length) {
            fnIndex = 'params';
        }
        return fns[fnIndex].apply(this, Array.prototype.slice.call(arguments));
    }
    return foo;
}());

Minha preferência pessoal tende a ser switch, embora aumente a função mestre. Um exemplo comum de onde eu usaria essa técnica seria um método acessador / modificador:

function Foo() {} //constructor
Foo.prototype = {
    bar: function (val) {
        switch (arguments.length) {
        case 0:
            return this._bar;
        case 1:
            this._bar = val;
            return this;
        }
    }
}
zzzzBov
fonte
Este é um bom exemplo de alternativa :)
Travis J
1
Legal, eu gosto dessa 2ª técnica
Nick Rolando
8

Você não pode sobrecarregar o método em sentido estrito. Não gosto da forma como é suportado em javaou c#.

O problema é que o JavaScript NÃO oferece suporte nativo à sobrecarga de método. Portanto, se ele vir / analisar duas ou mais funções com os mesmos nomes, ele apenas considerará a última função definida e sobrescreverá as anteriores.

Uma das maneiras que considero adequada para a maioria dos casos é a seguinte -

Vamos dizer que você tem método

function foo(x)
{
} 

Em vez de sobrecarregar o método, o que não é possível em javascript, você pode definir um novo método

fooNew(x,y,z)
{
}

e, em seguida, modifique a 1ª função da seguinte forma -

function foo(x)
{
  if(arguments.length==2)
  {
     return fooNew(arguments[0],  arguments[1]);
  }
} 

Se você tiver muitos desses métodos sobrecarregados, considere usar switchnão apenas if-elseinstruções.

( mais detalhes ) PS: O link acima leva ao meu blog pessoal que tem detalhes adicionais sobre isso.

Aniket Thakur
fonte
Isso não é um problema, é um recurso.
inetphantom
4

Estou usando uma abordagem de sobrecarga um pouco diferente com base no número de argumentos. No entanto, acredito que a abordagem de John Fawcett também é boa. Aqui está o exemplo, código baseado nas explicações de John Resig (autor do jQuery).

// o = existing object, n = function name, f = function.
    function overload(o, n, f){
        var old = o[n];
        o[n] = function(){
            if(f.length == arguments.length){
                return f.apply(this, arguments);
            }
            else if(typeof o == 'function'){
                return old.apply(this, arguments);
            }
        };
    }

usabilidade:

var obj = {};
overload(obj, 'function_name', function(){ /* what we will do if no args passed? */});
overload(obj, 'function_name', function(first){ /* what we will do if 1 arg passed? */});
overload(obj, 'function_name', function(first, second){ /* what we will do if 2 args passed? */});
overload(obj, 'function_name', function(first,second,third){ /* what we will do if 3 args passed? */});
//... etc :)
Valentin Rusk
fonte
4

Em javascript você pode implementar a função apenas uma vez e invocar a função sem os parâmetros myFunc() Você então verifica se as opções estão 'indefinidas'

function myFunc(options){
 if(typeof options != 'undefined'){
  //code
 }
}
Murtnowski
fonte
3

Tentei desenvolver uma solução elegante para este problema descrito aqui . E você pode encontrar a demonstração aqui . O uso é parecido com este:

var out = def({
    'int': function(a) {
        alert('Here is int '+a);
    },

    'float': function(a) {
        alert('Here is float '+a);
    },

    'string': function(a) {
        alert('Here is string '+a);
    },

    'int,string': function(a, b) {
        alert('Here is an int '+a+' and a string '+b);
    },
    'default': function(obj) {
        alert('Here is some other value '+ obj);
    }

});

out('ten');
out(1);
out(2, 'robot');
out(2.5);
out(true);

Os métodos usados ​​para conseguir isso:

var def = function(functions, parent) {
 return function() {
    var types = [];
    var args = [];
    eachArg(arguments, function(i, elem) {
        args.push(elem);
        types.push(whatis(elem));
    });
    if(functions.hasOwnProperty(types.join())) {
        return functions[types.join()].apply(parent, args);
    } else {
        if (typeof functions === 'function')
            return functions.apply(parent, args);
        if (functions.hasOwnProperty('default'))
            return functions['default'].apply(parent, args);        
    }
  };
};

var eachArg = function(args, fn) {
 var i = 0;
 while (args.hasOwnProperty(i)) {
    if(fn !== undefined)
        fn(i, args[i]);
    i++;
 }
 return i-1;
};

var whatis = function(val) {

 if(val === undefined)
    return 'undefined';
 if(val === null)
    return 'null';

 var type = typeof val;

 if(type === 'object') {
    if(val.hasOwnProperty('length') && val.hasOwnProperty('push'))
        return 'array';
    if(val.hasOwnProperty('getDate') && val.hasOwnProperty('toLocaleTimeString'))
        return 'date';
    if(val.hasOwnProperty('toExponential'))
        type = 'number';
    if(val.hasOwnProperty('substring') && val.hasOwnProperty('length'))
        return 'string';
 }

 if(type === 'number') {
    if(val.toString().indexOf('.') > 0)
        return 'float';
    else
        return 'int';
 }

 return type;
};
Stamat
fonte
1
Uma função para lidar com as entradas, muito inteligente.
Travis J de
3

https://github.com/jrf0110/leFunc

var getItems = leFunc({
  "string": function(id){
    // Do something
  },
  "string,object": function(id, options){
    // Do something else
  },
  "string,object,function": function(id, options, callback){
    // Do something different
    callback();
  },
  "object,string,function": function(options, message, callback){
    // Do something ca-raaaaazzzy
    callback();
  }
});

getItems("123abc"); // Calls the first function - "string"
getItems("123abc", {poop: true}); // Calls the second function - "string,object"
getItems("123abc", {butt: true}, function(){}); // Calls the third function - "string,object,function"
getItems({butt: true}, "What what?" function(){}); // Calls the fourth function - "object,string,function"
John Fawcett
fonte
3

Nenhum problema com sobrecarga em JS, O pb como manter um código limpo ao sobrecarregar a função?

Você pode usar um encaminhamento para ter um código limpo, com base em duas coisas:

  1. Número de argumentos (ao chamar a função).
  2. Tipo de argumento (ao chamar a função)

      function myFunc(){
          return window['myFunc_'+arguments.length+Array.from(arguments).map((arg)=>typeof arg).join('_')](...arguments);
       }
    
        /** one argument & this argument is string */
      function myFunc_1_string(){
    
      }
       //------------
       /** one argument & this argument is object */
      function myFunc_1_object(){
    
      }
      //----------
      /** two arguments & those arguments are both string */
      function myFunc_2_string_string(){
    
      }
       //--------
      /** Three arguments & those arguments are : id(number),name(string), callback(function) */
      function myFunc_3_number_string_function(){
                let args=arguments;
                  new Person(args[0],args[1]).onReady(args[3]);
      }
    
       //--- And so on ....   
Abdennour TOUMI
fonte
2

Como o JavaScript não tem opções de sobrecarga de função , o objeto pode ser usado. Se houver um ou dois argumentos necessários, é melhor mantê-los separados do objeto de opções. Aqui está um exemplo de como usar o objeto de opções e os valores preenchidos para o valor padrão, caso o valor não tenha sido passado no objeto de opções.

function optionsObjectTest(x, y, opts) {
    opts = opts || {}; // default to an empty options object

    var stringValue = opts.stringValue || "string default value";
    var boolValue = !!opts.boolValue; // coerces value to boolean with a double negation pattern
    var numericValue = opts.numericValue === undefined ? 123 : opts.numericValue;

    return "{x:" + x + ", y:" + y + ", stringValue:'" + stringValue + "', boolValue:" + boolValue + ", numericValue:" + numericValue + "}";

}

aqui está um exemplo de como usar o objeto de opções

Vlad Bezden
fonte
2

Para isso, você precisa criar uma função que adiciona a função a um objeto, então ela será executada dependendo da quantidade de argumentos que você enviar para a função:

<script > 
//Main function to add the methods
function addMethod(object, name, fn) {
  var old = object[name];
  object[name] = function(){
    if (fn.length == arguments.length)
      return fn.apply(this, arguments)
    else if (typeof old == 'function')
      return old.apply(this, arguments);
  };
}


  var ninjas = {
   values: ["Dean Edwards", "Sam Stephenson", "Alex Russell"]
};

//Here we declare the first function with no arguments passed
  addMethod(ninjas, "find", function(){
    return this.values;
});

//Second function with one argument
  addMethod(ninjas, "find", function(name){
    var ret = [];
    for (var i = 0; i < this.values.length; i++)
      if (this.values[i].indexOf(name) == 0)
        ret.push(this.values[i]);
    return ret;
  });

//Third function with two arguments
  addMethod(ninjas, "find", function(first, last){
    var ret = [];
    for (var i = 0; i < this.values.length; i++)
      if (this.values[i] == (first + " " + last))
        ret.push(this.values[i]);
    return ret;
  });


//Now you can do:
ninjas.find();
ninjas.find("Sam");
ninjas.find("Dean", "Edwards")
</script>
Max Cabrera
fonte
0

Gosto de adicionar subfunções dentro de uma função pai para conseguir a capacidade de diferenciar grupos de argumentos para a mesma funcionalidade.

var doSomething = function() {
    var foo;
    var bar;
};

doSomething.withArgSet1 = function(arg0, arg1) {
    var obj = new doSomething();
    // do something the first way
    return obj;
};

doSomething.withArgSet2 = function(arg2, arg3) {
    var obj = new doSomething();
    // do something the second way
    return obj;
};
km6zla
fonte
0

O que você está tentando alcançar é melhor feito usando a variável de argumentos locais da função .

function foo() {
    if (arguments.length === 0) {
        //do something
    }
    if (arguments.length === 1) {
        //do something else
    }
}

foo(); //do something
foo('one'); //do something else

Você pode encontrar uma explicação melhor de como isso funciona aqui .

jbarnett
fonte
E quanto à detecção de colisão? Esta é uma boa maneira de usar funções sem parâmetros ou de verificar a existência, mas não é realmente a melhor prática quando se trata de sobrecarga.
Travis J