ES6 – classes and inheritance
OO (Object Oriented) – termin ten był chyba najbardziej wyczekiwaną częścią nowego standardu ECMAScript. Wprowadzenie klas, to coś zupełnie świeżego w świecie JavaScript. Wraz z ES6 otrzymujemy spójne podejście do tworzenia obiektów. Nową funkcjonalność zbudowano ponad to w oparciu o prototypy, by zachować wsteczną kompatybilność.
Zobacz całą serię: Let-s talk about ECMAScript 2015
Zacznijmy od przykładu prostej klasy w ES6:
class Vehicle { constructor (name, type) { this.name = name; this.type = type; } getName () { return this.name; } getType () { return this.type; } } let car = new Vehicle('Tesla', 'car'); console.log(car.getName()); // Tesla console.log(car.getType()); // car
Widzimy tutaj, że pojawiają nam się nowe słowa kluczowe class oraz constructor, które znane są z innych obiektowych języków programowania. Mamy również dwie metody: getName() oraz getType(). Poniżej odpowiednik tej klasy z wykorzystaniem składni ES5:
function Vehicle (name, type) { this.name = name; this.type = type; }; Vehicle.prototype.getName = function getName () { return this.name; }; Vehicle.prototype.getType = function getType () { return this.type; }; var car = new Vehicle('Tesla', 'car'); console.log(car.getName()); // Tesla console.log(car.getType()); // car
Jest to podejście oparte o prototype.
dziedziczenie
ECMAScript2015 wspiera dziedziczenie, wykonywanie kodu klas dziedziczonych (super), metody statyczne i instancyjne oraz konstrukcję obiektu poprzez słowo kluczowe constructor.
Rozbudujmy poprzedni przykład o dziedziczenie. Zacznę tym razem od wersji ES5:
function Vehicle (name, type) { this.name = name; this.type = type; }; Vehicle.prototype.getName = function getName () { return this.name; }; Vehicle.prototype.getType = function getType () { return this.type; }; function Car (name) { Vehicle.call(this, name, ‘car’); } Car.prototype = Object.create(Vehicle.prototype); Car.prototype.constructor = Car; Car.parent = Vehicle.prototype; Car.prototype.getName = function () { return 'It is a car: '+ this.name; }; var car = new Car('Tesla'); console.log(car.getName()); // It is a car: Tesla console.log(car.getType()); // car
Widzimy, że całość mocno się komplikuje, a to przecież prosty przykład. Spójrzmy teraz na nowe podejście:
class Vehicle { constructor (name, type) { this.name = name; this.type = type; } getName () { return this.name; } getType () { return this.type; } } class Car extends Vehicle { constructor (name) { super(name, 'car'); } getName () { return 'It is a car: ' + super.getName(); } } let car = new Car('Tesla'); console.log(car.getName()); // It is a car: Tesla console.log(car.getType()); // car
Wygląda znajomo? Z pewnością jest to znacznie lepsze rozwiązanie. Mamy tutaj klasę Vehicle oraz Car, która przy pomocy extends dziedziczy po tej pierwszej. W konstruktorze klasy Car wykonywany jest ponad to konstruktor klasy dziedziczonej poprzez wywołanie z super. Dodatkowo metoda getName() zostaje nadpisana w klasie Car i wykonuje metodę getName() z klasy dziedziczonej również przy użyciu słowa kluczowego super.
By w ES5 osiągnąć podobną funkcjonalność musielibyśmy kombinować z wykorzystaniem metod call lub apply.
static
Podejście obiektowe pozwala nam na definiowanie metod statycznych, które dostępne są bez konieczności tworzenia instancji obiektu za pomocą new. Dostęp do metod statycznych otrzymujemy poprzez odwołanie do nazwy klasy, w której dana metoda została zdefiniowana. Poza tym, metody statyczne mogą być również dziedziczone oraz wywoływane z podklas.
class Vehicle { constructor (name, type) { this.name = name; this.type = type; } getName () { return this.name; } getType () { return this.type; } static create (name, type) { return new Vehicle(name, type); } } let car = Vehicle.create('Tesla', 'car'); console.log(car.getName()); // Tesla console.log(car.getType()); // car
get / set
Kolejnym standardowym elementem języków obiektowych są tak zwane settery oraz gettery. Pozwalają one zdefiniować dostęp do pól obiektu (get) oraz ostawienie ich wartości (set).
class Car { constructor (name) { this._name = name; } set name (name) { this._name = name; } get name () { return this._name; } } let car = new Car('Tesla'); console.log(car.name); // Tesla car.name = 'BMW'; console.log(car.name); // BMW
enhanced object properties
Warto również nadmienić, że ES6 to również skrócone oraz wyliczane tworzenie własności w obiekcie oraz przemodelowane funkcje, które widzieliśmy już w powyższych przykładach.
skrócone własności
To nic innego, jak tworzenie własności na podstawie nazwy zmiennej. Najlepiej zilustruje to poniższy przykład:
// ES6 let x = 1, y = 2, obj = { x, y }; console.log(obj); // Object { x: 1, y: 2 } // ES5 var x = 1, y = 2, obj = { x: x, y: y }; console.log(obj); // Object { x: 1, y: 2 }
W przypadku składni ES6 nie musimy podawać nazwy własności obiektu jeśli chcemy, by miała ona taką samą nazwę jak nazwa zmiennej, której wartość ma zostać przypisana do obiektu.
wyliczane własności
Kolejną funkcjonalnością ECMAScript2015 są wyliczane własności obiektu. Pozwalają one na użycie zmiennych oraz innych wartości do dynamicznego tworzenia nazw własności.
// ES6 let getKey = () => '123', obj = { foo: 'bar', ['key_' + getKey()]: 123 }; console.log(obj); // Object { foo: 'bar', key_123: 123 } // ES5 var getKey = function () { return '123'; }, obj = { foo: 'bar' }; obj['key_' + getKey()] = 123; console.log(obj); // Object { foo: 'bar', key_123: 123 }
metody w obiekcie
Na koniec metody tworzone jako własności obiektu. Widzieliśmy je już w poprzednich przykładach, dotyczących podejścia obiektowego w ES6.
// ES6 let obj = { name: 'object name', toString () { // 'function' keyword is omitted here return this.name; } }; console.log(obj.toString()); // object name // ES5 var obj = { name: 'object name', toString: function () { return this.name; } }; console.log(obj.toString()); // object name