4 марта 2010 в 19:29

Перегрузка функций в JS

Как известно, в JavaScript нельзя создать несколько функций, различающихся только списком параметров: последняя созданная перезапишет предыдущие. Про различие на уровне типов параметров говорить не приходится вообще. Обычно, если программист хочет создать функцию с множественным интерфейсом, он пишет что-то вроде такого:
  1. // getRectangleArea(x1, y1, x2, y2) или
  2. // getRectangleArea(width, height)
  3. function getRectangleArea(x1, y1, x2, y2) {
  4.   if(arguments.length==2) return x1*y1;
  5.   return (x2-x1)*(y2-y1);
  6. }
* This source code was highlighted with Source Code Highlighter.

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

Во-первых, хотелось бы написать отдельные функции для каждого набора параметров примерно так:
  1. function getRectangleArea(width, height) {
  2.   return width * height;
  3. }
  4.  
  5. function getRectangleArea(x1, y1, x2, y2) {
  6.   return (x2-x1)*(y2-y1);
  7. }
* This source code was highlighted with Source Code Highlighter.

Так код красивее и логичнее, можно в каждой функции дать параметрам говорящие имена. Одна беда: он не работает. Но мы можем написать генератор функций первого типа, подав ему на вход набор подфункций, как во втором примере. Пусть для начала у нас все подфункции имеют разное количество параметров. Количество параметров функции можем узнать по functionName.length, а количество переданных аргументов — по arguments.length. Вспомним также великую штуку apply и напишем:
  1. function polymorph() {
  2.   var len2func = [];
  3.   for(var i=0; i<arguments.length; i++)
  4.     if(typeof(arguments[i]) == "function")
  5.       len2func[arguments[i].length] = arguments[i];
  6.   return function() {
  7.     return len2func[arguments.length].apply(this, arguments);
  8.   }
  9. }
* This source code was highlighted with Source Code Highlighter.

Функция polymorph принимает в качестве аргументов набор подфункций с разным количеством параметров, заносит их в массив len2func, индекс которого — число параметров, и возвращает функцию-замыкание, которая в зависимости от числа аргументов вызывает ту или иную подфункцию. Пользоваться так:
  1. var getRectangleArea2 = polymorph(
  2.   function(width, height) {
  3.     return width * height;
  4.   },
  5.   function(x1, y1, x2, y2) {
  6.     return (x2-x1)*(y2-y1);
  7.   }
  8. );
* This source code was highlighted with Source Code Highlighter.

Теперь getRectangleArea2 — полный аналог getRectangleArea, однако код стал гораздо прозрачнее, и теперь уже не требуется комментарий: способы использования очевидны.

Ситуация становится сложнее, если мы хотим поддерживать функции с одинаковым количеством параметров, отличающихся типами. Однако типы можно контролировать с помощью typeof и instanceof, поэтому несложно расширить функцию polymorph() и для этих случаев. Типы параметров будем задавать в специальном объекте перед функцией:
  1. var PolyFunc = polymorph(
  2.     function(a,b,c) {
  3.         return "Three arguments version -- any types";
  4.     },
  5.     
  6.     {i: Number, str: String},
  7.     function(i,str) {
  8.         return "Number and string passed";
  9.     },
  10.     
  11.     {re: RegExp},
  12.     function(re,a) {
  13.         return "RegExp and something else passed";
  14.     },
  15.     
  16.     {f: Function, b: Boolean},
  17.     function(f,b) {
  18.         return "Function and boolean passed";
  19.     },
  20.     
  21.     {f: Function, i: Number},
  22.     function(f,i) {
  23.         return "Function and number passed";
  24.     }
  25. );
  26.  
  27. alert(PolyFunc(1,2,3)); // "Three arguments version -- any types"
  28. alert(PolyFunc(1,"qq")); // "Number and string passed"
  29. alert(PolyFunc(function() {}, true)); // "Function and boolean passed"
  30. alert(PolyFunc(function() {}, 1)); // "Function and number passed"
  31. alert(PolyFunc(/a/, 1)); // "RegExp and something else passed"
  32. alert(PolyFunc(/a/, "str")); // "RegExp and something else passed"
