Jak przeciążać funkcje w javascript?


103

Klasyczne (nie-js) podejście do przeciążania:

function myFunc(){
 //code
}

function myFunc(overloaded){
 //other code
}

Javascript nie pozwoli na zdefiniowanie więcej niż jednej funkcji o tej samej nazwie. W związku z tym pojawiają się takie rzeczy:

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

Czy istnieje lepsze obejście funkcji przeciążania funkcji w javascript, inne niż przekazywanie obiektu z tymi przeciążeniami?

Przekazywanie przeciążeń może szybko spowodować, że funkcja stanie się zbyt gadatliwa, ponieważ każde możliwe przeciążenie wymagałoby wtedy instrukcji warunkowej. Używanie funkcji do wypełniania //codewnętrza tych instrukcji warunkowych może powodować trudne sytuacje związane z zakresami.


1
Wtedy myślisz źle o javascript; jeśli masz problemy z przeciążeniami i zakresem, prawdopodobnie powinieneś mieć inną nazwę metody, ponieważ wygląda na to, że wykonuje inną pracę
joshcomley

@joshcomley - Przeciążenia i zakres wymagają kodu do obsługi. Po prostu staram się upewnić, że kod, którego używam, jest tak wydajny, jak to tylko możliwe. Obsługa zakresów z obejściami jest w porządku, ale wolę przynajmniej spróbować użyć najlepszych metod.
Travis J

Odpowiedzi:


126

Istnieje wiele aspektów związanych z przeciążaniem argumentów w JavaScript:

  1. Zmienne argumenty - możesz przekazywać różne zestawy argumentów (zarówno typu, jak i ilości), a funkcja będzie zachowywać się w sposób zgodny z przekazanymi do niej argumentami.

  2. Argumenty domyślne - można zdefiniować wartość domyślną argumentu, jeśli nie zostanie ona przekazana.

  3. Nazwane argumenty - kolejność argumentów staje się nieistotna i po prostu określasz, które argumenty chcesz przekazać do funkcji.

Poniżej znajduje się sekcja dotycząca każdej z tych kategorii obsługi argumentów.

Argumenty zmienne

Ponieważ javascript nie ma sprawdzania typu argumentów ani wymaganej liczby argumentów, możesz mieć tylko jedną implementację, myFunc()która dostosuje się do przekazywanych argumentów, sprawdzając typ, obecność lub liczbę argumentów.

jQuery robi to cały czas. Możesz ustawić niektóre argumenty jako opcjonalne lub możesz rozgałęzić swoją funkcję w zależności od tego, jakie argumenty są do niej przekazywane.

Podczas implementowania tego typu przeciążeń można użyć kilku różnych technik:

  1. Możesz sprawdzić obecność dowolnego argumentu, sprawdzając, czy zadeklarowaną wartością nazwy argumentu jest undefined.
  2. Możesz sprawdzić całkowitą ilość lub argumenty za pomocą arguments.length.
  3. Możesz sprawdzić typ dowolnego argumentu.
  4. W przypadku zmiennej liczby argumentów można użyć argumentspseudo-tablicy, aby uzyskać dostęp do dowolnego argumentu za pomocą arguments[i].

Oto kilka przykładów:

Spójrzmy na obj.data()metodę jQuery . Obsługuje cztery różne formy użytkowania:

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

Każdy z nich wywołuje inne zachowanie i bez użycia tej dynamicznej formy przeciążania wymagałby czterech oddzielnych funkcji.

Oto, jak można rozróżnić wszystkie te opcje w języku angielskim, a następnie połączę je wszystkie w kodzie:

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

Jeśli pierwszy przekazany argument .data()jest łańcuchem, a drugi argumentem undefined, to wywołujący musi używać tego formularza.


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

Jeśli drugi argument nie jest niezdefiniowany, ustaw wartość określonego klucza.


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

