Как перегружать функции в javascript?

103

Классический (не js) подход к перегрузке:

function myFunc(){
 //code
}

function myFunc(overloaded){
 //other code
}

Javascript не позволяет определять более одной функции с одним и тем же именем. Таким образом, появляются такие вещи:

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

Есть ли лучший способ обхода перегрузки функций в javascript, кроме передачи объекта с перегрузками в нем?

Передача перегрузок может быстро привести к тому, что функция станет слишком многословной, потому что каждая возможная перегрузка потребует условного оператора. Использование функций для выполнения //codeвнутренней части этих условных операторов может вызвать сложные ситуации с областями действия.

Трэвис Дж.
источник
1
Тогда вы неправильно думаете о javascript; если у вас есть проблемы с перегрузками и областью видимости, вам, вероятно, следует использовать другое имя метода, так как похоже, что он выполняет другую работу
Джошкомли, 01
@joshcomley - Перегрузки и область видимости требуют обработки кода. Я просто пытаюсь убедиться, что код, который я использую, максимально эффективен. Обработка областей с помощью обходных путей - это нормально, но я предпочитаю хотя бы попытаться использовать передовые методы.
Travis J

Ответы:

126

Есть несколько аспектов перегрузки аргументов в Javascript:

  1. Переменные аргументы - вы можете передавать разные наборы аргументов (как по типу, так и по количеству), и функция будет вести себя в соответствии с переданными ей аргументами.

  2. Аргументы по умолчанию - вы можете определить значение по умолчанию для аргумента, если оно не передано.

  3. Именованные аргументы - порядок аргументов становится неактуальным, и вы просто называете аргументы, которые хотите передать функции.

Ниже приводится раздел по каждой из этих категорий обработки аргументов.

Переменные аргументы

Поскольку в javascript нет проверки типа аргументов или необходимого количества аргументов, вы можете иметь только одну реализацию, myFunc()которая может адаптироваться к тому, какие аргументы были переданы ему, путем проверки типа, наличия или количества аргументов.

jQuery делает это постоянно. Вы можете сделать некоторые из аргументов необязательными или вы можете выполнить ветвление в своей функции в зависимости от того, какие аргументы ей переданы.

При реализации этих типов перегрузок вы можете использовать несколько различных методов:

  1. Вы можете проверить наличие любого заданного аргумента, проверив, равно ли объявленное значение имени аргумента undefined.
  2. Вы можете проверить общее количество или аргументы с помощью arguments.length.
  3. Вы можете проверить тип любого заданного аргумента.
  4. Для переменного числа аргументов вы можете использовать argumentsпсевдо-массив для доступа к любому заданному аргументу с помощью arguments[i].

Вот некоторые примеры:

Давайте посмотрим на obj.data()метод jQuery . Он поддерживает четыре различных формы использования:

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

Каждый запускает различное поведение, и без использования этой динамической формы перегрузки потребовалось бы четыре отдельные функции.

Вот как можно различить все эти варианты на английском языке, а затем я объединю их все в коде:

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

Если первый аргумент, переданный в, .data()является строкой, а второй аргумент - это undefined, то вызывающий должен использовать эту форму.


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

Если второй аргумент не является неопределенным, установите значение определенного ключа.


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

Если аргументы не переданы, вернуть все ключи / значения в возвращаемом объекте.


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

Если тип первого аргумента - простой объект, установите все ключи / значения из этого объекта.


Вот как вы могли бы объединить все это в один набор логики 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
     }
 },

Ключ к этой технике - убедиться, что все формы аргументов, которые вы хотите принять, однозначно идентифицируемы, и никогда не возникает путаницы в том, какую форму использует вызывающий. Как правило, это требует соответствующего упорядочивания аргументов и обеспечения достаточной уникальности типа и положения аргументов, чтобы вы всегда могли определить, какая форма используется.

Например, если у вас есть функция, которая принимает три строковых аргумента:

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

Вы можете легко сделать третий аргумент необязательным, и вы можете легко обнаружить это условие, но вы не можете сделать только второй аргумент необязательным, потому что вы не можете сказать, какой из них вызывающий имеет намерение передать, потому что нет способа определить, является ли второй аргумент Аргумент должен быть вторым аргументом, или второй аргумент был опущен, поэтому то, что находится в месте второго аргумента, на самом деле является третьим аргументом:

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

