Διαδρομές
/
JavaScript
JavaScript
/
Ύλη
/
Πρωτότυπα & Κλάσεις
Πρ

Πρωτότυπα & Κλάσεις σε JavaScript

32 ασκήσεις

Σχετικά με την έννοια Πρωτότυπα & Κλάσεις

Η JavaScript περιλαμβάνει τις δυνατότητες για αντικειμενοστρεφή προγραμματισμό (OOP). Στον αντικειμενοστρεφή προγραμματισμό, θέλεις να δημιουργείς αντικείμενα (στιγμιότυπα) από "πρότυπα" (κλάσεις), έτσι ώστε να περιλαμβάνουν συγκεκριμένα δεδομένα και λειτουργικότητα. Οι ιδιότητες που κρατούν δεδομένα ονομάζονται πεδία στο πλαίσιο του αντικειμενοστρεφούς προγραμματισμού, ενώ οι ιδιότητες που είναι συναρτήσεις ονομάζονται μέθοδοι.

Η JavaScript δεν είχε καθόλου κλάσεις πριν προστεθούν στην προδιαγραφή της γλώσσας το 2015, αλλά επέτρεπε τον αντικειμενοστρεφή προγραμματισμό μέσω κληρονομικότητας βασισμένης σε πρωτότυπα. Και παρόλο που σήμερα υπάρχει διαθέσιμη η λέξη-κλειδί class, η JavaScript παραμένει μια γλώσσα βασισμένη σε πρωτότυπα.

Για να καταλάβεις τι σημαίνει να είναι μια γλώσσα βασισμένη σε πρωτότυπα και πώς λειτουργεί στην πραγματικότητα η JavaScript, θα πάμε πίσω στον καιρό που δεν υπήρχαν κλάσεις.

Σύνταξη με Πρωτότυπα

Συνάρτηση Κατασκευαστή

Στη JavaScript, το πρότυπο (κλάση) υλοποιείται μέσω μιας συνηθισμένης συνάρτησης. Όταν μια συνάρτηση πρόκειται να χρησιμοποιηθεί ως τέτοιο πρότυπο, ονομάζεται συνάρτηση κατασκευαστή και η σύμβαση είναι το όνομα της συνάρτησης να ξεκινά με κεφαλαίο γράμμα. Τα στιγμιότυπα (αντικείμενα) προκύπτουν από το πρότυπο χρησιμοποιώντας τη λέξη-κλειδί new όταν καλείς τη συνάρτηση κατασκευαστή.

function Car() {
  // ...
}

const myCar = new Car();
const yourCar = new Car();

Είναι σημαντικό να σημειώσεις ότι στη JavaScript, τα στιγμιότυπα και η συνάρτηση κατασκευαστή διατηρούν μια σχέση μεταξύ τους ακόμα και αφού δημιουργηθούν τα στιγμιότυπα. Κάθε αντικείμενο στιγμιότυπου περιλαμβάνει μια κρυφή, εσωτερική ιδιότητα που αναφέρεται ως [[prototype]] στην προδιαγραφή της γλώσσας. Αυτή κρατά μια αναφορά στην τιμή του κλειδιού prototype της συνάρτησης κατασκευαστή. Ναι, το διάβασες σωστά: μια συνάρτηση JavaScript μπορεί να έχει ζεύγη κλειδιού/τιμής, επειδή στα παρασκήνια είναι και αυτή ένα αντικείμενο.

Από το 2015, μπορείς να προσπελάσεις το [[prototype]] μέσω της Object.getPrototypeOf(). Πριν από αυτό, ήταν προσπελάσιμο μέσω του κλειδιού __proto__ σε πολλά περιβάλλοντα.

Μην μπερδέψεις το πρωτότυπο ενός αντικειμένου ([[prototype]]) με την ιδιότητα prototype της συνάρτησης κατασκευαστή.

Note

Για να συνοψίσουμε:

  • Οι κατασκευαστές στη JavaScript είναι συνηθισμένες συναρτήσεις.
  • Η δημιουργία ενός νέου στιγμιότυπου δημιουργεί ένα αντικείμενο με σχέση προς τον κατασκευαστή του, που ονομάζεται πρωτότυπό του.
  • Οι συναρτήσεις είναι αντικείμενα (καλούμενα αντικείμενα) και επομένως μπορούν να έχουν ιδιότητες.
  • Η ιδιότητα prototype του κατασκευαστή (της συνάρτησης) θα γίνει το πρωτότυπο του στιγμιότυπου.