* This source code was highlighted with Source Code Highlighter.

Всё это дело я выложил в Google code и буду рад, если кому-нибудь пригодится. Перегружать можно создать не только обычные функции, но и методы, и конструкторы объектов (примеры есть по ссылке), причём их можно определять через друг друга (вызывая свою версию с другим набором параметров). В качестве типов параметров можно использовать как встроенные, так и пользовательские. Есть возможность модифицировать уже созданную полиморфную функцию, добавив или заменив какие-то подфункции.

За красоту, к сожалению, приходится платить и как всегда — быстродействием. Оверхед на вызов функции на моём Core2DUO @ 2.66GHz составил в FF3.6, IE8, Opera10 и Safari около 1-10 мкс (IE самый медленный, затем FF, потом Opera и Safari). Chrome демонстрирует наилучший результат в диапазоне 0.3-1 мкс, обгоняя FF в 10 раз. Для функций, которые сами по себе быстрые и вызываются очень часто, используйте осторожно.
Тагир Валеев @lany
карма
487,2
рейтинг 35,8
Программист
Похожие публикации
Самое читаемое Разработка

Комментарии (59)

  • 0
    спасибо, интересно!
  • +1
    Даешь Pattern matching в Javascript! :)
    Оригинально.
    А на практике насколько это удобно?
    • +1
      Только начал использовать, пока доволен. Мы портируем Java-библиотеку на JS, а в ней есть классы с десятью конструкторами. Эта штука серьёзно облегчает жизнь :-)
      • +1
        От того, что у вас есть, остался 1 шаг по pattern matching-а.
        На мой взгляд, это как раз апофигей полиморфности. Когда проверять можно не тип аргумента, но и его содержимое.
        • +1
          К сожалению, pattern matching с более-менее нетривиальными правилами и с приличной глубиной добавит ещё оверхеда, причём прилично, поскольку в JS сложно сделать эффективные итераторы. Я сейчас потихоньку экспериментирую с препроцессором, который будет встраивать вспомогательный код непосредственно в тело использующих его функций, тогда, может, будет нормально.
          • 0
            Вобщем да.
            Тут уже вопрос надобности. Надо ли писать язык на языке.
            • 0
              Насколько я знаю, это естественный путь развития скриптовых языков — все они написаны на каком-то языке :)
              • 0
                нескриптовые, впрочем, тоже. За очень редким исключением. По крайне мере компиляторы си написаны на си :)
    • 0
      > А на практике насколько это удобно?
      Менее удобно, чем реализация средствами языка, но уж гораздо лучше чем более часто используемый первый пример.

      Если есть способ избежать подобных извращений, то лучше избежать… Но если очень хочется, то можно))))

  • –12
    Я бы посоветовал сделать в виде плагина jQuery.
    Так было бы удобней подключать и использовать.
    • +2
      Я думал над этим, но это лишняя зависимость. В принципе это делается добавлением одной строчки в код, а так как лицензия MIT, то кто угодно может этим заняться :-) Возможно, добавлю в SVN jQuery-версию дополнительно к обычной.
      • +2
        Уж чегочего но это точно не для jQuery.
        Вы вообще в курсе, что javascript есть не только в браузере?
        • 0
          хм… сорри. Это был ответ на верхний коммент.
        • +3
          Я бы еще спросил «Вы вообще в курсе, что на javascript можно программировать без jQuery?».
          • +2
            Зачастую считается что нельзя :)
  • 0
    Может для библиотеки это хороший прием, но не для конечного кода — однозначно. Код получается не отражающим свою сущность, хотя интерфейс выходит удобным.

    А может можно увеличить быстродействие с помощью прототипов, или, скажем расширив класс функции через прототип?
    • 0
      Можно попытаться сгенерировать тело новой функции, активно используя toString и new Function. А как прототип класса функции может увеличить быстродействие? Можно пример?
      • 0
        Я рассуждаю гипотетически. К сожалению с необходимостью перегрузки функций в JS ни разу не сталкивался.

        Упоминая прототипы — я говорил о том, чтоб расширить Function, добавив туда возможность перегружать методы. Мне кажется это может работать быстрее.

        Если будет время — поковыряюсь, может что-то и придумаю. Или обнаружу что я дико ошибся, что тоже познавательно.
  • +7
    По-моему это называется перегрузкой, про которую можно почитать у John Resig — ejohn.org/blog/javascript-method-overloading/
    • 0
      Ага… не по тем ключевым словам искал. Спасибо. Ну хоть контроля типов там нет :-)
  • 0
    хмммм… что-то мне подсказывает что так можно сделать и в PHP5.3, зачем не спрашивайте
  • +16
    Перегрузка не является стандартным решением для JS. Статья не может считаться законченной без предупреждения: Подобная практика приводит к трудночитаемости кода и в условиях работы в команде разработчиков грозит физическими побоями, используйте на свой страх и риск, только в крайних случаях.
  • –2
    Аккуратное решение=) спасибо что поделились!
    • –3
      Вот интересно, за что меня проминусовал, тот человек который это сделал?)

      Я честно честно не буду пиздить печеньки лучшего решения, чем это не знаю=)

  • +12
    Я стараюсь во все более-менее сложные функции пихать не набор параметров, а объект параметров. Не так «по-взрослому», но вполне удобно (бесконечно расширяемо и обратно совместимо).
    • 0
      почему не по-взрослому? Фаулер, кстати, рекомендует :)
  • 0
    1) Решение — изврат
    2) Полиморфные функции в общем-то зло. Почему? Да потому что, раз они называются одинаково, то и код у них должен быть одинаковый, а так появляется путаница.
    • +2
      У них семантика должна быть одинаковая. А интерфейс может быть разный. Это удобно. Нередко все перегруженные функции вызывают одну из них, которая реализует фактическую функциональность.
      • 0
        Считаю, все же лучше ограничиться параметрами по умолчанию и (возможно) проверкой типа, но все же написать одну функцию, а не несколько. К тому же, эти функции будут содержать похожий код, что плохо.
        • 0
          есть замечательный практический пример. Нужно найти расстояние от точки до первой точки плюс от второй для третьей и так далее.

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

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

          В данном случае никто не мешает внутри перегруженной функции вызывать самою себя. Работа перезгрузки заключается лишь в приведении аргументов к общему виду.
          • +1
            Гм, тут лучше назвать 2 функции по-разному, так как логика их действия по моему немного различается. И в данном примере — отличный пример двусмысленности, которая порождает ошибки и трудности с отладкой. Куда проще тут сделать distanceForObjects(...) и distanceForArray(...)

            Еще, кстати один пример неудачного, имхо, полиморфизма — это когда функцию вроде max() можно вызвать с массивом аргументов: max([1,2, 3]), а можно — с несколькими числовым аргументами: max(1,2,3) На мой взгляд, выигрыш от экономии 2 скобок не перекрывает возмодные сложности с пониманием кода.
            • 0
              > max(1,2,3)… max([1, 2, 3])
              просто доп. гибкость. например, когда числа получаем из какой-нибудь фукнции массивом, не нужно apply делать — удобно, если формат возвращаемого значения от этого не меняется.
  • –1
    лучше так:
    var polyFunc= polymorph(function( i /*Number*/, str /*String*/ ){
    })
    • 0
      Не сработает в Gecko, пытался. Он выкусывает комментарии в .toString (стандарт не регламентирует, можно ли это делать).
  • 0
    зачем же такие сложности, когда можно передать в качестве параметра объект? :)
    • 0
      А конструировать объект тоже объектом? У вас в каждой функции один параметр? :-)
      • +1
        а зачем в каждую функцию один параметр? только в «полиморфные» :)
      • +3
        эм, туго соображаю под вечер — возможно, не совсем понял вопрос…

        но что мешает сделать как-нибудь так:

        function getRectangleArea(opt) {
        return (opt.x2 — opt.x1) * (opt.y2 — opt.y1) || (opt.width * opt.height)
        }
  • НЛО прилетело и опубликовало эту надпись здесь
  • +2
    Применительно к рассмотренному случаю, я бы написал так:

    function getRectangleArea(/**Object*/o)/**Number*/{
      function getRectangleArea(/**Number*/width, /**Number*/height)/**Number*/{
        return width * height;
      }
      if ('width' in o && 'height' in o) {
        return getRectangleArea(o.width, o.height);
      }
      if ('x1' in o && 'x2' in o && 'y1' in o && 'y2' in o) {
        return getRectangleArea(o.x2 - o.x1, o.y2 - o.y1);
      }
    }
    console.log(getRectangleArea({width: 10, height: 10}));
    console.log(getRectangleArea({x1: 10, x2: 20, y1: 10, y2: 20}));

    * This source code was highlighted with Source Code Highlighter.
  • 0
    помоему стоит разделить функцию polimorph на 2, т.е. реализацию отделить от заполнения функциями
    тогда: 1 будет понятней работа, 2: можно будет динамически добавлять функции/объекты
    • 0
      И так можно. Пример есть по ссылке, я в статье не написал, чтобы статья вышла покороче. В принципе вы можете написать:
      var getRectangleArea3 = polymorph();

      getRectangleArea3.update(
        function(width, height) {
          return width * height;
        },
        function(x1, y1, x2, y2) {
          return (x2-x1)*(y2-y1);
        }
      );

      * This source code was highlighted with Source Code Highlighter.

      Работает точно так же.
  • –1
    спасибо! в закладки
  • +1
    Конечно если вы мигряетесь с явы, то это полезно, но вообще с точки зрения JS абсолютно не надо. Во-первых, ни одна IDE вам таким образом не подскажет список параметров. Во-вторых, в отличие от Java в JS есть JSON, что позволяет передавать параметры в виде обьектов весьма удобным способом:

    myFunc({width: 100, height: 200})
    myFunc({x1: 10, y1: 10, x2: 300, y2: 240})

    Ну а в-третьих, JS-кодеры со стажем по голове дадут (: И за перформанс и за мясо.
    • 0
      А при JSON-передаче данных редакторы подскажут список параметров?

      В моём случае, кстати, редакторам будет несложно подтянуться, если вдруг штука станет популярной :-) А с JSON вообще непонятно. Писать перед функцией комментарий в определённом формате, чтобы редактор узнал возможные прототипы функции?

      А с перформансом, мне кажется, многие не заморачиваются. Вот тестировал на днях jQuery-inheritance. Там на вызов метода, который вызывает себя же в базовом классе (this.__base(...)), оверхед примерно такой же, как у меня (по сравнению со стандартной некрасивой записью типа BaseClass.prototype..call(this, ...)). То есть чисто красота вызова метода базового класса стоит тех же микросекунд. И ничего, люди пользуют и хвалят :-)
      • 0
        > BaseClass.prototype..call(this, ...)
        Галочки Хабр скушал. Следует читать:
        BaseClass.prototype.<MethodName>.call(this, ...)
      • +2
        С параметрами штука в том, что в дев-версии скриптов многие пишут JSDoc и большинство IDE понимает JSDoc. Проблема JSDoc в том, что в данный момент он не расчитан на перегруженные функции, а вот указать формат обьекта и полноценно задокументировать можно (: Конечно если JSDoc подтянется, то такие перегрузки будут иметь смысл, но это врядли.

        Зачем вообще делать аннотации через JSDoc? Потому, что JS язык слабо-типизированный и IDE сама по себе не может определить что вы ей вернёте. Поэтому все вменяемые люди им пользуются.

        Ну а то, что jQuery не самая лучшая либа — это давно известно. Но над ней работают и постоянно доводят до ума. Ещё года три-четыре назад ни один вменяемый JS программер её не юзал по причине того, что внутрях были гигантские кучи идиотизма и авторы совсем были не в курсе что такое JS и с чем его едят (вот вы тоже пытаетесь JS превратить в Java, но зачем?). Сейчас ситуация в разы лучше, хотя некоторые вещи до сих пор вызывают недоумение.
        • 0
          > ни один вменяемый JS программер её не юзал
          Эк вы людей обидели :-) А тем временем невменяемые программеры сделали на коленке немало красивеньких сайтов.

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

          > вот вы тоже пытаетесь JS превратить в Java, но зачем?
          Я не пурист, а практик. Есть много готового кода, который внезапно захотелось выполнять на клиенте (java-апплет не надо советовать). Перенос с сохранением Java-концепций и исходных интерфейсов выполняется быстро и почти автоматически. Пока мы будем писать аналогичный, но концептуально прекрасный JS-код, конкуренты уйдут вперёд :-)
          • +1
            Ну так я сразу написал: Конечно если вы мигряетесь с явы, то это полезно. Никто не спорит, что в таких ситуациях важнее быстро смигряться. Но потом когда будет время постарайтесь уделить время JS в виде JS (:
  • 0
    Чуточку не в тему, но про похожий мехнизм.

    Я однажды очень расстроился когда не нашел в ЖС механизмов геттеров и сеттров для объектов, которые позволили бы мне делать подобную штуку с объектами
    • 0
      __defineGetter__ и __defineSetter__ были реализованы в движке SpiderMonkey, который используется в firefox (теперь уже TraceMonkey) а также в движке V8 (google chrome)
      • +1
        А остальные движки?
      • 0
        ну, скриптовые сеттеры и геттеры это не обычные сеттеры и геттеры, т.к. они не привязаны к переменной, а именованные «несвойства» объекта
        • 0
          не суть важно. Прототип в жаваскрипте тоже можно назвать несвойством. Здесь больше вопрос удобства, бо в жаваскрипте вопрос быстродействия на уровне обращения к структурам данных обычно не стоит. Быстродействие загибается в момент взаимодействия с дом-моделью.
  • +2
    Ну очень хочется заметить самое ГЛАВНОЕ. Вы говорите о ПОЛИМОРФИЗМЕ? А реализуете перегрузку (overloading). Тот кто захочет найти реализацию полиморфизма в вашем коде, зря потратит время. Вам их не жалко? А если они еще не знают о существовании полиморфизма (одном из 4 главнейших механизмов!!! ООП). Получится как в истории про молодого человека, который был уверен, что точка G («же») его подруги находится в «ж»опе.

    И за красоту надо платить только в том случае, если вы хотите ее купить :)

    Для эмигрантов явы, можно посоветовать GWT. Если нужна более близкая объектная реализация (притягивание) JS присмотритесь к Prototype.
    • +1
      Прежде чем чем говорить о термине, удостоверьтесь, что вы знаете его общее значение.

      В данном случае перегрузка (overloading) является видом полиморфизма.

      ООП тут не причем.
      Полиморфизм есть и в ML и даже в С.
      • –1
        Случай обычный, перегрузка это перегрузка, полиморфизм это полиморфизм :) кто из них вид, класс, отряд, подвид, частный случай конечно вам решать. В реализации языка JS нет «Полиморфизма функций». Вы эмулировали и создали случай, не вводите в заблуждение людей. Коли речь идет о параметрическом полиморфизме, встречающемся в функциональном программировании, не путайте только и не говорите потом, что функция JS является видом, случаем, частным отголоском функционального программирования, якобы слова однокоренные и есть общий термин «функция», то так и пишите :)
        • +2
          Во первых я не автор топика, поэтому ничего не эмулировал.
          Во-вторых параметрический полиморфизм (например шаблоны С++), перегрузка функций и неявное приведение типов является полиморфизмом в той же степени как и иерархический полиморфизм в ООП (механизм виртуальных функций).
          Переходя на метафоры
          Просто я заметил, что JS — это язык программирования.
          Вы же утверждаете, что язык программирования это язык программирования, а JS это JS.

          я говорил про общий случай.
          In computer science, polymorphism is a programming language feature that allows values of different data types to be handled using a uniform interface. [from wiki]

          вы про частный:
          Subtype polymorphism, almost universally called just polymorphism in the context of object-oriented programming, is the ability of one type, A, to appear as and be used like another type, B. [wiki]
    • +1
      В целом согласен, хотя это всё равно полиморфизм, как отметил zvulon. Изменил «полиморфизм» на «перегрузку» в названии и в тексте один раз и добавил тегов. В коде уж менять не буду, пусть остаётся :-)

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