Computer Keyboard - Głównie JavaScript

Głównie JavaScript

rss feed icon

by

Przeciążanie funkcji i metod w JavaScript

Przeładowana ciężarówka w Indiach
Peter Krimbacher, źródło wikimedia commons licencja CC-BY-SA

JavaScript jest językiem dynamicznym, w którym funkcje mogą przyjmować wiele argumentów. Nie ma w nim jednak mechanizmu, który by wywoływał inne funkcje w zależności do liczby argumentów (czyli nie obsługuje przeciążania funkcji). W tym wpisie przedstawię jak prosto można taki mechanizm dodać do języka.

Idea wzięła się od wpisu na blogu Johna Resiga (tego od jQuery) JavaScript Method Overloading. W swojej implementacji Jonh używał takiego API:

function Users(){
  addMethod(this, "find", function(){
    // Find all users...
  });
  addMethod(this, "find", function(name){
    // Find a user by name
  });
  addMethod(this, "find", function(first, last){
    // Find a user by first and last name
  });
}

var users = new Users();
users.find(); // wyszukaj wszystkie
users.find("John"); // Wysuzkaj po imieniu
users.find("John", "Resig"); // Wyszukaj po imieniu i nazwsiku
users.find("John", "E", "Resig"); // nie zadziała

Poszedłem o krok dalej i napisałem funkcje, której można użyć, przekazując tablicę funkcji. Zwraca ona nową, przeładowaną funkcje. Można jej użyć w ten sposób:

var o = {
    x: 10,
    foo: method("foo", [
        function(a) {
            console.log("a: " + a);
        },
        function(a, b) {
            console.log("b: " + a + ' ' + b);
        },
        function() {
            console.log("c: " + this.x);
       }
    ])
};

o.foo(10);
o.foo(10, 20);
o.foo();
try {
    o.foo(1,2,3);
} catch (e) {
    console.error(e.message);
}

Ostatnie wywołanie zwróci wyjątek ponieważ nie ma funkcji z 3 argumentami.

Moja funkcja method wygląda tak:

function method(name, fns) {
    if (fns instanceof Array) {
        if (fns.length == 1) {
            return fns[0];
        }
        // zamiast Array::reduce można zwracać funkcje z pętlą for
        return fns.reduce(function(result, fn) {
            return function() {
                var len = arguments.length;
                if (len == fn.length) {
                    fn.apply(this, arguments);
                } else if (typeof result == 'function') {
                    result.apply(this, arguments);
                } else {
                    throw new Error("Can't find method '" + name + "' with " + len +
                                    ' arg' + (len != 1 ? 's' : ''));
                }
            };
        }, null);
    } else {
        return fns;
    }
}

Nazwa method może nie jest najtrafniejsza, ponieważ zwracana jest zwykła funkcja i lepsza byłaby np. overload.

Funkcja korzysta z ciekawej właściwości języka JavaScript, gdzie każda funkcja posiada właściwość length, która określa liczbę parametrów oraz wewnątrz funkcji arguments.length, która zawiera liczbę argumentów wywołania.

Można też funkcje uprościć i pobierać listę funkcji jako argumenty, wtedy funkcja wyglądałaby tak:

function method(name) {
    var fns = [].slice.call(arguments, 1);
    if (fns.length == 1) {
        return fns[0];
    }
    return fns.reduce(function(result, fn) {
        return function() {
            var len = arguments.length;
            if (len == fn.length) {
                fn.apply(this, arguments);
            } else if (typeof result == 'function') {
                result.apply(this, arguments);
            } else {
                throw new Error("Can't find method '" + name + "' with " + len +
                                ' arg' + (len != 1 ? 's' : ''));
            }
        };
    }, null);
}

Zamiast [].slice.call można użyć operatora rest z nowej wersji ECMAScript czyli:

function method(name, ...fns) {
    // jak wyżej
}

Możesz przetestować funkcje method w demo na CodePen.

źródło strony (aby zobaczyć kod na githubie musisz kliknąć przycisk raw)