Εισαγωγή στον αντικειμενοστραφή προγραμματισμό σε JavaScript: αντικείμενα, πρωτότυπα και τάξεις

Σε πολλές γλώσσες προγραμματισμού, τα μαθήματα είναι μια καλά καθορισμένη έννοια. Σε JavaScript δεν ισχύει. Ή τουλάχιστον αυτό δεν συνέβαινε. Εάν ψάχνετε για OOP και JavaScript θα συναντήσετε πολλά άρθρα με πολλές διαφορετικές συνταγές για το πώς μπορείτε να μιμηθείτε ένα classσε JavaScript.

Υπάρχει ένας απλός τρόπος KISS για να ορίσετε μια τάξη σε JavaScript; Και αν ναι, γιατί τόσες πολλές διαφορετικές συνταγές για να ορίσετε μια τάξη;

Πριν απαντήσουμε σε αυτές τις ερωτήσεις, ας καταλάβουμε καλύτερα τι είναι το JavaScript Object.

Αντικείμενα σε JavaScript

Ας ξεκινήσουμε με ένα πολύ απλό παράδειγμα:

const a = {}; a.foo = 'bar';

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

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

function distance(p1, p2) { return Math.sqrt( (p1.x - p2.x) ** 2 + (p1.y - p2.y) ** 2 ); } distance({x:1,y:1},{x:2,y:2});

Στο παραπάνω παράδειγμα, δεν χρειάζομαι μια κλάση πόντων για να δημιουργήσω ένα σημείο, απλώς επέκτεινα μια παρουσίαση Objectπροσθήκης xκαι yιδιοτήτων. Η απόσταση της συνάρτησης δεν ενδιαφέρεται αν τα ορίσματα είναι μια παρουσία της κλάσης Pointή όχι. Μέχρι να καλέσετε τη distanceλειτουργία με δύο αντικείμενα που έχουν xκαι yιδιότητα τύπου Number, θα λειτουργήσει μια χαρά. Αυτή η έννοια καλείται μερικές φορές δακτυλογράφηση πάπιας .

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

const point1 = { x: 1, y: 1, toString() { return `(${this.x},${this.y})`; } }; const point2 = { x: 2, y: 2, toString() { return `(${this.x},${this.y})`; } };

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

Υπάρχουν πολλοί τρόποι για να αποφύγετε αυτήν την επανάληψη, και, στην πραγματικότητα, σε διαφορετικά άρθρα σχετικά με αντικείμενα και τάξεις στο JS θα βρείτε διαφορετικές λύσεις. Έχετε ακούσει ποτέ για το "Revealing module pattern"; Περιέχει τις λέξεις "μοτίβο" και "αποκαλυπτικό", ακούγεται δροσερό και το "module" είναι απαραίτητο. Άρα πρέπει να είναι ο σωστός τρόπος για να δημιουργήσετε αντικείμενα… εκτός από το ότι δεν είναι. Η αποκάλυψη μοτίβου ενότητας μπορεί να είναι η σωστή επιλογή σε ορισμένες περιπτώσεις, αλλά σίγουρα δεν είναι ο προεπιλεγμένος τρόπος δημιουργίας αντικειμένων με συμπεριφορές.

Είμαστε τώρα έτοιμοι να εισαγάγουμε μαθήματα.

Μαθήματα σε JavaScript

Τι είναι μια τάξη; Από ένα λεξικό: μια τάξη είναι «ένα σύνολο ή κατηγορία πραγμάτων που έχουν κάποια ιδιότητα ή ένα κοινό χαρακτηριστικό και διαφοροποιείται από άλλα ανά είδος, τύπο ή ποιότητα».

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

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

Ευτυχώς το ECMAScript 6 παρέχει τη λέξη-κλειδί class, καθιστώντας πολύ εύκολο να δημιουργήσετε μια τάξη:

class Point { constructor(x, y) { this.x = x; this.y = y; } toString() { return `(${this.x},${this.y})`; } }

