ES6 – classes and inheritance

javascript

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 statyczneinstancyjne 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