Jeśli nie przekazano żadnych argumentów, zwróć wszystkie klucze / wartości w zwróconym obiekcie.


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

Jeśli typ pierwszego argumentu jest zwykłym obiektem, ustaw wszystkie klucze / wartości z tego obiektu.


Oto, jak można połączyć je wszystkie w jeden zestaw logiki 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
     }
 },

Kluczem do tej techniki jest upewnienie się, że wszystkie formy argumentów, które chcesz zaakceptować, są jednoznacznie identyfikowalne i nigdy nie ma wątpliwości, której formy używa dzwoniący. Zwykle wymaga to odpowiedniego uporządkowania argumentów i upewnienia się, że typ i pozycja argumentów są na tyle unikalne, aby zawsze można było stwierdzić, która forma jest używana.

Na przykład, jeśli masz funkcję, która przyjmuje trzy argumenty łańcuchowe:

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

Możesz łatwo ustawić trzeci argument jako opcjonalny i możesz łatwo wykryć ten warunek, ale nie możesz uczynić tylko drugiego argumentu opcjonalnym, ponieważ nie możesz powiedzieć, który z nich ma przekazać wywołujący, ponieważ nie ma sposobu, aby określić, czy drugi argument argument ma być drugim argumentem lub drugi argument został pominięty, więc to, co znajduje się w miejscu drugiego argumentu, jest w rzeczywistości trzecim argumentem:

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

Ponieważ wszystkie trzy argumenty są tego samego typu, nie możesz odróżnić różnych argumentów, więc nie wiesz, co zamierzał dzwoniący. W tym stylu wywoływania tylko trzeci argument może być opcjonalny. Gdybyś chciał pominąć drugi argument, musiałby zostać przekazany jako null(lub inna wykrywalna wartość), a kod wykryłby, że:

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

Oto przykład opcjonalnych argumentów w jQuery. oba argumenty są opcjonalne i przyjmują wartości domyślne, jeśli nie zostaną przekazane:

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

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

Oto przykład jQuery, w którym może brakować argumentu lub jeden z trzech różnych typów, co daje cztery różne przeciążenia:

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

Nazwane argumenty

Inne języki (takie jak Python) pozwalają na przekazywanie nazwanych argumentów jako sposobu przekazywania tylko niektórych argumentów i uniezależnienia ich od kolejności, w jakiej są przekazywane. Javascript nie obsługuje bezpośrednio funkcji nazwanych argumentów. Wzorzec projektowy, który jest powszechnie stosowany w jego miejsce, to przekazywanie mapy właściwości / wartości. Można to zrobić, przekazując obiekt z właściwościami i wartościami lub w wersji ES6 i nowszych można faktycznie przekazać sam obiekt Map.

Oto prosty przykład ES5:

jQuery $.ajax()akceptuje formę użycia, w której wystarczy przekazać mu pojedynczy parametr, który jest zwykłym obiektem JavaScript z właściwościami i wartościami. Jakie właściwości mu przekazujesz, określają, które argumenty / opcje są przekazywane do wywołania Ajax. Niektóre mogą być wymagane, wiele jest opcjonalnych. Ponieważ są one właściwościami obiektu, nie ma określonej kolejności. W rzeczywistości istnieje ponad 30 różnych właściwości, które można przekazać do tego obiektu, wymagana jest tylko jedna (adres URL).

Oto przykład:

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

Wewnątrz $.ajax()implementacji może po prostu zapytać, które właściwości zostały przekazane do przychodzącego obiektu i użyć ich jako nazwanych argumentów. Można to zrobić za pomocą for (prop in obj)lub przez pobranie wszystkich właściwości do tablicy za pomocą, Object.keys(obj)a następnie iterację tej tablicy.