Πεδία Στιγμιοτύπων

Συχνά, θέλεις όλα τα παράγωγα αντικείμενα (στιγμιότυπα) να περιλαμβάνουν κάποια πεδία και να περνούν κάποιες αρχικές τιμές για αυτά κατά την κατασκευή του αντικειμένου. Αυτό μπορεί να γίνει μέσω της λέξης-κλειδί this. Μέσα στη συνάρτηση κατασκευαστή, το this αντιπροσωπεύει το νέο αντικείμενο που θα δημιουργηθεί μέσω του new. Το this επιστρέφεται αυτόματα από τη συνάρτηση κατασκευαστή όταν αυτή καλείται με το new.

Αυτό σημαίνει ότι μπορούμε να προσθέσουμε πεδία στο νέο στιγμιότυπο προσθέτοντάς τα στο this μέσα στη συνάρτηση κατασκευαστή.

function Car(color, weight) {
  this.color = color;
  this.weight = weight;
  this.engineRunning = false;
}

const myCar = new Car('red', '2mt');
myCar.color;
// => 'red'
myCar.engineRunning;
// => false

Μέθοδοι Στιγμιοτύπων

Οι μέθοδοι προστίθενται μέσω της ιδιότητας prototype της συνάρτησης κατασκευαστή. Μέσα σε μια μέθοδο, μπορείς να προσπελάσεις τα πεδία του στιγμιότυπου μέσω του this. Αυτό λειτουργεί χάρη στον παρακάτω γενικό κανόνα.

Όταν μια συνάρτηση καλείται ως μέθοδος ενός αντικειμένου, το this της ορίζεται στο αντικείμενο στο οποίο καλείται η μέθοδος. 1

function Car() {
  this.engineRunning = false;
  // ...
}

Car.prototype.startEngine = function () {
  this.engineRunning = true;
};

Car.prototype.addGas = function (litre) {
  // ...
};

const myCar = new Car();
myCar.startEngine();
myCar.engineRunning;
// => true

Η Αλυσίδα Πρωτοτύπων

Το myCar στο παραπάνω παράδειγμα είναι ένα συνηθισμένο αντικείμενο JavaScript και αν το εξετάζαμε (π.χ. στην κονσόλα του περιηγητή), δεν θα βρίσκαμε μια ιδιότητα startEngine με τιμή κάποια συνάρτηση απευθείας μέσα στο αντικείμενο myCar. Πώς λειτουργεί, λοιπόν, ο παραπάνω κώδικας;

Το μυστικό εδώ ονομάζεται αλυσίδα πρωτοτύπων. Όταν προσπαθείς να προσπελάσεις οποιαδήποτε ιδιότητα (πεδίο ή μέθοδο) ενός αντικειμένου, η JavaScript ελέγχει πρώτα αν το αντίστοιχο κλειδί υπάρχει απευθείας στο ίδιο το αντικείμενο. Αν όχι, συνεχίζει να ψάχνει το κλειδί στο αντικείμενο στο οποίο παραπέμπει η ιδιότητα [[prototype]] του αρχικού αντικειμένου. Όπως αναφέρθηκε προηγουμένως, στο παράδειγμά μας το [[prototype]] δείχνει στην ιδιότητα prototype της συνάρτησης κατασκευαστή. Εκεί θα έβρισκε η JavaScript τη συνάρτηση startEngine, επειδή την προσθέσαμε εκεί.

function Car() {
  // ...
}

Car.prototype.startEngine = function () {
  // ...
};

Και η αλυσίδα δεν τελειώνει εκεί. Η ιδιότητα [[prototype]] του Car.prototype (myCar.[[prototype]].[[prototype]]) παραπέμπει στο Object.prototype (την ιδιότητα prototype της συνάρτησης κατασκευαστή Object). Περιέχει γενικές μεθόδους που είναι διαθέσιμες για όλα τα αντικείμενα JavaScript, π.χ. toString(). Το [[prototype]] του Object είναι συνήθως null, οπότε η αλυσίδα πρωτοτύπων τελειώνει εκεί. Συμπερασματικά, μπορείς να καλέσεις το myCar.toString() και αυτή η μέθοδος θα υπάρχει, επειδή η JavaScript την αναζητά σε ολόκληρη την αλυσίδα πρωτοτύπων. Μπορείς να βρεις ένα αναλυτικό παράδειγμα στο άρθρο του MDN "Inheritance and the prototype chain".

