Definicja klas lub obiektów ECMAScript

Użycie zdefiniowanych przez użytkownika obiektów jest tylko częścią zdolności języka obiektowego, ale jego prawdziwą siłą jest możliwość tworzenia własnych klas i obiektów.

ECMAScript ma wiele metod do tworzenia obiektów lub klas.

Metoda fabryczna

Oryginalny sposób

Ponieważ atrybuty obiektu mogą być dynamicznie definiowane po utworzeniu obiektu, wielu deweloperów pisało podobne do poniższego kodu w początkowych latach wprowadzenia JavaScript:

var oCar = new Object;
oCar.color = "blue";
oCar.doors = 4;
oCar.mpg = 25;
oCar.showColor = function() {
  alert(this.color);
};

TIY

W powyższym kodzie tworzy się obiekt car. Następnie ustawia się kilka jego atrybutów: jego kolor to niebieski, ma cztery drzwi, a na galon paliwa można przejechać 25 mil. Ostatni atrybut jest wskaźnikiem do funkcji, co oznacza, że jest to metoda. Po wykonaniu tego kodu można używać obiektu car.

Jednak tutaj pojawia się problem, że może być konieczne utworzenie wielu instancji car.

Rozwiązanie: metoda fabryczna

Aby rozwiązać ten problem, deweloperzy stworzyli funkcje fabryczne, które mogą tworzyć i zwracać obiekty określonego typu.

Na przykład, funkcja createCar() może być używana do zawierania operacji tworzenia obiektu car, które były wcześniej wymienione:

function createCar() {
  var oTempCar = new Object;
  oTempCar.color = "blue";
  oTempCar.doors = 4;
  oTempCar.mpg = 25;
  oTempCar.showColor = function() {
    alert(this.color);
  };
  return oTempCar;
}
var oCar1 = createCar();
var oCar2 = createCar();

TIY

W tym miejscu, wszystkie kod z pierwszego przykładu zawiera się w funkcji createCar(). Ponadto, znajduje się tam dodatkowy wiersz kodu, który zwraca obiekt car jako wartość funkcji. Wywołanie tej funkcji tworzy nowy obiekt, przypisuje mu wszystkie niezbędne atrybuty i kopiuje obiekt car, o którym mówiliśmy wcześniej. W ten sposób możemy łatwo utworzyć dwie wersje obiektu car (oCar1 i oCar2), które mają identyczne atrybuty.

Przekazywanie parametrów do funkcji

Możemy również zmodyfikować funkcję createCar(), przekazując jej domyślne wartości dla różnych atrybutów, a nie tylko przypisywać atrybuty domyślne:

function createCar(sColor,iDoors,iMpg) {
  var oTempCar = new Object;
  oTempCar.color = sColor;
  oTempCar.doors = iDoors;
  oTempCar.mpg = iMpg;
  oTempCar.showColor = function() {
    alert(this.color);
  };
  return oTempCar;
}
var oCar1 = createCar("red",4,23);
var oCar2 = createCar("blue",3,25);
oCar1.showColor();		//wyświetla "red"
oCar2.showColor();		//wyświetla "blue"

TIY

Dodanie parametrów do funkcji createCar() umożliwia przypisanie wartości atrybutów color, doors i mpg do obiektu car, który ma być utworzony. To sprawia, że dwa obiekty mają te same atrybuty, ale różne wartości atrybutów.

Definiowanie metod obiektu poza funkcją fabryczną

Chociaż ECMAScript staje się coraz bardziej formalny, metody tworzenia obiektów są pomijane, a ich standaryzacja wciąż spotyka się z oporem. Częściowo z powodu przyczyn semantycznych (wygląda to mniej profesjonalnie niż użycie operatora new z konstruktorem), częściowo z powodu przyczyn funkcjonalnych. Przyczyny funkcjonalne leżą w konieczności tworzenia obiektów w ten sposób. W poprzednim przykładzie, za każdym razem, gdy wywoływana jest funkcja createCar(), tworzy się nową funkcję showColor(), co oznacza, że każdy obiekt ma swoją własną wersję showColor(). W rzeczywistości, każdy obiekt dzieli tę samą funkcję.

Niektórzy deweloperzy definiują metody obiektu poza funkcją fabryczną, a następnie wskazują na nią za pomocą atrybutu, aby uniknąć tego problemu:

function showColor() {
  alert(this.color);
}
function createCar(sColor,iDoors,iMpg) {
  var oTempCar = new Object;
  oTempCar.color = sColor;
  oTempCar.doors = iDoors;
  oTempCar.mpg = iMpg;
  oTempCar.showColor = showColor;
  return oTempCar;
}
var oCar1 = createCar("red",4,23);
var oCar2 = createCar("blue",3,25);
oCar1.showColor();		//wyświetla "red"
oCar2.showColor();		//wyświetla "blue"

TIY

W powyższym przepisowanym kodzie, funkcja showColor() została zdefiniowana przed funkcją createCar(). Wewnątrz createCar(), przypisuje się obiektowi wskaźnik do istniejącej funkcji showColor(). W funkcjonalności rozwiązuje to problem powtarzających się obiektów funkcji; ale z semantycznego punktu widzenia, funkcja ta nie wygląda zbyt dobrze jako metoda obiektu.

Wszystkie te problemy wywołałyDefiniowane przez deweloperapojawiła się.

Metoda konstruktor

Tworzenie konstruktora jest tak łatwe jak tworzenie funkcji fabrycznych. Pierwszym krokiem jest wybór nazwy klasy, która jest nazwą konstruktora. Zgodnie z konwencją, pierwsza litera nazwy jest duża, aby odróżnić ją od zmiennych o małej pierwszej literze. Poza tym konstruktor wygląda podobnie do funkcji fabrycznej. Weźmy pod uwagę poniższy przykład:

function Car(sColor,iDoors,iMpg) {
  this.color = sColor;
  this.doors = iDoors;
  this.mpg = iMpg;
  this.showColor = function() {
    alert(this.color);
  };
}
var oCar1 = new Car("red",4,23);
var oCar2 = new Car("blue",3,25);

TIY

Poniżej wyjaśnimy różnicę między powyższym kodem a metodą fabryczną. Najpierw w konstruktorze nie tworzy się obiektu, ale używa się klucza this. Podczas tworzenia obiektu za pomocą operatora new, przed wykonaniem pierwszego wiersza kodu tworzy się obiekt, jedynie za pomocą this można uzyskać dostęp do tego obiektu. Następnie można bezpośrednio przypisać atrybuty this, domyślnie jest to wartość zwracana przez konstruktor (nie jest konieczne wyraźne użycie operatora return).

Teraz, tworzenie obiektów za pomocą operatora new i nazwy klasy Car jest bardziej podobne do tworzenia typowych obiektów w ECMAScript.

Możesz zapytać, czy ten sposób ma te same problemy z zarządzaniem funkcjami, co poprzedni sposób? Tak.

Jak funkcje fabryczne, konstruktory powtarzają tworzenie funkcji, tworząc oddzielną wersję funkcji dla każdego obiektu. Jednak, jak funkcje fabryczne, można również przedefiniować konstruktora za pomocą zewnętrznej funkcji, co nie ma żadnego znaczenia semantycznego. To jest dokładnie zaletą metody prototypowej.

Metoda prototypowa

Ta metoda wykorzystuje właściwość prototype obiektu, którą można rozumieć jako prototyp, na którym opiera się tworzenie nowych obiektów.

W tym przypadku najpierw używamy pustego konstruktora do ustawienia nazwy klasy. Następnie wszystkie atrybuty i metody są bezpośrednio przypisane do właściwości prototype. Przekształciliśmy poprzedni przykład, kod wygląda tak:

function Car() {
}
Car.prototype.color = "blue";
Car.prototype.doors = 4;
Car.prototype.mpg = 25;
Car.prototype.showColor = function() {
  alert(this.color);
};
var oCar1 = new Car();
var oCar2 = new Car();

TIY

W tym kodzie najpierw definiujemy funkcję konstruktora (Car), która nie zawiera żadnego kodu. Następne linie kodu dodają atrybuty do właściwości prototype, aby zdefiniować atrybuty obiektu Car. Wywołanie new Car() natychmiast przypisuje wszystkie atrybuty prototypu do tworzonego obiektu, co oznacza, że wszystkie instancje Car przechowują wskaźniki do funkcji showColor(). W sensie semantycznym wszystkie atrybuty wydają się należeć do jednego obiektu, co rozwiązuje problemy z poprzednimi metodami.

