MDN’s new design is in Beta! A sneak peek: https://blog.mozilla.org/opendesign/mdns-new-design-beta/

Wprowadzenie do programowania obiektowego w języku JavaScript

To tłumaczenie jest niekompletne. Pomóż przetłumaczyć ten artykuł z języka angielskiego.

JavaScript jest zorientowany obiektowo do szpiku kości dzięki potężnym, elastycznym możliwościom realizacji OOP. Ten artykuł zawiera wprowadzenie do programowania obiektowego (ogółem), analizuje model obiektowy w JavaScript i w końcu demonstruje aspekty programowania obiektowego w JavaScript.

Przegląd JavaScript

Jeśli nie czujesz się pewnie w zagadnieniach dotyczących JavaScript takich, jak zmienne, typy, funkcje oraz zasięg, możesz przeczytać o nich w Ponownym wprowadzeniu do JavaScript (angielski). Możesz także zasięgnąć wiedzy zawartej w Core JavaScript 1.5 Guide.

Programowanie zorientowane obiektowo

Programowanie zorientowane obiektowo jest paradygmatem programowania, który korzysta z abstrakcji do tworzenia modeli opartych na świecie rzeczywistym. Stosuje on kilka technik z poprzednio ustanowionych paradygmatów, np. modułowość, polimorfizm czy enkapsulację. Obecnie wiele popularnych języków programowania (takich, jak Java, JavaScript, C#, C++, Python, PHP, Ruby i Objective-C) wspierają programowanie zorientowane obiektowo (OOP - z ang. "object-oriented programming").

Programowanie zorientowane obiektowo może być rozumiane jako sposób projektowania oprogramowania stosujący kolekcję powiązanych ze sobą obiektów, w przeciwieństwie do tradycyjnego punktu widzenia, gdzie program może być rozumiany jako zestaw funkcji lub, po prostu, jako lista instrukcji przekazywanych do komputera. W OOP każdy obiekt jest zdolny odbierać wiadomości, przetwarzać dane i wysyłać wiadomości do innych obiektów. Każdy obiekt może być rozumiany jako niezależna mała maszyna pełniąca odrębną rolę lub odpowiedzialność.

Programowanie obiektowe ma na celu promować większą elastyczność i łatwość rozwoju w programowaniu. Jest ono bardzo popularne przy tworzeniu oprogramowania na dużą skalę. Dzięki silnemu naciskowi na modułowość, kod programu zorientowany obiektowo jest łatwiejszy do napisania i późniejszego zrozumienia, stając się łatwiejszym do bezpośredniej analizy, kodowania i rozumienia złożonych sytuacji i procedur niż mniej modułowe metody programowania.2

Terminologia

Przestrzeń nazw (ang. "namespace")
Przestrzeń pozwalająca programiście na zawarcie wszystkich funkcjonalności pod unikalną nazwą, właściwą dla danej aplikacji.
Klasa (ang. "class")
Definiuje własności obiektu.
Obiekt (ang. "object")
Instancja (byt, twór) klasy.
Właściwość (ang. "property")
Własność obiektu, np. kolor.
Metoda (ang. "method")
Zdolność (czynność) obiektu, np. chodzenie (idź).
Konstruktor (ang. "constructor")
Metoda wywoływana w momencie inicjalizacji obiektu.
Dziedziczenie (ang. "inheritance")
Klasa może dziedziczyć własności od innej klasy.
Hermetyzacja (lub enkapsulacja - ang. "encapsulation")
Klasa definiuje tylko własności obiektu, podczas gdy metoda definiuje tylko sposób realizacji.
Abstrakcja (ang. "abstraction")
Koniunkcja złożonego dziedziczenia, metod, właściwości obiektu musi dobrze oddawać model rzeczywistości.
Polimorfizm (ang. "polymorphism")
Poli znaczy "wiele", a morfizm oznacza "formy". Różne klasy mogą definiować takie same metody albo właściwości.

Bardziej obszerną definicję programowania obiektowego można znaleźć w Programowaniu obiektowym na Wikipedii.

Programowanie oparte na prototypie

Programowanie prototypowe jest stylem programowania obiektowego, w którym klasy nie są obecne, a ponowne wykorzystanie zachowań (w językach opartych na klasach znane jako dziedziczenie) jest realizowane przez proces dekoracji istniejących obiektów, które służą jako prototypy. Ten model jest znany również jako programowanie "bezklasowe", "zorientowane prototypowo" lub "oparte na instancji".

Oryginalnym (i najbardziej kanonicznym) przykładem języka opartego na prototypie jest język programowaina Self stworzony przez Davida Ungara i Randalla Smitha. Niemniej jednak, programowanie w stylu bezklasowym stało się ostatnimi czasy bardzo popularne i zostało zaimplementowane w takich językach, jak Javascript, Cecil, NewtonScript, Io, MOO, REBOL, Kevo, Squeak (podczas używania frameworka Viewer do manipulacji komponentami Morphic) i kilku innych.2

OOP w języku JavaScript

Przestrzeń nazw

Przestrzeń nazw jest pojemnikiem pozwalającym programiście na zawarcie wszystkich funkcjonalności pod unikalną nazwą, właściwą dla danej aplikacji. W JavaScript przestrzeń nazw jest po prostu obiektem przechowującym metody, właściwości i inne obiekty. Cel przyświecający przestrzeniom nazw w JavaScript jest prosty: utworzony zostaje jeden globalny obiekt, a wszystkie zmienne, metody i funkcje stają się właściwościami tego obiektu. Użycie przestrzeni nazw zmniejsza również ryzyko kolizji nazw w aplikacji.

Obiekt jest przestrzenią nazw:

Stwórzmy globalny obiekt o nazwie MYAPP

// globalna przestrzeń nazw
var MYAPP = MYAPP || {};

W powyższym przykładzie najpierw sprawdzamy czy MYAPP jest już zdefiniowany w tym samym lub innym pliku. Jeśli tak, używamy istniejącego globalnego obiektu MYAPP. W przeciwnym razie tworzymy pusty obiekt MYAPP, który zenkapsuluje metody, funkcje, zmienne i obiekty.

Możemy również utworzyć podrzędną przestrzeń nazw:

// pod-przestrzeń nazw
MYAPP.event = {};

Poniżej znajduje się kod tworzący przestrzeń nazw i dodający zmienne, funkcje i metody:

// Utwórz pojemnik MYAPP.commonMethod na typowe metody i właściwości
MYAPP.commonMethod = {
  regExForName: "", // zdefiniuj wyrażenie regularne do walidacji nazwiska
  regExForPhone: "", // zdefiniuj wyrażenie regularne do walidacji numeru telefonu
  validateName: function(name){
    // Zrób coś z nazwiskiem. Możesz użyć zmiennej regExForName
    // użycie "this.regExForName"
  },
 
  validatePhoneNo: function(phoneNo){
    // zrób coś z numerem telefonu
  }
}

// Obiekt razem z deklaracją metod
MYAPP.event = {
    addListener: function(el, type, fn) {
    // jakiś kod
    },
   removeListener: function(el, type, fn) {
    // jakiś kod
   },
   getEvent: function(e) {
   // jakiś kod
   }
  
   // Można dodać kolejne metody i właściwości
}

// Składnia do użycia metody AddListener:
MYAPP.event.addListener("yourel", "type", callback);

Obiekty wbudowane

JavaScript posiada kilka obiektów wbudowanych, na przykład Math, Object, Array, i String. Poniższy przykład pokazuje, jak użyć obiektu Math do pobrania pseudo-losowej liczby używając metody random().

alert(Math.random());
Notka: Ten i wszystkie dalsze przykłady zakładają, że istnieje funkcja globalna alert (taka, jak ta zaimplementowana w przeglądarkach internetowych). Tak naprawdę funkcja alert nie jest częścią języka JavaScript.

Artykuł Core JavaScript 1.5 Reference:Global Objects zawiera listę wszystkich obiektów wbudowanych w JavaScript.

Każdy obiekt w JavaScript jest instancją obiektu Object i tym samym dziedziczy jego wszystkie właściwości i metody.

Własne obiekty

Klasa

JavaScript jest językiem opartym na prototypie, w którym nie występuje pojęcie klasy, w przeciwieństwie do języków takich, jak C++ czy Java. Fakt ten bywa dezorientujący dla programistów przyzwyczajonych do języków z pojęciem klasy. Zamiast klas, JavaScript stosuje funkcje. Zdefiniowanie klasy ogranicza się do prostej czynności, jaką jest zdefiniowanie funkcji. W poniższym przykładzie definiujemy nową klasę Person.

function Person() { } 
or 
var Person = function(){ }

Obiekt (instancja klasy)

Żeby utworzyć nową instancję obiektu obj, używamy wyrażenia new obj, przypisując jego wynik (który jest typu obj) do zmiennej, żeby później mieć do niego dostęp.

W poniższym przykładzie definiujemy klasę Person i tworzymy dwie instancje (person1 i person2).

function Person() { }
var person1 = new Person();
var person2 = new Person();
Zobacz również Object.create, który jest nową metodą instancjalizacji.

Konstruktor

Konstruktor jest wywoływany w momencie instancjalizacji (moment, w którym instancja obiektu zostaje utworzona). Konstruktor jest metodą klasy. W JavaScript, funkcja służy za konstruktor obiektu. Nie ma jednak wyraźnej potrzeby definiowania konstruktora. Każda akcja zadeklarowana w konstruktorze zostanie wykonana w momencie utworzenia obiektu.

Konstruktor jest używany do ustawienia właściwości obiektu lub do wywołania metod przygotowujących obiekt do użytku.

W poniższym przykładzie konstruktor klasy Person wyświetla ostrzeżenie w momencie kiedy Person zostaje utworzony.

function Person() {
  alert('Person został utworzony');
}

var person1 = new Person();
var person2 = new Person();

Właściwość (atrybut obiektu)

Właściwości są zmiennymi zawartymi wewnątrz klasy. Każda instancja obiektu posiada te właściwości. Właściwości powinny być ustawiane we właściwości prototype klasy (funkcji), dzięki czemu dziedziczenie zadziała prawidłowo.

Dostęp do właściwości z wnętrza klasy odbywa się za pomocą słowa kluczowego this, które odnosi się do aktualnego obiektu. Dostęp (odczyt lub zapis) do właściwości poza klasą odbywa się za pomocą składni: NazwaInstancji.Wlasciwosc; jest to taka sama składnia, jak w językach C++, Java i szeregu innych języków.

W poniższym przykładzie definiujemy właściwość firstName dla klasy Person i robimy to w momencie utworzenia obiektu.

function Person(firstName) {
  this.firstName = firstName;
  alert('Person instantiated');
}

var person1 = new Person('Alice');
var person2 = new Person('Bob');

// Pokaż właściwości firstName obiektów
alert('person1 nazywa się ' + person1.firstName); // komunikat "person1 nazywa się Alice"
alert('person2 nazywa się ' + person2.firstName); // komunikat "person2 nazywa się Bob"

Metody

Metody opierają się na tej samej logice, co właściwości; różnica polega na tym, że są one funkcjami i definiuje się je jak funkcje. Wywołanie metody wygląda podobnie do wywołania właściwości, z tym, że dodajemy ( ) na końcu nazwy metody, czasami z argumentami. Żeby zdefiniować metodę, przypisujemy funkcję do jakiejś właściwości obiektu prototype klasy; nazwa właściwości staje się nazwą metody, po jakiej wywołamy ją na obiekcie.

W poniższym przykładzie definiujemy i używamy metodę sayHello() dla klasy Person.

function Person(firstName) {
  this.firstName = firstName;
}

Person.prototype.sayHello = function() {
  alert("Hello, I'm " + this.firstName);
};

var person1 = new Person("Alice");
var person2 = new Person("Bob");

// wywołanie metody sayHello klasy Person
person1.sayHello(); // komunikat "Hello, I'm Alice"
person2.sayHello(); // komunikat "Hello, I'm Bob"

W JavaScript metody to zwykłe funkcje, które są przypisane do obiektu jako jego właściwości, dzięki czemu mogą być wywoływane w jego kontekście. Przyjrzyj się natępującemu przykładowi kodu:

function Person(firstName) {
  this.firstName = firstName;
}

Person.prototype.sayHello = function() {
  alert("Hello, I'm " + this.firstName);
};

var person1 = new Person("Alice");
var person2 = new Person("Bob");
var helloFunction = person1.sayHello;

person1.sayHello();                                 // komunikat "Hello, I'm Alice"
person2.sayHello();                                 // komunikat "Hello, I'm Bob"
helloFunction();                                    // komunikat "Hello, I'm undefined" (lub niepowodzenie
                                                    // wyświetlające TypeError w trybie strict)
alert(helloFunction === person1.sayHello);          // komunikat true
alert(helloFunction === Person.prototype.sayHello); // komunikat true
helloFunction.call(person1);                        // komunikat "Hello, I'm Alice"

Jak pokazuje powyższy przykład, wszystkie odniesienia do funkcji sayHello  to w obiekcie person1, w Person.prototype, w helloFunctionvariable, itd. — dotyczą tej samej funkcji. W trakcie wywołania funkcji, wartość this zależy od tego, jak ją wywołamy. W typowym przypadku, gdzie wywołujemy funkcję jako metodę obiektu — person1.sayHello() — this odnosi się do obiektu, z którego funkcja pochodzi (person1), stąd person1.sayHello() używa nazwy "Alice", a person2.sayHello() używa nazwy "Bob". Natomiast wywołanie funkcji ze zmiennej — helloFunction() — ustawia this na obiekt globalny (window w przypadku przeglądarek). Ponieważ ten obiekt najprawdopodobniej nie posiada właściwości firstName, ostatecznie otrzymujemy komunikat "Hello, I'm undefined". (Tak będzie w trybie loose; byłoby inaczej [błąd] w trybie strict, ale nie będziemy go tutaj opisywać, żeby nie wprowadzać zamieszania). Możemy też ustawić this wedle uznania, używając funkcji call (lub apply), tak jak pokazuje ostatni przykład.

Więcej na temat this w Function#call oraz Function#apply

Dziedziczenie

Dziedziczenie jest sposobem na stworzenie klasy jako specjalistycznej wersji jednej lub większej ilości klas (JavaScript wspiera tylko dziedziczenie pojedyncze). Taka wyspecjalizowana klasa jest często nazywana dzieckiem, natomiast ta druga — rodzicem. W JavaScript osiąga się to poprzez przypisanie klasy rodzica do klasy dziecka, a następnie wyspecjalizowaniu jej. W nowoczesnych przeglądarkach można również użyć Object.create do implementacji dziedziczenia.

JavaScript nie wykrywa właściwości klasy dziecka prototype.constructor (zobacz Core JavaScript 1.5 Reference:Global Objects:Object:prototype), więc musimy tego dokonać ręcznie.

W poniższym przykładzie definiujemy klasę Student jako dziecko klasy Person. Następnie definiujemy ponownie metodę sayHello() oraz dodajemy metodę sayGoodBye().

// Definicja konstruktora Person
function Person(firstName) {
  this.firstName = firstName;
}

// Dodajemy kilka metod do Person.prototype
Person.prototype.walk = function(){
  alert("I am walking!");
};
Person.prototype.sayHello = function(){
  alert("Hello, I'm " + this.firstName);
};

// Definiujemy konstruktor
function Student(firstName, subject) {
  // Wywołujemy konstruktor rodzica (używając Function#call) upewniając się, że "this"
  // zostanie ustawione poprawnie podczas wywołania
  Person.call(this, firstName);

  // inicjalizujemy właściwości odpowiednie dla studenta
  this.subject = subject;
};

// Tworzymy obiekt Student.prototype, który dziedziczy po Person
// Uwaga: Typowym błędem w takich przypadkach jest użycie "new Person()" do utworzenia Student.prototype.
// Jest to niepoprawne z kilku powodów, nie wspominając o tym,
// Nie mielibyśmy jakiej wartości podać jako argument "firstName".
// Prawidłowym miejscem na wywołanie konstruktora Person jest to powyżej, w konstruktorze Student
Student.prototype = Object.create(Person.prototype);

// Ustawiamy właściwość "constructor" na obiekt Student
Student.prototype.constructor = Student;

// Zmieniamy metodę "sayHello"
Student.prototype.sayHello = function(){
  alert("Hello, I'm " + this.firstName + ". I'm studying " + this.subject + ".");
};

// Dodajemy metodę "sayGoodBye"
Student.prototype.sayGoodBye = function(){
  alert("Goodbye!");
};

// Przykład użycia:
var student1 = new Student("Janet", "Applied Physics");
student1.sayHello();   // "Hello, I'm Janet. I'm studying Applied Physics."
student1.walk();       // "I am walking!"
student1.sayGoodBye(); // "Goodbye!"

// sprawdzamy poprawność działania "instanceof"
alert(student1 instanceof Person);  // true 
alert(student1 instanceof Student); // true

Jeśli chodzi o linię Student.prototype = Object.create(Person.prototype); w starszych silnikach JavaScript, nie posiadających metody Object.create, można użyć tzw. "polyfill" (aka "shim", patrz artykuł powyżej), czyli funkcję, która stanowi swego rodzaju łatkę zapewniającą kompatybilność wsteczną danej funkcjonalności. Można też samemu napisać funkcję dającą taki sam efekt:

function createObject(proto) {
    function ctor() { }
    ctor.prototype = proto;
    return new ctor();
}

// Przykład użycia:
Student.prototype = createObject(Person.prototype);
Zobacz Object.create, żeby poznać wszystkie jego możliwości oraz znaleźć łatkę dla starszych silników JavaScript.

Enkapsulacja (hermetyzacja)

W poprzednim przykładzie klasa Student nie musiała wiedzieć, w jaki sposób metoda walk() klasy Person została zaimplementowana, ale wciąż mogła jej używać; klasa Student nie musi ponownie definiować tej metody, dopóki nie chcemy jej zmienić. To zjawisko nazywamy enkapsulacją, czyli każda klasa dziedziczy metody swojego rodzica i definiuje własne tylko wtedy, gdy chce coś zmienić.

Abstrakcja

Abstrakcja jest mechanizmem, który pozwala modelować aktualnie rozpatrywany problem. Może to być osiągane przez dziedziczenie (specjalizację) lub kompozycję. JavaScript osiąga specjalizację dzięki dziedziczeniu, a kompozycję dzięki umożliwieniu instancjom klas bycie wartościami atrybutów innych obiektów.

Klasa Function w JavaScript dziedziczy po klasie Object (jest to przykład specjalizacji modelu), natomiast właściwość Function.prototype jest instancją Object (co z kolei jest przykładem kompozycji).

var foo = function(){};
alert( 'foo is a Function: ' + (foo instanceof Function) );
alert( 'foo.prototype is an Object: ' + (foo.prototype instanceof Object) );

Polimorfizm

Tak, jak wszystkie metody i właściwości są zdefiniowane wewnątrz właściwości prototype, tak różne klasy mogą definiować metody z tą samą nazwą; metody mają zasięg ograniczony do klasy, w której zostały zdefiniowane. Ma to rację bytu tylko w przypadku, gdy dwie klasy nie są w relacji rodzic-dziecko (kiedy jedna nie dziedziczy po drugiej w łańcuchu dziedziczenia).

Uwagi

Techniki implementacji programowania zorientowanego obiektowo zaprezentowane w tym artykule nie są jedynymi, jakie umożliwia JavaScript, dzięki czemu sposób osiągnięcia programowania obiektowego jest w tym języku bardzo elastyczny.

Techniki tutaj przedstawione nie zawierają żadnych sztuczek językowych, ani nie próbują naśladować implementacji teorii obiektowości z innych języków.

Istnieją inne techniki, które czynią programowanie obiektowe w JavaScript jeszcze bardziej zaawansowanym, jednak są one poza zasięgiem tego artykułu wprowadzającego.

Przypisy

  1. Mozilla. "Core JavaScript 1.5 Guide", https://developer.mozilla.org/docs/Web/JavaScript/Guide
  2. Wikipedia. "Object-oriented programming", http://en.wikipedia.org/wiki/Object-...ed_programming
  3. OOP JavaScript Overview series by Kyle Simpson

Autorzy i etykiety dokumentu

 Autorzy tej strony: Xelix196, matewka
 Ostatnia aktualizacja: Xelix196,