Поскольку все три аргумента относятся к одному типу, вы не можете отличить разные аргументы, поэтому не знаете, что имел в виду вызывающий. При таком стиле вызова только третий аргумент может быть необязательным. Если вы хотите опустить второй аргумент, его необходимо передать как null(или какое-либо другое обнаруживаемое значение), и ваш код обнаружит это:

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

Вот пример необязательных аргументов jQuery. оба аргумента являются необязательными и принимают значения по умолчанию, если не переданы:

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

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

Вот пример jQuery, где аргумент может отсутствовать или один из трех разных типов, что дает вам четыре разных перегрузки:

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;
},

Именованные аргументы

Другие языки (например, Python) позволяют передавать именованные аргументы как средство передачи только некоторых аргументов и сделать аргументы независимыми от порядка, в котором они передаются. Javascript не поддерживает напрямую функцию именованных аргументов. Вместо него обычно используется шаблон проектирования, который передает карту свойств / значений. Это можно сделать, передав объект со свойствами и значениями, или в ES6 и выше вы можете передать сам объект Map.

Вот простой пример ES5:

jQuery $.ajax()принимает форму использования, в которой вы просто передаете ему один параметр, который является обычным объектом Javascript со свойствами и значениями. Какие свойства вы ему передаете, определяют, какие аргументы / параметры передаются в вызов ajax. Некоторые могут потребоваться, многие - по желанию. Поскольку они являются свойствами объекта, особого порядка нет. Фактически, этому объекту можно передать более 30 различных свойств, требуется только одно (URL-адрес).

Вот пример:

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

$.ajax()Затем внутри реализации он может просто запросить, какие свойства были переданы входящему объекту, и использовать их в качестве именованных аргументов. Это можно сделать либо с помощью, for (prop in obj)либо путем передачи всех свойств в массив с помощью Object.keys(obj)и затем повторения этого массива.

Этот метод очень часто используется в Javascript, когда есть большое количество аргументов и / или многие аргументы являются необязательными. Примечание: это накладывает бремя на реализующую функцию, чтобы убедиться, что присутствует минимальный допустимый набор аргументов, и дать вызывающей стороне некоторую обратную связь отладки, которая отсутствует, если передано недостаточное количество аргументов (возможно, путем выдачи исключения с полезным сообщением об ошибке) .

В среде ES6 можно использовать деструктурирование для создания свойств / значений по умолчанию для переданного выше объекта. Более подробно это обсуждается в этой справочной статье .

Вот один пример из той статьи:

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

Это создает свойства и значения по умолчанию для start, endи stepсвойства на объект передается в selectEntries()функцию.

Значения по умолчанию для аргументов функции

В ES6 Javascript добавляет встроенную языковую поддержку для значений аргументов по умолчанию.

Например:

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

multiply(5); // 5

Дальнейшее описание способов использования здесь, в MDN .

jfriend00
источник
Так что я должен просто придерживаться адаптируемой функции и принять это как лучшую практику для реализации фасада перегрузки функции?
Travis J
2
@TravisJ - да, адаптируемая функция - это способ перегрузки в Javascript. Другой вариант - использовать отдельно названную функцию с разными аргументами. Если у этих двух реализаций нет ничего общего, то любая из них является приемлемой практикой. Если две реализации по существу делают одно и то же, но только начинают с разных аргументов, то я думаю, что это делает ваш интерфейс более компактным для использования адаптируемой функции и дает вам возможность совместно использовать код между двумя связанными реализациями.
jfriend00 01
Добавлены примеры кода, объясненные как на английском языке, так и в реальном коде.
jfriend00
Когда я пытаюсь это сделать, я получаю сообщение об ошибке, неожиданный токен '=' в последней версии Safari resetProgressBar: function(display_errors, lockout = false).
Ник
@Nick - согласно этой таблице совместимости с ES6 значения параметров по умолчанию должны работать в Safari 10 и новее. На этой странице MDN указано «нет поддержки» в Safari. У меня нет Mac, поэтому я не могу проверить его сам. Как правило, это одна из последних реализованных функций ES6 во многих браузерах. Например, это еще не похоже на iOS9.
jfriend00
33

Перегрузка функции в JavaScript может выполняться разными способами. Все они включают одну главную функцию, которая либо выполняет все процессы, либо делегирует подфункции / процессы.

Один из наиболее распространенных простых методов включает простой переключатель:

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;
    }
}

