Як перевантажити функції в javascript?


103

Класичний (не js) підхід до перевантаження:

function myFunc(){
 //code
}

function myFunc(overloaded){
 //other code
}

Javascript не дозволить визначити більше однієї функції з тим самим іменем. Такі речі з'являються:

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

Чи є краще рішення для перевантаження функцій у javascript, крім передачі об'єкта з перевантаженнями в ньому?

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


1
Тоді ви неправильно думаєте про JavaScript; якщо у вас є проблеми з перевантаженнями та сферою застосування, ви, ймовірно, повинні мати іншу назву методу, оскільки це звучить, як це
робиться

@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's $.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 .


Тож я повинен просто дотримуватися пристосованої функції і визнати, що як найкраща практика для реалізації функції фасаду перевантажується?
Travis J

2
@TravisJ - так, адаптована функція - це те, як здійснюються перевантаження в Javascript. Вашим іншим варіантом є використання окремо названої функції з різними аргументами. Якщо дві реалізації не мають нічого спільного, то це є прийнятною практикою. Якщо дві реалізації по суті роблять одне і те ж, але тільки починаючи з різних аргументів, я думаю, що це робить ваш інтерфейс більш компактним для використання адаптивної функції, і це дає вам можливість ділитися кодом між двома пов'язаними реалізаціями.
jfriend00

Додано кілька прикладів коду, пояснених як англійською, так і фактичним кодом.
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;
        }
    }
}

Це хороший приклад альтернативи :)
Travis J

1
Приємно, мені подобається ця 2-а техніка
Нік Роландо

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() Потім перевірте, чи є параметри "не визначено"

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

0

Те, що ви намагаєтеся досягти, найкраще зробити за допомогою змінної локальних аргументів функції .

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

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

Ви можете знайти найкраще пояснення того , як це працює тут .


А як щодо виявлення зіткнень? Це хороший спосіб використання функцій без параметрів або перевірки на наявність, але не дуже найкраща практика, коли справа стосується перевантаження.
Travis J
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.