Javascript, kiedy używać prototypów

93

Chciałbym wiedzieć, kiedy należy używać metod prototypowych w js. Czy zawsze powinny być używane? Czy są przypadki, w których ich używanie nie jest preferowane i / lub powoduje spadek wydajności?

Przeszukując tę ​​witrynę pod kątem typowych metod określania przestrzeni nazw w js, wydaje się, że większość używa implementacji nieopartej na prototypie: po prostu używa obiektu lub obiektu funkcji do hermetyzacji przestrzeni nazw.

Pochodząc z języka opartego na klasach, trudno nie próbować rysować podobieństw i myśleć, że prototypy są jak „klasy”, a implementacje przestrzeni nazw, o których wspomniałem, są jak metody statyczne.

opl
źródło

Odpowiedzi:

134

Prototypy to optymalizacja .

Świetnym przykładem ich dobrego wykorzystania jest biblioteka jQuery. Za każdym razem, gdy uzyskujesz obiekt jQuery przy użyciu $('.someClass'), obiekt ten ma dziesiątki „metod”. Biblioteka mogłaby to osiągnąć, zwracając obiekt:

return {
   show: function() { ... },
   hide: function() { ... },
   css: function() { ... },
   animate: function() { ... },
   // etc...
};

Ale to oznaczałoby, że każdy obiekt jQuery w pamięci miałby dziesiątki nazwanych slotów zawierających w kółko te same metody.

Zamiast tego metody te są zdefiniowane w prototypie, a wszystkie obiekty jQuery „dziedziczą” ten prototyp, aby uzyskać wszystkie te metody przy bardzo niewielkim koszcie wykonania.

Niezwykle ważną częścią tego, jak jQuery robi to dobrze, jest to, że jest to ukryte przed programistą. Traktuje się go wyłącznie jako optymalizację, a nie jako coś, o co musisz się martwić podczas korzystania z biblioteki.

Problem z JavaScriptem polega na tym, że nagie funkcje konstruktora wymagają, aby wywołujący pamiętał, aby poprzedzić je przedrostkiem newlub w przeciwnym razie zazwyczaj nie działają. Nie ma ku temu dobrego powodu. jQuery robi to dobrze, ukrywając ten nonsens za zwykłą funkcją $, więc nie musisz przejmować się implementacją obiektów.

Aby można było wygodnie utworzyć obiekt z określonym prototypem, ECMAScript 5 zawiera standardową funkcję Object.create. Znacznie uproszczona wersja wyglądałaby tak:

Object.create = function(prototype) {
    var Type = function () {};
    Type.prototype = prototype;
    return new Type();
};

Dba o ból związany z pisaniem funkcji konstruktora, a następnie wywoływaniem jej za pomocą new.

Kiedy unikałbyś prototypów?

Użyteczne porównanie jest z popularnymi językami OO, takimi jak Java i C #. Obsługują one dwa rodzaje dziedziczenia:

  • Interfejs dziedziczenia, gdzie takie, że klasa udostępnia swój niepowtarzalny realizacji dla każdego członka interfejsu.implementinterface
  • dziedziczenie implementacji , gdzie extendjest classdostarczana domyślna implementacja niektórych metod.