Oprócz tego, w ten sposób można używać operatora instanceof do sprawdzania typu obiektu, do którego wskazuje zmienna. Dlatego poniższy kod wyświetli TRUE:

alert(oCar1 instanceof Car);	//Wyświetla "true"

Problem metody prototypowej

Metoda prototypowa wydaje się być dobrym rozwiązaniem. Niestety, nie jest to w pełni zadowalające.

Najpierw, ta funkcja konstruktora nie przyjmuje parametrów. Dzięki prototypom, nie można inicjalizować wartości atrybutów poprzez przekazywanie parametrów do funkcji konstruktora, ponieważ atrybuty color, doors i mpg dla Car1 i Car2 są równe "blue", 4 i 25. Oznacza to, że wartości domyślne atrybutów można zmieniać tylko po utworzeniu obiektu, co jest dość denerwujące, ale to jeszcze nie wszystko. Prawdziwy problem pojawia się, gdy atrybuty wskazują na obiekt, a nie na funkcję. Funkcje są 共享的,nie powodują problemów, ale obiekty rzadko są współdzielone między wieloma instancjami. Pomyślmy o poniższym przykładzie:

function Car() {
}
Car.prototype.color = "blue";
Car.prototype.doors = 4;
Car.prototype.mpg = 25;
Car.prototype.drivers = new Array("Mike","John");
Car.prototype.showColor = function() {
  alert(this.color);
};
var oCar1 = new Car();
var oCar2 = new Car();
oCar1.drivers.push("Bill");
alert(oCar1.drivers); // Wyświetla "Mike,John,Bill"
alert(oCar2.drivers); // Wyświetla "Mike,John,Bill"

TIY

W powyższym kodzie, atrybut drivers jest wskaźnikiem do obiektu Array, który zawiera dwa imiona "Mike" i "John". Ponieważ drivers jest wartością referencyjną, dwa egzemplarze obiektu Car wskazują na ten sam obiekt. Oznacza to, że dodanie wartości "Bill" do oCar1.drivers, jest również widoczne w oCar2.drivers. Wyświetlenie dowolnego z tych wskaźników wynosi string "Mike,John,Bill".

Ponieważ tworzenie obiektów wiąże się z tak wieloma problemami, można zapytać, czy istnieje racjonalny sposób tworzenia obiektów? Odpowiedź brzmi tak, wymagane jest jednoczesne użycie konstruktora i prototypu.

Mieszany sposób konstruktora/prototypu

Użycie jednocześnie konstruktora i prototypu pozwala na tworzenie obiektów w sposób podobny do innych języków programowania. Koncept ten jest bardzo prosty, polega na zdefiniowaniu wszystkich nie-funkcyjnych atrybutów obiektu za pomocą konstruktora, a funkcji atrybutów (metod) za pomocą prototypu. W rezultacie, wszystkie funkcje są tworzone tylko raz, a każdy obiekt ma swoją osobną instancję atrybutów obiektowych.

Przerobiliśmy poprzedni przykład, kod wygląda tak:

function Car(sColor,iDoors,iMpg) {
  this.color = sColor;
  this.doors = iDoors;
  this.mpg = iMpg;
  this.drivers = new Array("Mike","John");
}
Car.prototype.showColor = function() {
  alert(this.color);
};
var oCar1 = new Car("red",4,23);
var oCar2 = new Car("blue",3,25);
oCar1.drivers.push("Bill");
alert(oCar1.drivers); // Wyświetla "Mike,John,Bill"
alert(oCar2.drivers); // Wyświetla "Mike,John"

TIY

Teraz jest to bardziej podobne do tworzenia zwykłych obiektów. Wszystkie nie-funkcyjne atrybuty są tworzone w konstruktorze, co oznacza, że można używać parametrów konstruktorów do przypisywania wartości domyślnych atrybutom. Ponieważ tworzy się tylko jeden egzemplarz funkcji showColor(), nie ma marnotrawstwa pamięci. Ponadto, dodanie wartości "Bill" do tablicy drivers obiektu oCar1, nie wpływa na tablicę oCar2, więc wartości tych tablic wynoszą "Mike,John,Bill" dla oCar1.drivers, a "Mike,John" dla oCar2.drivers. Ponieważ używa się prototypu, można nadal używać operatora instanceof do określania typu obiektu.

