Przejdź do treści

Dziedziczenie obiektów w JavaScript (bez użycia klas)

Od 2015 roku dzięki specyfikacji ES6 a.k.a. ES2015 mamy możliwość stworzenia klasy jako reprezentacji opisu obiektu, na podstawie której zostanie on stworzony.

Co jeśli architektura Twojej aplikacji nie opiera się na klasach? A może po prostu nie jesteś zwolennikiem Object-Oriented Programming (OOP)?

Taka sytuacja jest dosyć częsta, kiedy zaczynasz poznawać świat programowania w JavaScript i klasy nie są jeszcze Ci znane.

Z pomocą przychodzi wzorzec delegacji i styl Objects Linked to Other Objects (OLOO), który swoją definicję posiada w książce “You Don’t Know JS” autorstwa Kyle Simsona.

Dziedziczenie

Przykładowy model architektury w ujęciu klasycznym, czyli istnieją obiekty główne (rodzice) oraz obiekty pochodne (dzieci).

Schemat obiektów prostej architektury aplikacji

Dziedziczenie obiektów? Nie!

Model architektury opartej na OLOO nie wprowadza hierarchii. On po prostu informuje, że jeden obiekty będą korzystały z innych.

Schemat obiektów prostej architektury aplikacji

Tego procesu nie można nazwać dziedziczeniem, tylko delegacją.

W specyfikacji ES2015 została zdefiniowana funkcja statyczna Object.setPrototypeOf, która łączy ze sobą obiekty, ustawiając ich wewnętrzne wiązanie [[Prototype]] (dostępne też przez akcesor __proto__). Takie wiązanie ma każdy obiekt, także funkcja. Właściwość prototype, którą mają funkcje, to coś innego: to zwykła właściwość konstruktora, z której new bierze prototyp dla tworzonych obiektów.

const point = {
    x: null,
    y: null,
    draw() {
        // ...
    }
};

const point1 = {
    x: 0,
    y: 0
};
console.log(typeof point1.draw); // undefined

Object.setPrototypeOf(point1, point);

console.log(point1.hasOwnProperty('draw')); // false
console.log('draw' in point1); // true
console.log(typeof point1.draw); // "function"

const point2 = {
    x: 50,
    y: 50
};
Object.setPrototypeOf(point2, point);

Uwaga

Zmiana prototypu istniejącego obiektu przez Object.setPrototypeOf jest według MDN bardzo wolna i psuje optymalizacje silnika JavaScript. Lepiej od razu tworzyć obiekt z wybranym prototypem: const point1 = Object.assign(Object.create(point), { x: 0, y: 0 });

Weryfikacja

Aby móc zweryfikować aktualny stan proponuję uruchomić:

// Check: Different points
console.log(point1 !== point2) // true

// Same definition of "draw" method
console.log(point.draw === point1.draw) // true
console.log(point.draw === point2.draw) // true
console.log(point1.draw === point2.draw) // true

Wsparcie systemowe

Ale gdzie można używać takiej składni zapytasz? Otóż dzięki najpopularniejszemu zestawieniu wsparcia feature-ów JavaScript-owych, realizowanemu przez Kangax-a, możemy się dowiedzieć, że:

Jest wsparcie

Poniższe wersje były aktualne w dniu pisania - minimalne są dużo niższe (np. Chrome 34, Firefox 31, Safari 9, Edge 12).

  • Google Chrome 57+ (Opera 44+)
  • Firefox 52+
  • Safari:
    • desktop: 10+
    • mobile: 9+
  • Microsoft
    • IE 11
    • Edge 14+
  • Node.js 4+

Brak wsparcia

Sam transpiler nie pomoże w zapewnieniu wsparcia funkcji Object.setPrototypeOf, bo to wbudowana funkcja, a nie składnia - potrzebny jest polyfill (np. z biblioteki core-js). W zestawieniu brak wsparcia mają:

  • Babel.js
  • Traceur
  • Closure
  • TypeScript

Link do pełnego zestawienia.

Polyfill

Link do definicji polyfilla znajduje się na MDN jednak, abyś mógł szybko zobaczyć jak on wygląda umieszczę go tutaj:

// Only works in Chrome and FireFox, does not work in IE 10 and older:
Object.setPrototypeOf = Object.setPrototypeOf || function(obj, proto) {
  obj.__proto__ = proto;
  return obj;
}

Cya! 👋