Έτσι, κατά τη γνώμη μου, αυτός είναι ο καλύτερος τρόπος για να δηλώσετε τάξεις σε JavaScript. Τα μαθήματα σχετίζονται συχνά με την κληρονομιά:

class Point extends HasXY { constructor(x, y) { super(x, y); } toString() { return `(${this.x},${this.y})`; } }

Όπως μπορείτε να δείτε στο παραπάνω παράδειγμα, για να επεκτείνετε μια άλλη τάξη αρκεί να χρησιμοποιήσετε τη λέξη-κλειδί extends.

Μπορείτε να δημιουργήσετε ένα αντικείμενο από μια τάξη χρησιμοποιώντας τον newτελεστή:

const p = new Point(1,1); console.log(p instanceof Point); // prints true

Ένας καλός αντικειμενοστρεφής τρόπος καθορισμού τάξεων θα πρέπει να παρέχει:

  • μια απλή σύνταξη για να δηλώσετε μια τάξη
  • ένας απλός τρόπος πρόσβασης στην τρέχουσα παρουσία, γνωστός και ως this
  • μια απλή σύνταξη για επέκταση τάξης
  • ένας απλός τρόπος πρόσβασης στην παρουσία super class, γνωστός και ως super
  • ενδεχομένως, ένας απλός τρόπος για να πει εάν ένα αντικείμενο είναι μια παρουσία μιας συγκεκριμένης τάξης. obj instanceof AClassθα πρέπει να επιστρέψει trueεάν το αντικείμενο είναι μια παρουσία αυτής της κλάσης.

Η νέα classσύνταξη παρέχει όλα τα παραπάνω σημεία.

Πριν από την εισαγωγή της classλέξης-κλειδιού, ποιος ήταν ο τρόπος για να ορίσετε μια τάξη σε JavaScript;

Επιπλέον, τι είναι πραγματικά ένα μάθημα σε JavaScript; Γιατί μιλάμε συχνά για πρωτότυπα ;

Μαθήματα σε JavaScript 5

Από τη σελίδα του Mozilla MDN σχετικά με τα μαθήματα:

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

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

class Shape {} console.log(typeof Shape); // prints function

Φαίνεται ότι classκαι functionσχετίζονται. Είναι classαπλώς ένα ψευδώνυμο function; Όχι, δεν είναι.

Shape(2); // Uncaught TypeError: Class constructor Shape cannot be invoked without 'new'

Έτσι, φαίνεται ότι τα άτομα που εισήγαγαν τη classλέξη-κλειδί ήθελαν να μας πουν ότι μια τάξη είναι μια συνάρτηση που πρέπει να καλείται χρησιμοποιώντας τον newτελεστή.

var Shape = function Shape() {} // Or just function Shape(){} var aShape = new Shape(); console.log(aShape instanceof Shape); // prints true

The example above shows that we can use function to declare a class. We cannot, however, force the user to call the function using the new operator. It is possible to throw an exception if the new operator wasn’t used to call the function.

Anyway I suggest you don’t put that check in every function that acts as a class. Instead use this convention: any function whose name begins with a capital letter is a class and must be called using the new operator.

Let’s move on, and find out what a prototype is:

class Shape { getName() { return 'Shape'; } } console.log(Shape.prototype.getName); // prints function getName() ...

Each time you declare a method inside a class, you actually add that method to the prototype of the corresponding function. The equivalent in JS 5 is:

function Shape() {} Shape.prototype.getName = function getName() { return 'Shape'; }; console.log(new Shape().getName()); // prints Shape

Sometimes the class-functions are called constructors because they act like constructors in a regular class.

You may wonder what happens if you declare a static method:

class Point { static distance(p1, p2) { // ... } } console.log(Point.distance); // prints function distance console.log(Point.prototype.distance); // prints undefined

Since static methods are in a 1 to 1 relation with classes, the static function is added to the constructor-function, not to the prototype.

Let’s recap all these concepts in a simple example:

function Point(x, y) { this.x = x; this.y = y; } Point.prototype.toString = function toString() { return '(' + this.x + ',' + this.y + ')'; }; Point.distance = function distance() { // ... } console.log(new Point(1,2).toString()); // prints (1,2) console.log(new Point(1,2) instanceof Point); // prints true

Up to now, we have found a simple way to:

  • declare a function that acts as a class
  • access the class instance using the this keyword
  • create objects that are actually an instance of that class (new Point(1,2) instanceof Point returns true )

But what about inheritance? What about accessing the super class?

class Hello { constructor(greeting) { this._greeting = greeting; } greeting() { return this._greeting; } } class World extends Hello { constructor() { super('hello'); } worldGreeting() { return super.greeting() + ' world'; } } console.log(new World().greeting()); // Prints hello console.log(new World().worldGreeting()); // Prints hello world

Above is a simple example of inheritance using ECMAScript 6, below the same example using the the so called prototype inheritance:

function Hello(greeting) { this._greeting = greeting; } Hello.prototype.greeting = function () { return this._greeting; }; function World() { Hello.call(this, 'hello'); } // Copies the super prototype World.prototype = Object.create(Hello.prototype); // Makes constructor property reference the sub class World.prototype.constructor = World; World.prototype.worldGreeting = function () { const hello = Hello.prototype.greeting.call(this); return hello + ' world'; }; console.log(new World().greeting()); // Prints hello console.log(new World().worldGreeting()); // Prints hello world

This way of declaring classes is also suggested in the Mozilla MDN example here.

Using the class syntax, we deduced that creating classes involves altering the prototype of a function. But why is that so? To answer this question we must understand what the new operator actually does.

New operator in JavaScript

The new operator is explained quite well in the Mozilla MDN page here. But I can provide you with a relatively simple example that emulates what the new operator does:

function customNew(constructor, ...args) { const obj = Object.create(constructor.prototype); const result = constructor.call(obj, ...args); return result instanceof Object ? result : obj; } function Point() {} console.log(customNew(Point) instanceof Point); // prints true

Note that the real new algorithm is more complex. The purpose of the example above is just to explain what happens when you use the new operator.

When you write new Point(1,2)what happens is:

  • The Point prototype is used to create an object.
  • The function constructor is called and the just created object is passed as the context (a.k.a. this) along with the other arguments.
  • If the constructor returns an Object, then this object is the result of the new, otherwise the object created from the prototype is the result.

So, what does prototype inheritance mean? It means that you can create objects that inherit all the properties defined in the prototype of the function that was called with the new operator.

If you think of it, in a classical language the same process happens: when you create an instance of a class, that instance can use the this keyword to access to all the functions and properties (public) defined in the class (and the ancestors). As opposite to properties, all the instances of a class will likely share the same references to the class methods, because there is no need to duplicate the method’s binary code.

Functional programming

Sometimes people say that JavaScript is not well suited for Object Oriented programming, and you should use functional programming instead.

Αν και δεν συμφωνώ ότι το JS δεν είναι κατάλληλο για OOP, νομίζω ότι ο λειτουργικός προγραμματισμός είναι ένας πολύ καλός τρόπος προγραμματισμού. Στο JavaScript οι λειτουργίες είναι πολίτες πρώτης κατηγορίας (π.χ. μπορείτε να μεταβιβάσετε μια συνάρτηση σε μια άλλη λειτουργία) και παρέχει δυνατότητες όπως bind, callή applyπου είναι βασικές κατασκευές που χρησιμοποιούνται σε λειτουργικό προγραμματισμό.

Επιπλέον, ο προγραμματισμός RX θα μπορούσε να θεωρηθεί ως εξέλιξη (ή εξειδίκευση) του λειτουργικού προγραμματισμού. Ρίξτε μια ματιά στα RxJs εδώ.

συμπέρασμα

Χρησιμοποιήστε, όταν είναι δυνατόν, classσύνταξη ECMAScript 6 :

class Point { toString() { //... } }

ή χρησιμοποιήστε πρωτότυπα συναρτήσεων για να ορίσετε τάξεις στο ECMAScript 5:

function Point() {} Point.prototype.toString = function toString() { // ... }

Ελπίζω να σας άρεσε η ανάγνωση!