This is the main method adopted by ECMAScript, which has the characteristics of other methods without their side effects. However, some developers still think that this method is not perfect.

Dynamic prototype methods

For developers accustomed to using other languages, using the mixed constructor/proto method may not feel as harmonious. After all, when defining a class, most object-oriented languages visually encapsulate properties and methods. Consider the following Java class:

class Car {
  public String color = "blue";
  public int doors = 4;
  public int mpg = 25;
  public Car(String color, int doors, int mpg) {
    this.color = color;
    this.doors = doors;
    this.mpg = mpg;
  }
  public void showColor() {
    System.out.println(color);
  }
}

Java packages all properties and methods of the Car class well, so seeing this code you know what it is supposed to do, it defines the information of an object. Critics of the mixed constructor/proto method argue that it is illogical to find properties within the constructor and methods outside of it. Therefore, they designed dynamic prototype methods to provide a more friendly coding style.

Dynamic prototype methods have the same basic idea as mixed constructor/proto methods, that is, non-function properties are defined within the constructor, while function properties are defined using prototype properties. The only difference is the position where the methods are assigned to the object. Below is the Car class rewritten using dynamic prototype methods:

function Car(sColor,iDoors,iMpg) {
  this.color = sColor;
  this.doors = iDoors;
  this.mpg = iMpg;
  this.drivers = new Array("Mike","John");
  if (typeof Car._initialized == "undefined") {
    Car.prototype.showColor = function() {
      alert(this.color);
    };
    Car._initialized = true;
  }
}

TIY

Ten konstruktor nie zmienia się, dopóki nie zostanie sprawdzona wartość typeof Car._initialized, która jest równa "undefined". To jest najważniejsza część metody dynamicznego prototypu. Jeśli ta wartość jest niezdefiniowana, konstruktor będzie kontynuował definiowanie metod obiektu w sposób prototypowy, a następnie ustawia Car._initialized na true. Jeśli wartość ta jest zdefiniowana (jej wartość jest true, wartość typeof jest Boolean), metoda ta już nie zostanie utworzona. Krótko mówiąc, metoda ta używa znacznika (_initialized) do określenia, czy już nadano jakieś metody prototypowi. Metoda jest tworzone i przypisywana tylko raz, co ucieszy tradycyjnych deweloperów OOP, bo kod wygląda bardziej jak definicje klas w innych językach.

Mieszany sposób fabryczny

Ten sposób jest zazwyczaj stosowany jako alternatywa, gdy nie można zastosować poprzedniego sposobu. Celem tego jest stworzenie fałszywego konstruktora, który zwraca nową instancję innego obiektu.

Ten kod wygląda bardzo podobnie do funkcji fabrykowej:

function Car() {
  var oTempCar = new Object;
  oTempCar.color = "blue";
  oTempCar.doors = 4;
  oTempCar.mpg = 25;
  oTempCar.showColor = function() {
    alert(this.color);
  };
  return oTempCar;
}

TIY

Różni się od klasycznego sposobu, używając operatora new, co sprawia, że wygląda jak prawdziwy konstruktor:

var car = new Car();

Ponieważ w wewnętrznym konstruktora Car() jest wywoływany operator new, zignoruje się drugi operator new (poza konstruktorem), obiekt utworzony wewnątrz konstruktora jest przekazywany do zmiennej car.

Ten sposób ma te same problemy w zarządzaniu metodami obiektowymi, co klasyczny sposób. Silnie zaleca się, aby unikać tego sposobu, chyba że jest to absolutnie konieczne.

Wybór metody

Jak już wspomniano, najbardziej szeroko stosowanym jest mieszany sposób konstruktora/protototypu. Ponadto, dynamiczne metody surowe są również popularne i są funkcjonalnie równoważne sposóbowi konstruktora/protototypu. Można zastosować jedno z tych dwóch sposobów. Jednak unikaj单独使用单独的 klasycznego konstruktora lub prototypu, ponieważ to może wprowadzać problemy do kodu.

Przykład