Более элегантным методом было бы использование массива (или объекта, если вы не делаете перегрузки для каждого количества аргументов):

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

Этот предыдущий пример не очень элегантен, любой может изменить fooArr, и он потерпит неудачу, если кто-то передаст более двух аргументов foo, поэтому лучшей формой было бы использовать шаблон модуля и несколько проверок:

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;
}());

Конечно, ваши перегрузки могут захотеть использовать динамическое количество параметров, поэтому вы можете использовать объект для fnsколлекции.

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;
}());

Мое личное предпочтение, как правило, заключается в том switch, что это делает основную функцию больше. Типичным примером того, где я бы использовал эту технику, был бы метод доступа / мутатора:

function Foo() {} //constructor
Foo.prototype = {
    bar: function (val) {
        switch (arguments.length) {
        case 0:
            return this._bar;
        case 1:
            this._bar = val;
            return this;
        }
    }
}
zzzzBov
источник
Это хороший пример альтернативы :)
Travis J
1
Отлично, мне нравится этот второй прием
Ник Роландо
8

Строго говоря, вы не можете выполнять перегрузку методов. Не так, как это поддерживается в javaили c#.

Проблема в том, что JavaScript изначально НЕ поддерживает перегрузку методов. Итак, если он видит / анализирует две или более функций с одинаковыми именами, он просто учитывает последнюю определенную функцию и перезаписывает предыдущие.

Один из способов, который, на мой взгляд, подходит для большинства случаев, следующий:

Допустим, у вас есть метод

function foo(x)
{
} 

Вместо метода перегрузки, который невозможен в javascript, вы можете определить новый метод

fooNew(x,y,z)
{
}

а затем измените 1-ю функцию следующим образом -

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

Если у вас много таких перегруженных методов, рассмотрите возможность использования switchне только if-elseоператоров.

( подробнее ) PS: Выше ссылка ведет на мой личный блог, в котором есть дополнительная информация по этому поводу.

Аникет Такур
источник
Это не проблема, это особенность.
inetphantom
4

Я использую немного другой подход к перегрузке, основанный на количестве аргументов. Однако я считаю, что подход Джона Фосетта тоже хорош. Вот пример кода, основанного на пояснениях Джона Ресига (автора 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);
            }
        };
    }

юзабилити:

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 :)
Валентин Раск
источник
4

В javascript вы можете реализовать функцию только один раз и вызвать функцию без параметров. myFunc() Затем вы проверяете, является ли параметр undefined.

function myFunc(options){
 if(typeof options != 'undefined'){
  //code
 }
}
Муртновский
источник
3

Я попытался разработать элегантное решение этой проблемы, описанной здесь . А демо можно найти здесь . Использование выглядит так:

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);

Методы, используемые для этого:

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;
};
стамат
источник
1
Очень умная функция для обработки входных данных.
Travis J
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"
Джон Фосетт
источник
3

Нет проблем с перегрузкой в ​​JS, pb как сохранить чистый код при перегрузке функции?

Вы можете использовать форвард, чтобы получить чистый код, основываясь на двух вещах:

  1. Количество аргументов (при вызове функции).
  2. Тип аргументов (при вызове функции)

      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 ....   
Абденнур Туми
источник
2

Поскольку в JavaScript нет параметров перегрузки функций , можно использовать вместо него. Если есть один или два обязательных аргумента, лучше хранить их отдельно от объекта параметров. Вот пример того, как использовать объект параметров и заполненные значения для значения по умолчанию в случае, если значение не было передано в объекте параметров.

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 + "}";

}

вот пример того, как использовать объект параметров

Влад Безден
источник
2

Для этого вам нужно создать функцию, которая добавляет функцию к объекту, а затем она будет выполняться в зависимости от количества аргументов, которые вы отправляете функции:

<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>
Макс Кабрера
источник
0

Мне нравится добавлять подфункции в родительскую функцию, чтобы получить возможность различать группы аргументов для одной и той же функциональности.

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
источник
0

То, что вы пытаетесь достичь, лучше всего делать с помощью переменной локальных аргументов функции .

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

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

Вы можете найти лучшее объяснение того, как это работает, здесь .

Jbarnett
источник
А как насчет обнаружения столкновений? Это хороший способ использовать функции без параметров или проверить их существование, но это не лучший способ, когда дело касается перегрузки.
Travis J