W JavaScript dziedziczenie prototypowe jest rodzajem dziedziczenia implementacyjnego . Tak więc w sytuacjach, w których (w C # lub Javie) wyprowadziłbyś z klasy bazowej, aby uzyskać domyślne zachowanie, do którego następnie wprowadzasz niewielkie modyfikacje za pomocą zastąpień, wtedy w JavaScript ma sens dziedziczenie prototypowe.

Jeśli jednak jesteś w sytuacji, w której używałbyś interfejsów w C # lub Javie, nie potrzebujesz żadnej konkretnej funkcji językowej w JavaScript. Nie ma potrzeby jawnego deklarowania czegoś, co reprezentuje interfejs i nie ma potrzeby oznaczania obiektów jako „implementujących” ten interfejs:

var duck = {
    quack: function() { ... }
};

duck.quack(); // we're satisfied it's a duck!

Innymi słowy, jeśli każdy „typ” obiektu ma swoje własne definicje „metod”, to dziedziczenie z prototypu nie ma wartości. Następnie zależy to od liczby przydzielonych instancji każdego typu. Jednak w wielu projektach modułowych występuje tylko jeden egzemplarz danego typu.

W rzeczywistości wiele osób sugerowało, że dziedziczenie implementacji jest złe . Oznacza to, że jeśli istnieją pewne typowe operacje dla typu, być może jest to bardziej zrozumiałe, jeśli nie są one umieszczane w klasie podstawowej / super, ale zamiast tego są ujawniane jako zwykłe funkcje w jakimś module, do którego przekazujesz obiekt (y) chcesz na nich operować.

Daniel Earwicker
źródło
2
Dobre wytłumaczenie. Czy zgodziłbyś się wtedy, że skoro uważasz prototypy za optymalizację, to zawsze można ich użyć do ulepszenia kodu? Zastanawiam się, czy są przypadki, w których używanie prototypów nie ma sensu, czy faktycznie wiąże się z obniżeniem wydajności.
opl
W dalszej części wspomniałeś, że „to zależy od liczby instancji każdego typu”. Ale przykład, do którego się odnosisz, nie wykorzystuje prototypów. Gdzie jest pojęcie przydzielania instancji (czy nadal używałbyś tutaj „nowego”)? Ponadto: powiedzmy, że metoda quack ma parametr - czy każde wywołanie duck.quack (param) spowoduje utworzenie nowego obiektu w pamięci (może nie ma to znaczenia, czy ma parametr, czy nie)?
opl
3
1. Chodziło mi o to, że gdyby istniała duża liczba wystąpień jednego typu kaczki, sensowne byłoby zmodyfikowanie przykładu tak, aby quackfunkcja była w prototypie, z którym jest połączonych wiele instancji kaczki. 2. Składnia literału obiektu { ... }tworzy instancję (nie ma potrzeby używania newz nią). 3. Wywołanie dowolnej funkcji JS powoduje utworzenie przynajmniej jednego obiektu w pamięci - nazywa się argumentsobiektem i przechowuje argumenty przekazane w wywołaniu: developer.mozilla.org/en/JavaScript/Reference/ ...
Daniel Earwicker
Dzięki, przyjąłem twoją odpowiedź. Ale nadal mam pewne zamieszanie z twoim punktem (1): nie rozumiem, co masz na myśli, mówiąc o „dużej liczbie przypadków jednego rodzaju kaczki”. Jak powiedziałeś w (3) za każdym razem, gdy wywołujesz funkcję JS, w pamięci tworzony jest jeden obiekt - więc nawet jeśli masz tylko jeden typ kaczki, czy nie przydzielałbyś pamięci za każdym razem, gdy wywołujesz funkcję kaczki (w w którym przypadku zawsze warto użyć prototypu)?
opl
11
+1 Porównanie z jQuery było pierwszym jasnym i zwięzłym wyjaśnieniem, kiedy i dlaczego używać prototypów, które przeczytałem. Dziękuję Ci bardzo.
GFoley83
46

Powinieneś używać prototypów, jeśli chcesz zadeklarować „niestatyczną” metodę obiektu.

var myObject = function () {

};

myObject.prototype.getA = function (){
  alert("A");
};

myObject.getB = function (){
  alert("B");
};

myObject.getB();  // This works fine

myObject.getA();  // Error!

var myPrototypeCopy = new myObject();
myPrototypeCopy.getA();  // This works, too.
KeatsKelleher
źródło
@keatsKelleher, ale możemy stworzyć niestatyczną metodę dla obiektu, po prostu definiując metodę wewnątrz funkcji konstruktora, używając thisprzykładu, this.getA = function(){alert("A")}prawda?
Amr Labib
17

Jednym z powodów używania wbudowanego prototypeobiektu jest wielokrotne powielanie obiektu, który będzie miał wspólną funkcjonalność. Dołączając metody do prototypu, możesz zaoszczędzić na powielaniu metod tworzonych dla każdej newinstancji. Jednak po dołączeniu metody do elementu prototypewszystkie wystąpienia będą miały dostęp do tych metod.

Powiedzmy, że masz Car()klasę / obiekt bazowy .

function Car() {
    // do some car stuff
}

następnie tworzysz wiele Car()instancji.

var volvo = new Car(),
    saab = new Car();

Teraz wiesz, że każdy samochód będzie musiał jeździć, włączać itd. Zamiast dołączać metodę bezpośrednio do Car()klasy (która zajmuje pamięć dla każdej utworzonej instancji), możesz zamiast tego dołączyć metody do prototypu (tworzenie tylko metod raz), dając tym samym dostęp do tych metod zarówno nowemu, jak volvoi saab.

// just mapping for less typing
Car.fn = Car.prototype;

Car.fn.drive = function () {
    console.log("they see me rollin'");
};
Car.fn.honk = function () {
    console.log("HONK!!!");
}

volvo.honk();
// => HONK!!!
saab.drive();
// => they see me rollin'
hellatan
źródło
2
w rzeczywistości jest to niepoprawne. volvo.honk () nie będzie działać, ponieważ całkowicie wymieniłeś prototypowy obiekt, a nie rozszerzyłeś go. Gdybyś miał zrobić coś takiego, działałoby tak, jak się spodziewasz: Car.prototype.honk = function () {console.log ('HONK');} volvo.honk (); // „HONK”
29er
1
@ 29er - tak jak napisałem ten przykład, masz rację. Kolejność ma znaczenie. Gdybym miał zachować ten przykład bez zmian , Car.prototype = { ... }musiałby przyjść przed wywołaniem a, new Car()jak pokazano w tym jsfiddle: jsfiddle.net/mxacA . Jeśli chodzi o twój argument, byłby to właściwy sposób: jsfiddle.net/Embnp . Zabawne jest to, że nie pamiętam odpowiedzi na to pytanie =)
hellatan
@hellatan możesz to naprawić, ustawiając konstruktor: Car na, ponieważ nadpisałeś właściwość prototypu literałem obiektu.
Josh Bedo
@josh dzięki za wskazanie tego. Zaktualizowałem swoją odpowiedź, aby nie nadpisywać prototypu literałem obiektu, tak jak powinno być od początku.
hellatan
12