Caution

Σημείωσε ότι η αλυσίδα πρωτοτύπων διανύεται μόνο κατά την ανάκτηση μιας τιμής. Ο ορισμός μιας ιδιότητας απευθείας ή η διαγραφή μιας ιδιότητας ενός αντικειμένου στιγμιότυπου επηρεάζει μόνο αυτό το συγκεκριμένο στιγμιότυπο. Αυτό μπορεί να μην είναι αυτό που θα περίμενες αν είσαι συνηθισμένος σε μια γλώσσα με κληρονομικότητα βασισμένη σε κλάσεις.

Δυναμικές Μέθοδοι (Προσθήκη Μεθόδων σε Όλα τα Υπάρχοντα Στιγμιότυπα)

Η JavaScript σου επιτρέπει να προσθέτεις μεθόδους σε όλα τα υπάρχοντα στιγμιότυπα ακόμα και αφού αυτά δημιουργηθούν.

Μάθαμε ότι κάθε στιγμιότυπο κρατά μια αναφορά στην ιδιότητα prototype της συνάρτησης κατασκευαστή. Αυτό σημαίνει ότι αν προσθέσεις μια καταχώριση σε αυτό το αντικείμενο prototype, η νέα καταχώριση (π.χ. μια νέα μέθοδος) είναι αμέσως διαθέσιμη σε όλα τα στιγμιότυπα που έχουν δημιουργηθεί από αυτή τη συνάρτηση κατασκευαστή.

function Car() {
  this.engineRunning = false;
}

const myCar = new Car();
// Calling myCar.startEngine() here would result in "TypeError:
// myCar.startEngine is not a function".

Car.prototype.startEngine = function () {
  this.engineRunning = true;
};

myCar.startEngine();
// This works, even though myCar was created before the method
// was added.

Θεωρητικά, οι δυναμικές μέθοδοι μπορούν ακόμα και να χρησιμοποιηθούν για να επεκτείνουν τη λειτουργικότητα ενσωματωμένων αντικειμένων όπως το Object ή το Array, τροποποιώντας το πρωτότυπό τους. Αυτό ονομάζεται monkey patching. Επειδή αυτή η αλλαγή επηρεάζει ολόκληρη την εφαρμογή, καλό είναι να αποφεύγεται για να μην προκαλούνται ανεπιθύμητες παρενέργειες. Η μόνη λογική περίπτωση χρήσης είναι να παρέχεις ένα polyfill για μια μέθοδο που λείπει σε παλαιότερα περιβάλλοντα.

Σύνταξη Κλάσεων

Σήμερα, η JavaScript υποστηρίζει τον ορισμό κλάσεων με τη λέξη-κλειδί class. Αυτό προστέθηκε στην προδιαγραφή της γλώσσας το 2015. Αφενός, αυτό παρέχει συντακτική ζάχαρη που κάνει τις κλάσεις πιο εύκολες στην ανάγνωση και τη συγγραφή. Η νέα σύνταξη μοιάζει περισσότερο με τον τρόπο που γράφονται οι κλάσεις σε γλώσσες όπως η C++ ή η Java. Οι προγραμματιστές που έρχονται από αυτές τις γλώσσες προσαρμόζονται πιο εύκολα. Αφετέρου, η σύνταξη κλάσεων ανοίγει τον δρόμο για νέα χαρακτηριστικά της γλώσσας που δεν είναι διαθέσιμα στη σύνταξη με πρωτότυπα.

Δηλώσεις Κλάσεων

Με τη νέα σύνταξη, οι κλάσεις ορίζονται με τη λέξη-κλειδί class, ακολουθούμενη από το όνομα της κλάσης και το σώμα της κλάσης μέσα σε άγκιστρα. Το σώμα περιέχει τον ορισμό της συνάρτησης κατασκευαστή, δηλαδή μια ειδική μέθοδο με το όνομα constructor. Αυτή η συνάρτηση λειτουργεί ακριβώς όπως η συνάρτηση κατασκευαστή στη σύνταξη με πρωτότυπα. Το σώμα της κλάσης περιέχει επίσης όλους τους ορισμούς μεθόδων. Η σύνταξη για τις μεθόδους μοιάζει με τη συντομευμένη γραφή που είδαμε για την προσθήκη συναρτήσεων ως τιμών μέσα σε ένα αντικείμενο, δες την έννοια Αντικείμενα.