Interesujący aspekt obiektów to sposób, w jaki rozwiązują problemy. Jednym z najczęstszych problemów w ECMAScript jest wydajność łączenia ciągów znaków. Jak w innych językach, ciągi znaków w ECMAScript są niemodyfikowalne, co oznacza, że ich wartość nie może się zmienić. Rozważ poniższy kod:

var str = "hello ";
str += "world";

W rzeczywistości, te kroki są wykonywane w tle w następujący sposób:

  1. Utworzyć string przechowujący "hello ".
  2. Utworzyć string przechowujący "world".
  3. Utworzyć string przechowujący wynik połączenia.
  4. Skopiować bieżącą zawartość str do wyniku.
  5. Skopiować "world" do wyniku.
  6. Zaktualizować str, aby wskazywał na wynik.

Każdy raz, gdy wykonuje się łączenie stringów, jest wykonywany krok 2 do 6, co sprawia, że ta operacja jest bardzo zasobożerna. Jeśli powtórzy się to setki, a nawet tysiące razy, może to spowodować problemy z wydajnością. Rozwiązaniem jest przechowywanie stringów w obiekcie Array, a następnie tworzenie końcowego stringa za pomocą metody join() (parametr to pusty string). Wyobraź sobie, że zastąpi się poniższy kod miejscem wcześniejszego kodu:

var arr = new Array();
arr[0] = "hello ";
arr[1] = "world";
var str = arr.join("");

W ten sposób, niezależnie od liczby stringów wprowadzonych do tablicy, nie będzie to problemem, ponieważ połączenie następuje tylko podczas wywoływania metody join(). W tym przypadku, wykonywane są następujące kroki:

  1. Utworzyć string przechowujący wynik
  2. Skopiować każdy string do odpowiedniego miejsca w wyniku

Choć to rozwiązanie jest dobre, to istnieje lepszy sposób. Problemem jest to, że ten kod nie odzwierciedla dokładnie jego intencji. Aby uczynić go łatwiejszym do zrozumienia, można upakować tę funkcjonalność w klasę StringBuffer:

function StringBuffer () {
  this._strings_ = new Array();
}
StringBuffer.prototype.append = function(str) {
  this._strings_.push(str);
};
StringBuffer.prototype.toString = function() {
  zwróć this._strings_.join("");
};

Najważniejszą rzeczą w tym kodzie jest atrybut strings, który jest prywatną właściwością. Ma on tylko dwie metody, tj. append() i toString(). Metoda append() przyjmuje jeden parametr, który dodaje go do tablicy stringów, metoda toString() wywołuje metodę join tablicy, zwracając rzeczywiście połączone stringi. Aby połączyć grupę stringów za pomocą obiektu StringBuffer, można użyć poniższego kodu:

var buffer = new StringBuffer();
buffer.append("hello ");
buffer.append("world");
var result = buffer.toString();

TIY

Można przetestować wydajność obiektu StringBuffer i tradycyjnej metody łączenia ciągów znaków za pomocą poniższego kodu:

var d1 = new Date();
var str = "";
for (var i=0; i < 10000; i++) {
    str += "text";
}
var d2 = new Date();
document.write("Concatenation with plus: ")
 + (d2.getTime() - d1.getTime()) + " milliseconds");
var buffer = new StringBuffer();
d1 = new Date();
for (var i=0; i < 10000; i++) {
    buffer.append("text");
}
var result = buffer.toString();
d2 = new Date();
document.write("<br />Concatenation with StringBuffer: ")
 + (d2.getTime() - d1.getTime()) + " milliseconds");

TIY

Ten kod przeprowadza dwa testy łączenia ciągów znaków, pierwszy z użyciem plusa, drugi z użyciem klasy StringBuffer. Każda operacja łączy 10000 ciągów znaków. Daty d1 i d2 są używane do oszacowania czasu potrzebującego na wykonanie operacji. Proszę zauważyć, że przy tworzeniu obiektu Date, jeśli nie podano argumentów, obiekt jest nadawany bieżąca data i czas. Aby obliczyć, ile czasu zajęło połączenie, wystarczy odjąć wartości milisekund dat (zwracane przez metodę getTime()). To jest powszechna metoda oceny wydajności JavaScript. Wyniki tego testu mogą pomóc w porównaniu wydajności używania klasy StringBuffer z użyciem plusa.