Перегрузка функций в Javascript - лучшие практики - PullRequest
711 голосов
/ 19 января 2009

Как лучше всего подделать перегрузку функций в Javascript?

Я знаю, что невозможно перегрузить функции в Javascript, как в других языках. Если мне нужна была функция с двумя вариантами использования foo(x) и foo(x,y,z), которая является наилучшей / предпочтительной:

  1. Во-первых, используя разные имена
  2. Использование необязательных аргументов, таких как y = y || 'default'
  3. Использование количества аргументов
  4. Проверка типов аргументов
  5. или как?

Ответы [ 33 ]

545 голосов
/ 19 января 2009

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

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

function foo(a, b, opts) {
  // ...
  if (opts['test']) { } //if test param exists, do something.. 
}


foo(1, 2, {"method":"add"});
foo(3, 4, {"test":"equals", "bar":"tree"});

Тогда вы можете справиться с этим так, как хотите в своем методе. [Переключение, если-еще, и т. Д.]

154 голосов
/ 15 февраля 2012

Я часто так делаю:

C #:

public string CatStrings(string p1)                  {return p1;}
public string CatStrings(string p1, int p2)          {return p1+p2.ToString();}
public string CatStrings(string p1, int p2, bool p3) {return p1+p2.ToString()+p3.ToString();}

CatStrings("one");        // result = one
CatStrings("one",2);      // result = one2
CatStrings("one",2,true); // result = one2true

Эквивалент JavaScript:

function CatStrings(p1, p2, p3)
{
  var s = p1;
  if(typeof p2 !== "undefined") {s += p2;}
  if(typeof p3 !== "undefined") {s += p3;}
  return s;
};

CatStrings("one");        // result = one
CatStrings("one",2);      // result = one2
CatStrings("one",2,true); // result = one2true

Этот конкретный пример на самом деле более элегантен в JavaScript, чем в C #. Параметры, которые не указаны, являются «неопределенными» в javascript, что приводит к значению false в операторе if. Однако определение функции не передает информацию о том, что p2 и p3 являются необязательными. Если вам нужно много перегрузок, jQuery решил использовать объект в качестве параметра, например, jQuery.ajax (options). Я согласен с ними, что это самый мощный и четко документируемый подход к перегрузке, но мне редко требуется более одного или двух быстрых необязательных параметров.

РЕДАКТИРОВАТЬ: изменение теста ЕСЛИ в соответствии с предложением Яна

66 голосов
/ 19 января 2009

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

47 голосов
/ 03 ноября 2015

Правильный ответ: В JAVASCRIPT НЕТ ПЕРЕГРУЗКИ.

Проверка / Переключение внутри функции не является ПЕРЕГРУЗКОЙ.

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

Например, doTask () и doTask (объект O) являются перегруженными методами. Чтобы вызвать последнее, объект должен быть передан как параметр, тогда как первый не требует параметра и вызывается с пустым полем параметра. Распространенной ошибкой будет присвоение объекту значения по умолчанию во втором методе, что приведет к неоднозначной ошибке вызова, поскольку компилятор не будет знать, какой из двух методов использовать.

https://en.wikipedia.org/wiki/Function_overloading

Все предлагаемые реализации великолепны, но, по правде говоря, для JavaScript нет нативной реализации.

26 голосов
/ 19 января 2009

Есть два способа лучше подойти к этому:

  1. Передайте словарь (ассоциативный массив), если вы хотите оставить большую гибкость

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

17 голосов
/ 26 декабря 2011

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

Func(new Point());
Func(new Dimension());
Func(new Dimension(), new Point());
Func(0, 0, 0, 0);

Редактировать (2018) : С тех пор, как это было написано в 2011 году, скорость прямых вызовов методов значительно возросла, а скорость перегруженных методов - нет.

Я бы не рекомендовал такой подход, но стоит подумать над тем, как решить эти проблемы.


Вот эталон различных подходов - https://jsperf.com/function-overloading. Это показывает, что перегрузка функций (с учетом типов) может быть примерно в 13 раз медленнее в Google Chrome V8 по состоянию на 16,0 (бета) .

Наряду с передачей объекта (т. Е. {x: 0, y: 0}), можно также использовать подход C, когда это уместно, называя методы соответственно. Например, Vector.AddVector (vector), Vector.AddIntegers (x, y, z, ...) и Vector.AddArray (integerArray). Вы можете посмотреть библиотеки C, такие как OpenGL, для вдохновения в именовании.