class Car {
  constructor(color, weight) {
    this.color = color;
    this.weight = weight;
    this.engineRunning = false;
  }

  startEngine() {
    this.engineRunning = true;
  }

  addGas(litre) {
    // ...
  }
}

const myCar = new Car();
myCar.startEngine();
myCar.engineRunning;
// => true

Όπως συμβαίνει με τις δηλώσεις συναρτήσεων και τις εκφράσεις συναρτήσεων, η JavaScript υποστηρίζει επίσης εκφράσεις κλάσεων εκτός από τη δήλωση κλάσης που φαίνεται παραπάνω.

Να θυμάσαι ότι στα παρασκήνια, η JavaScript παραμένει μια γλώσσα βασισμένη σε πρωτότυπα. Όλοι οι μηχανισμοί που μάθαμε στην παραπάνω ενότητα "Σύνταξη με Πρωτότυπα" εξακολουθούν να ισχύουν.

Ιδιωτικά Πεδία, Getters και Setters

Από προεπιλογή, όλα τα πεδία στιγμιότυπων είναι δημόσια στη JavaScript. Μπορείς να τα προσπελάσεις και να τους αναθέσεις τιμές απευθείας.

Η προσθήκη πραγματικών ιδιωτικών πεδίων στην προδιαγραφή της γλώσσας είναι σε εξέλιξη, δες το έγγραφο πρότασης για λεπτομέρειες.

Στο μεταξύ, μπορείς να αξιοποιήσεις την καθιερωμένη σύμβαση ότι τα πεδία και οι μέθοδοι που ξεκινούν με κάτω παύλα πρέπει να αντιμετωπίζονται ως ιδιωτικά. Δεν πρέπει ποτέ να προσπελάζονται απευθείας από έξω από την κλάση.

Τα ιδιωτικά πεδία συνοδεύονται μερικές φορές από getters και setters. Με τις λέξεις-κλειδιά get και set μπορείς να ορίσεις συναρτήσεις που εκτελούνται όταν μια ιδιότητα με το ίδιο όνομα με τη συνάρτηση προσπελαύνεται ή της ανατίθεται τιμή.

class Car {
  constructor() {
    this._mileage = 0;
  }

  get mileage() {
    return this._mileage;
  }

  set mileage(value) {
    throw new Error(`Mileage cannot be manipulated, ${value} is ignored.`);
    // Just an example, usually you would not provide a setter in this case.
  }
}

const myCar = new Car();
myCar.mileage;
// => 0
myCar.mileage = 100;
// => Error: Mileage cannot be manipulated, 100 is ignored.

Πεδία Κλάσης και Μέθοδοι Κλάσης

Στον αντικειμενοστρεφή προγραμματισμό, μερικές φορές θέλεις να παρέχεις βοηθητικά πεδία ή μεθόδους που δεν εξαρτώνται από το συγκεκριμένο στιγμιότυπο. Αντ' αυτού, ορίζονται για την ίδια την κλάση. Αυτό μπορεί να επιτευχθεί με τη λέξη-κλειδί static.

class Car {
  static type = 'vehicle';

  static isType(targetType) {
    return targetType === 'vehicle';
  }
}

Car.type;
// => 'vehicle'

Car.isType('road sign');
// => false

Κληρονομικότητα Βασισμένη σε Κλάσεις

Εκτός από τον τύπο κληρονομικότητας κατά μήκος της αλυσίδας πρωτοτύπων που είδαμε νωρίτερα, μπορείς επίσης να αναπαραστήσεις κληρονομικότητα μεταξύ κλάσεων στη JavaScript. Αυτό καλύπτεται στην έννοια Κληρονομικότητα.


  1. Παραδείγματα this - Ως μέθοδος αντικειμένου, MDN. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/this#as_an_object_method (αναφέρθηκε στις 3 Δεκεμβρίου 2021) ↩

Επεξεργασία μέσω GitHub Ο σύνδεσμος ανοίγει σε νέο παράθυρο ή καρτέλα

Μάθε την έννοια Πρωτότυπα & Κλάσεις