Ta technika jest bardzo często używana w Javascript, gdy występuje duża liczba argumentów i / lub wiele argumentów jest opcjonalnych. Uwaga: nakłada to obowiązek na funkcję implementującą, aby upewnić się, że obecny jest minimalny prawidłowy zestaw argumentów i przekazać wywołującemu pewne informacje zwrotne dotyczące debugowania, czego brakuje, jeśli przekazano niewystarczające argumenty (prawdopodobnie przez zgłoszenie wyjątku z pomocnym komunikatem o błędzie) .

W środowisku ES6 możliwe jest użycie destrukturyzacji do tworzenia domyślnych właściwości / wartości dla powyższego przekazanego obiektu. Omówiono to bardziej szczegółowo w tym artykule referencyjnym .

Oto przykład z tego artykułu:

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

Stwarza to domyślne właściwości i wartości dla start, endi stepwłaściwości na obiekt przekazany do selectEntries()funkcji.

Domyślne wartości argumentów funkcji

W ES6 Javascript dodaje wbudowaną obsługę języka dla domyślnych wartości argumentów.

Na przykład:

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

multiply(5); // 5

Dalszy opis sposobów, w jakie można to wykorzystać, znajduje się tutaj na MDN .


Powinienem więc po prostu trzymać się funkcji adaptowalnej i zaakceptować to jako najlepszą praktykę przy implementacji fasady przeciążenia funkcji?
Travis J

2
@TravisJ - tak, adaptowalna funkcja to sposób, w jaki przeciążenia są wykonywane w JavaScript. Inną opcją jest użycie osobno nazwanej funkcji z różnymi argumentami. Jeśli te dwie implementacje nie mają ze sobą nic wspólnego, to jedna z nich jest dopuszczalna. Jeśli te dwie implementacje zasadniczo robią to samo, ale zaczynają od różnych argumentów, myślę, że sprawia to, że twój interfejs jest bardziej zwarty, aby używać funkcji adaptowalnej i daje możliwość współdzielenia kodu między dwiema powiązanymi implementacjami.
jfriend00

Dodano kilka przykładów kodu, wyjaśnionych zarówno w języku angielskim, jak i w rzeczywistym kodzie.
jfriend00

Gdy próbuję, otrzymuję błąd, nieoczekiwany token „=” w najnowszej wersji Safari resetProgressBar: function(display_errors, lockout = false).
Nick,

@Nick - zgodnie z tą tabelą zgodności ES6 domyślne wartości parametrów powinny działać w przeglądarce Safari 10 i nowszych. Na tej stronie MDN jest napisane „brak wsparcia” w Safari. Nie mam komputera Mac, więc nie mogę go samodzielnie przetestować. Wydaje się, że jest to ogólnie jedna z później zaimplementowanych funkcji ES6 w wielu przeglądarkach. Na przykład nie wygląda jeszcze tak, jakby był w iOS9.
jfriend00

33

Przeciążanie funkcji w JavaScript można wykonać na wiele sposobów. Wszystkie obejmują jedną funkcję nadrzędną, która wykonuje wszystkie procesy lub deleguje podfunkcje / procesy.

Jedna z najczęstszych prostych technik polega na prostym przełączeniu:

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

Bardziej elegancką techniką byłoby użycie tablicy (lub obiektu, jeśli nie wykonujesz przeciążeń dla każdej liczby argumentów):

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

Ten poprzedni przykład nie jest zbyt elegancki, każdy może go zmodyfikować fooArri nie powiedzie się, jeśli ktoś przekaże więcej niż 2 argumenty do foo, więc lepszą formą byłoby użycie wzorca modułu i kilku sprawdzeń:

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

Oczywiście przeciążenia mogą chcieć użyć dynamicznej liczby parametrów, więc możesz użyć obiektu do fnskolekcji.

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

Moje osobiste preferencje wydają się być takie switch, chociaż zwiększa to funkcję nadrzędną. Typowym przykładem zastosowania tej techniki byłaby metoda akcesora / mutatora:

function Foo() {} //constructor
Foo.prototype = {
    bar: function (val) {
        switch (arguments.length) {
        case 0:
            return this._bar;
        case 1:
            this._bar = val;
            return this;
        }
    }
}

To dobry przykład alternatywy :)
Travis J

1
Fajnie, podoba mi się ta druga technika
Nick Rolando

8

Nie można przeciążać metod w ścisłym tego słowa znaczeniu. Nie tak, jak jest obsługiwany w javalub c#.

Problem polega na tym, że JavaScript NIE obsługuje natywnie przeciążania metod. Tak więc, jeśli zobaczy / przeanalizuje dwie lub więcej funkcji o takich samych nazwach, po prostu weźmie pod uwagę ostatnio zdefiniowaną funkcję i nadpisze poprzednie.

Jeden ze sposobów, który moim zdaniem jest odpowiedni dla większości przypadków, jest następujący:

Powiedzmy, że masz metodę

function foo(x)
{
} 

Zamiast przeciążania metody, co nie jest możliwe w javascript , możesz zdefiniować nową metodę

fooNew(x,y,z)
{
}

a następnie zmodyfikuj pierwszą funkcję w następujący sposób -

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

Jeśli masz wiele takich przeciążonych metod, rozważ użycie switchniż tylko if-elseinstrukcji.

( więcej szczegółów ) PS: Powyższy link prowadzi do mojego osobistego bloga, który zawiera dodatkowe informacje na ten temat.


To nie jest problem, to jest funkcja.
inetphantom

4

Używam nieco innego podejścia do przeciążania w oparciu o liczbę argumentów. Uważam jednak, że podejście Johna Fawcetta jest również dobre. Oto przykład, kod oparty na wyjaśnieniach Johna Resiga (autora 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);
            }
        };
    }

użyteczność:

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

W javascript możesz zaimplementować funkcję tylko raz i wywołać funkcję bez parametrów myFunc() Następnie sprawdzasz czy opcje są 'undefined'

function myFunc(options){
 if(typeof options != 'undefined'){
  //code
 }
}

3

Starałem się wypracować eleganckie rozwiązanie opisanego tutaj problemu . Demo można znaleźć tutaj . Sposób użycia wygląda następująco:

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

Metody zastosowane do osiągnięcia tego:

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
Funkcja do obsługi wejść, bardzo sprytna.
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

Nie ma problemu z przeciążaniem w JS, pb jak zachować czysty kod podczas przeciążania funkcji?

Możesz użyć przekazywania, aby mieć czysty kod, oparty na dwóch rzeczach:

  1. Liczba argumentów (podczas wywoływania funkcji).
  2. Typ argumentów (podczas wywoływania funkcji)

      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

Ponieważ JavaScript nie ma opcji przeciążania funkcji, można zamiast tego użyć obiektu. Jeśli jest jeden lub dwa wymagane argumenty, lepiej trzymać je oddzielnie od obiektu opcji. Oto przykład użycia obiektu opcji i wypełnienia wartości domyślnych w przypadku, gdy wartość nie została przekazana w obiekcie opcji.

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

}

tutaj jest przykład jak używać obiektu opcji


2

W tym celu musisz utworzyć funkcję, która doda funkcję do obiektu, a następnie będzie wykonywana w zależności od ilości argumentów przesłanych do funkcji:

<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

Lubię dodawać podfunkcje w ramach funkcji nadrzędnej, aby uzyskać możliwość rozróżniania grup argumentów dla tej samej funkcjonalności.

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

To, co próbujesz osiągnąć, najlepiej zrobić przy użyciu zmiennej lokalnych argumentów funkcji .

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

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

Możesz znaleźć lepsze wyjaśnienie, jak to działa tutaj .


A co z wykrywaniem kolizji? To dobry sposób na używanie funkcji bez parametrów lub sprawdzanie istnienia, ale nie jest to najlepsza praktyka, jeśli chodzi o przeciążanie.
Travis J
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.