Редактировать : я добавил тест для прохождения объекта и тестирования объекта, используя как 'param' in arg, так и arg.hasOwnProperty('param'), и перегрузка функции намного быстрее, чем передача объекта и проверка свойств (по крайней мере, в этом тесте).

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

Мой пример взят из реализации Rectangle - отсюда и упоминание Dimension and Point. Возможно, Rectangle может добавить метод GetRectangle() к прототипу Dimension и Point, и тогда проблема перегрузки функции будет решена. А как насчет примитивов? Итак, у нас есть длина аргумента, которая теперь является допустимым тестом, поскольку у объектов есть метод GetRectangle().

function Dimension() {}
function Point() {}

var Util = {};

Util.Redirect = function (args, func) {
  'use strict';
  var REDIRECT_ARGUMENT_COUNT = 2;

  if(arguments.length - REDIRECT_ARGUMENT_COUNT !== args.length) {
    return null;
  }

  for(var i = REDIRECT_ARGUMENT_COUNT; i < arguments.length; ++i) {
    var argsIndex = i-REDIRECT_ARGUMENT_COUNT;
    var currentArgument = args[argsIndex];
    var currentType = arguments[i];
    if(typeof(currentType) === 'object') {
      currentType = currentType.constructor;
    }
    if(typeof(currentType) === 'number') {
      currentType = 'number';
    }
    if(typeof(currentType) === 'string' && currentType === '') {
      currentType = 'string';
    }
    if(typeof(currentType) === 'function') {
      if(!(currentArgument instanceof currentType)) {
        return null;
      }
    } else {
      if(typeof(currentArgument) !== currentType) {
        return null;
      }
    } 
  }
  return [func.apply(this, args)];
}

function FuncPoint(point) {}
function FuncDimension(dimension) {}
function FuncDimensionPoint(dimension, point) {}
function FuncXYWidthHeight(x, y, width, height) { }

function Func() {
  Util.Redirect(arguments, FuncPoint, Point);
  Util.Redirect(arguments, FuncDimension, Dimension);
  Util.Redirect(arguments, FuncDimensionPoint, Dimension, Point);
  Util.Redirect(arguments, FuncXYWidthHeight, 0, 0, 0, 0);
}

Func(new Point());
Func(new Dimension());
Func(new Dimension(), new Point());
Func(0, 0, 0, 0);
16 голосов
/ 19 января 2009

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

  1. Использование необязательных аргументов, таких как y = y || 'default'. Это удобно, если вы можете это сделать, но это может не всегда работать практически, например, когда 0 / null / undefined будет действительным аргументом.

  2. Использование количества аргументов. Аналогично последнему параметру, но может работать, когда # 1 не работает.

  3. Проверка типов аргументов. Это может работать в некоторых случаях, когда количество аргументов одинаково. Если вы не можете точно определить типы, вам может потребоваться использовать разные имена.

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

13 голосов
/ 13 сентября 2014

Если мне нужна была функция с двумя функциями foo (x) и foo (x, y, z), что является лучшим / предпочтительным способом?

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

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

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

function foo(x)
{
} 

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

fooNew(x,y,z)
{
}

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

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

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

( подробнее )

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

9 голосов
/ 06 апреля 2015

Я не уверен насчет лучшей практики, но вот как я это делаю:

/*
 * 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"
6 голосов
/ 17 июня 2013

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

TEST = {};

TEST.multiFn = function(){
    // function map for our overloads
    var fnMap = {};

    fnMap[0] = function(){
        console.log("nothing here");
        return this;    //    support chaining
    }

    fnMap[1] = function(arg1){
        //    CODE here...
        console.log("1 arg: "+arg1);
        return this;
    };

    fnMap[2] = function(arg1, arg2){
        //    CODE here...
        console.log("2 args: "+arg1+", "+arg2);
        return this;
    };

    fnMap[3] = function(arg1,arg2,arg3){
        //    CODE here...
        console.log("3 args: "+arg1+", "+arg2+", "+arg3);
        return this;
    };

    console.log("multiFn is now initialized");

    //    redefine the function using the fnMap in the closure
    this.multiFn = function(){
        fnMap[arguments.length].apply(this, arguments);
        return this;
    };

    //    call the function since this code will only run once
    this.multiFn.apply(this, arguments);

    return this;    
};

Проверьте это.

TEST.multiFn("0")
    .multiFn()
    .multiFn("0","1","2");
...