Umieść funkcje na obiekcie prototypowym, gdy masz zamiar utworzyć wiele kopii określonego rodzaju obiektu i wszystkie muszą mieć wspólne zachowania. W ten sposób zaoszczędzisz trochę pamięci, mając tylko jedną kopię każdej funkcji, ale to tylko najprostsza korzyść.

Zmiana metod w obiektach prototypowych lub dodanie metod powoduje natychmiastową zmianę charakteru wszystkich wystąpień odpowiedniego typu (typów).

To, dlaczego robisz te wszystkie rzeczy, jest głównie funkcją twojego własnego projektu aplikacji i rodzajów rzeczy, które musisz zrobić w kodzie po stronie klienta. (Zupełnie inną historią byłby kod na serwerze; o wiele łatwiej wyobrazić sobie wykonywanie tam kodu „OO” na dużą skalę).

Spiczasty
źródło
więc kiedy tworzę instancję nowego obiektu z metodami prototypowymi (poprzez słowo kluczowe new), to obiekt ten nie otrzymuje nowej kopii każdej funkcji (tylko rodzaj wskaźnika)? Jeśli tak jest, dlaczego nie chcesz używać prototypu?
opl
jak @marcel, d'oh ... =)
hellatan
@opi tak, masz rację - nie ma kopii. Zamiast tego symbole (nazwy właściwości) w obiekcie prototypowym są po prostu jakby „tam” jako wirtualne części każdego obiektu instancji. Jedynym powodem, dla którego ludzie nie chcieliby się tym przejmować, byłyby przypadki, w których przedmioty są krótkotrwałe i odrębne lub gdy nie ma zbyt wielu „zachowań” do podzielenia się.
Pointy
3

Jeśli wyjaśnię terminem opartym na klasie, to Person jest klasą, walk () jest metodą prototypową. Więc walk () będzie istniał dopiero po utworzeniu instancji nowego obiektu z this.

Więc jeśli chcesz utworzyć kopie obiektu takiego jak Osoba, możesz utworzyć wielu użytkowników Prototyp jest dobrym rozwiązaniem, ponieważ oszczędza pamięć przez współdzielenie / dziedziczenie tej samej kopii funkcji dla każdego obiektu w pamięci.

Natomiast statyczność nie jest tak wielką pomocą w takim scenariuszu.

function Person(){
this.name = "anonymous";
}

// its instance method and can access objects data data 
Person.prototype.walk = function(){
alert("person has started walking.");
}
// its like static method
Person.ProcessPerson = function(Person p){
alert("Persons name is = " + p.name);
}

var userOne = new Person();
var userTwo = new Person();

//Call instance methods
userOne.walk();

//Call static methods
Person.ProcessPerson(userTwo);

Więc z tym bardziej przypomina metodę instancji. Podejście obiektu jest podobne do metod statycznych.

https://developer.mozilla.org/en/Introduction_to_Object-Oriented_JavaScript

Anil Namde
źródło