Το μόνο που χρειάζεται να γνωρίζετε για να κατανοήσετε το πρωτότυπο JavaScript

Τις περισσότερες φορές, το πρωτότυπο JavaScript μπερδεύει άτομα που μόλις άρχισαν να μαθαίνουν JavaScript - ειδικά αν προέρχονται από φόντο C ++ ή Java.

Στο JavaScript, η κληρονομιά λειτουργεί λίγο διαφορετικά σε σύγκριση με το C ++ ή Java. Η κληρονομιά JavaScript είναι ευρύτερα γνωστή ως «πρωτότυπη κληρονομιά».

Τα πράγματα γίνονται πιο δύσκολα κατανοητά όταν συναντάτε και classσε JavaScript. Η νέα classσύνταξη μοιάζει με C ++ ή Java, αλλά στην πραγματικότητα, λειτουργεί διαφορετικά.

Σε αυτό το άρθρο, θα προσπαθήσουμε να κατανοήσουμε την «πρωτότυπη κληρονομιά» σε JavaScript. Εξετάζουμε επίσης τη νέα classβασισμένη σύνταξη και προσπαθούμε να καταλάβουμε τι είναι πραγματικά. Ας ξεκινήσουμε λοιπόν.

Αρχικά, θα ξεκινήσουμε με τη λειτουργία και το πρωτότυπο JavaScript του παλιού σχολείου.

Κατανόηση της ανάγκης για πρωτότυπο

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

Για παράδειγμα:

var arr = [1,2,3,4];arr.reverse(); // returns [4,3,2,1]
var obj = {id: 1, value: "Some value"};obj.hasOwnProperty('id'); // returns true
var str = "Hello World";str.indexOf('W'); // returns 6

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

Μπορείτε να ορίσετε τις δικές σας μεθόδους έτσι; Θα μπορούσατε να πείτε ότι μπορείτε με αυτόν τον τρόπο:

var arr = [1,2,3,4];arr.test = function() { return 'Hi';}arr.test(); // will return 'Hi'

Αυτό θα λειτουργήσει, αλλά μόνο για αυτήν τη μεταβλητή που ονομάζεται arr. Ας πούμε ότι έχουμε μια άλλη μεταβλητή που ονομάζεται arr2και arr2.test()θα ρίξει ένα σφάλμα "TypeError: arr2.test is not function".

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

Ας δούμε πρώτα από πού προέρχονται αυτές οι λειτουργίες. Εξετάστε το απόσπασμα κώδικα παρακάτω:

var arr1 = [1,2,3,4];var arr2 = Array(1,2,3,4);

Έχουμε δημιουργήσει δύο πίνακες με δύο διαφορετικούς τρόπους: arr1με κυριολεκτικά γράμματα και arr2με Arrayλειτουργία κατασκευαστή. Και τα δύο είναι ισοδύναμα μεταξύ τους με κάποιες διαφορές που δεν έχουν σημασία για αυτό το άρθρο.

Τώρα έρχεται στη συνάρτηση κατασκευαστή Array- είναι μια προκαθορισμένη συνάρτηση κατασκευαστή σε JavaScript. Εάν ανοίξετε τα εργαλεία προγραμματιστή του Chrome και μεταβείτε στην κονσόλα και πληκτρολογήσετε console.log(Array.prototype)και πατήσετε enter, θα δείτε κάτι παρακάτω:

Εκεί θα δείτε όλες τις μεθόδους που αναρωτιόμασταν. Τώρα λοιπόν φτάνουμε από όπου έρχονται αυτές οι λειτουργίες. Μη διστάσετε να δοκιμάσετε με String.prototypeκαι Object.prototype.

Ας δημιουργήσουμε τη δική μας απλή συνάρτηση κατασκευαστή:

var foo = function(name) { this.myName = name; this.tellMyName = function() { console.log(this.myName); }}
var fooObj1 = new foo('James');fooObj1.tellMyName(); // will print Jamesvar fooObj2 = new foo('Mike');fooObj2.tellMyName(); // will print Mike

Μπορείτε να εντοπίσετε ένα θεμελιώδες πρόβλημα με τον παραπάνω κώδικα; Το πρόβλημα είναι ότι σπαταλάμε τη μνήμη με την παραπάνω προσέγγιση. Σημειώστε ότι η μέθοδος tellMyNameείναι η ίδια για κάθε παρουσία του foo. Κάθε φορά που δημιουργούμε μια παρουσία της fooμεθόδου tellMyNameκαταλήγει να παίρνει χώρο στη μνήμη του συστήματος. Εάν tellMyNameείναι το ίδιο για όλες τις περιπτώσεις, είναι καλύτερα να το διατηρήσετε σε ένα μόνο μέρος και να κάνετε όλες τις παρουσίες μας να αναφέρονται από αυτό το μέρος. Ας δούμε πώς να το κάνουμε αυτό.

var foo = function(name) { this.myName = name;}
foo.prototype.tellMyName = function() { console.log(this.myName);}
var fooObj1 = new foo('James');fooObj1.tellMyName(); // will print Jamesvar fooObj2 = new foo('Mike');fooObj2.tellMyName(); // will print Mike

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

Σημειώστε ότι ως ιδιοκτησία των παρουσιών που έχουμε μόνο myname. tellMyNameορίζεται στο __proto__. Θα έρθω σε αυτό __proto__μετά από λίγο. Το πιο σημαντικό είναι ότι η σύγκριση και tellMyNameτων δύο παρουσιών αξιολογείται ως αληθής. Η σύγκριση λειτουργιών στο JavaScript αξιολογεί αληθές μόνο εάν οι αναφορές τους είναι ίδιες. Αυτό αποδεικνύει ότι tellMyNameδεν καταναλώνει επιπλέον μνήμη για πολλές περιπτώσεις.

Ας δούμε το ίδιο με την προηγούμενη προσέγγιση:

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

Ελπίζω τώρα να καταλάβετε την αναγκαιότητα prototype.

Τώρα ας δούμε περισσότερες λεπτομέρειες σχετικά με το πρωτότυπο.

Κάθε συνάρτηση JavaScript θα έχει μια prototypeιδιότητα που είναι του τύπου αντικειμένου. Μπορείτε να ορίσετε τις δικές σας ιδιότητες prototype. Όταν θα χρησιμοποιήσετε τη συνάρτηση ως συνάρτηση κατασκευαστή, όλες οι παρουσίες της θα κληρονομήσουν ιδιότητες από το prototypeαντικείμενο.

Τώρα ας έρθουμε σε αυτήν την __proto__ιδιοκτησία που είδατε παραπάνω. Η __proto__είναι απλώς μια αναφορά στο πρωτότυπο αντικείμενο από το οποίο έχει κληρονομήσει το παράδειγμα. Ακούγεται περίπλοκο; Στην πραγματικότητα δεν είναι τόσο περίπλοκο. Ας το απεικονίσουμε με ένα παράδειγμα.

Εξετάστε τον παρακάτω κώδικα. Γνωρίζουμε ήδη τη δημιουργία ενός πίνακα με λεκτικές σειρά θα κληρονομήσουν ιδιότητες από Array.prototype.

var arr = [1, 2, 3, 4];

Αυτό που μόλις είπα παραπάνω είναι " Η __proto__απλή αναφορά στο πρωτότυπο αντικείμενο από το οποίο κληρονόμησε η παρουσία ". Έτσι arr.__proto__πρέπει να είναι το ίδιο με Array.prototype. Ας το επαληθεύσουμε.

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

var arr = [1, 2, 3, 4];var prototypeOfArr = Object.getPrototypeOf(arr);prototypeOfArr === Array.prototype;prototypeOfArr === arr.__proto__;

The last line of the above code snippet shows that __proto__ and Object.getPrototypeOf return the same thing.

Now it’s time for a break. Grab a coffee or whatever you like and try out the examples above on your own. Once you are ready, come back to this article and we will then continue.

Prototype chaining & Inheritance

In Fig: 2 above, did you notice that there is another __proto__ inside the first __proto__ object? If not then scroll up a bit to Fig: 2. Have a look and come back here. We will now discuss what that is actually. That is known as prototype chaining.

In JavaScript, we achieve Inheritance with the help of prototype chaining.

Consider this example: We all understand the term “Vehicle”. A bus could be called as a vehicle. A car could be called a vehicle. A motorbike could be called a vehicle. Bus, car, and motorbike have some common properties that's why they are called vehicle. For example, they can move from one place to another. They have wheels. They have horns, etc.

Again bus, car, and motorbike can be of different types for example Mercedes, BMW, Honda, etc.

In the above illustration, Bus inherits some property from vehicle, and Mercedes Benz Bus inherits some property from bus. Similar is the case for Car and MotorBike.

Let's establish this relationship in JavaScript.

First, let's assume a few points for the sake of simplicity:

  1. All buses have 6 wheels
  2. Accelerating and Braking procedures are different across buses, cars, and motorbikes, but the same across all buses, all cars, and all motorbikes.
  3. All vehicles can blow the horn.
function Vehicle(vehicleType) { //Vehicle Constructor this.vehicleType = vehicleType;}
Vehicle.prototype.blowHorn = function () { console.log('Honk! Honk! Honk!'); // All Vehicle can blow Horn}
function Bus(make) { // Bus Constructor Vehicle.call(this, "Bus"); this.make = make}
Bus.prototype = Object.create(Vehicle.prototype); // Make Bus constructor inherit properties from Vehicle Prototype Object
Bus.prototype.noOfWheels = 6; // Let's assume all buses have 6 wheels
Bus.prototype.accelerator = function() { console.log('Accelerating Bus'); //Bus accelerator}
Bus.prototype.brake = function() { console.log('Braking Bus'); // Bus brake}
function Car(make) { Vehicle.call(this, "Car"); this.make = make;}
Car.prototype = Object.create(Vehicle.prototype);
Car.prototype.noOfWheels = 4;
Car.prototype.accelerator = function() { console.log('Accelerating Car');}
Car.prototype.brake = function() { console.log('Braking Car');}
function MotorBike(make) { Vehicle.call(this, "MotorBike"); this.make = make;}
MotorBike.prototype = Object.create(Vehicle.prototype);
MotorBike.prototype.noOfWheels = 2;
MotorBike.prototype.accelerator = function() { console.log('Accelerating MotorBike');}
MotorBike.prototype.brake = function() { console.log('Braking MotorBike');}
var myBus = new Bus('Mercedes');var myCar = new Car('BMW');var myMotorBike = new MotorBike('Honda');

Allow me to explain the above code snippet.

We have a Vehicle constructor which expects a vehicle type. As all vehicles can blow their horns, we have a blowHorn property in Vehicle's prototype.

As Bus is a vehicle it will inherit properties from Vehicle object.

We have assumed all buses will have 6 wheels and have the same accelerating and braking procedures. So we have noOfWheels, accelerator and brake property defined in Bus’s prototype.

Similar logic applies for Car and MotorBike.

Let’s go to Chrome Developer Tools -> Console and execute our code.

After execution, we will have 3 objects myBus, myCar, and myMotorBike.

Type console.dir(mybus) in the console and hit enter. Use the triangle icon to expand it and you will see something like below:

Under myBus we have properties make and vehicleType. Notice the value of __proto__ is prototype of Bus. All the properties of its prototype are available here: accelerator, brake, noOfWheels.

Now have a look that the first __proto__ object. This object has another __proto__ object as its property.

Under which we have blowHorn and constructor property.

Bus.prototype = Object.create(Vehicle.prototype);

Remember the line above? Object.create(Vehicle.prototype) will create an empty object whose prototype is Vehicle.prototype. We set this object as a prototype of Bus. For Vehicle.prototype we haven’t specified any prototype so by default it inherits from Object.prototype.

Let’s see the magic below:

We can access the make property as it is myBus's own property.

We can access the brake property from myBus's prototype.

We can access the blowHorn property from myBus's prototype’s prototype.

We can access the hasOwnProperty property from myBus's prototype’s prototype’s prototype. :)

This is called prototype chaining. Whenever you access a property of an object in JavaScript, it first checks if the property is available inside the object. If not it checks its prototype object. If it is there then good, you get the value of the property. Otherwise, it will check if the property exists in the prototype’s prototype, if not then again in the prototype’s prototype’s prototype and so on.

So how long it will check in this manner? It will stop if the property is found at any point or if the value of __proto__ at any point is null or undefined. Then it will throw an error to notify you that it was unable to find the property you were looking for.

This is how inheritance works in JavaScript with the help of prototype chaining.

Feel free to try the above example with myCar and myMotorBike.

As we know, in JavaScript everything is an object. You will find that for every instance, the prototype chain ends with Object.prototype.

The exception for the above rule is if you create an object with Object.create(null)

var obj = Object.create(null)

With the above code obj will be an empty object without any prototype.

For more information on Object.create check out the documentation on MDN.

Can you change the prototype object of an existing object? Yes, with Object.setPrototypeOf() you can. Check out the documentation in MDN.

Want to check if a property is the object’s own property? You already know how to do this.Object.hasOwnProperty will tell you if the property is coming from the object itself or from its prototype chain. Check out its documentation on MDN.

Note that __proto__ also referred to as [[Prototype]].

Τώρα είναι ώρα για ένα άλλο διάλειμμα. Μόλις είστε έτοιμοι, επιστρέψτε σε αυτό το άρθρο. Στη συνέχεια θα συνεχίσουμε και υπόσχομαι ότι αυτό είναι το τελευταίο μέρος.

Κατανόηση των μαθημάτων σε JavaScript

Σύμφωνα με το MDN:

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

Τα μαθήματα σε JavaScript θα παρέχουν καλύτερη σύνταξη για να επιτύχουμε αυτό που κάναμε παραπάνω με πολύ καθαρότερο τρόπο. Ας ρίξουμε μια ματιά στη σύνταξη της τάξης πρώτα.

class Myclass { constructor(name) { this.name = name; } tellMyName() { console.log(this.name) }}
const myObj = new Myclass("John");

constructorΗ μέθοδος είναι ένας ειδικός τύπος μεθόδου. Θα εκτελείται αυτόματα κάθε φορά που δημιουργείτε μια παρουσία αυτής της κλάσης. Μέσα στο σώμα της τάξης σας. constructorΕίναι δυνατή μόνο μία εμφάνιση .

The methods that you will define inside the class body will be moved to the prototype object.

If you want some property inside the instance you can define it in the constructor, as we did with this.name = name.

Let’s have a look into our myObj.

Note that we have the name property inside the instance that is myObj and the method tellMyName is in the prototype.

Consider the code snippet below:

class Myclass { constructor(firstName) { this.name = firstName; } tellMyName() { console.log(this.name) } lastName = "lewis";}
const myObj = new Myclass("John");

Let’s see the output:

See that lastName is moved into the instance instead of prototype. Only methods you that you declare inside the Class body will be moved to prototype. There is an exception though.

Consider the code snippet below:

class Myclass { constructor(firstName) { this.name = firstName; } tellMyName = () => { console.log(this.name) } lastName = "lewis";}
const myObj = new Myclass("John");

Output:

Note that tellMyName is now an arrow function, and it has been moved to the instance instead of prototype. So remember that arrow functions will always be moved to the instance, so use them carefully.

Let’s look into static class properties:

class Myclass { static welcome() { console.log("Hello World"); }}
Myclass.welcome();const myObj = new Myclass();myObj.welcome();

Output:

Static properties are something that you can access without creating an instance of the class. On the other hand, the instance will not have access to the static properties of a class.

So is static property a new concept that is available only with the class and not in the old school JavaScript? No, it’s there in old school JavaScript also. The old school method of achieving static property is:

function Myclass() {}Myclass.welcome = function() { console.log("Hello World");}

Now let’s have a look at how we can achieve inheritance with classes.

class Vehicle { constructor(type) { this.vehicleType= type; } blowHorn() { console.log("Honk! Honk! Honk!"); }}
class Bus extends Vehicle { constructor(make) { super("Bus"); this.make = make; } accelerator() { console.log('Accelerating Bus'); } brake() { console.log('Braking Bus'); }}
Bus.prototype.noOfWheels = 6;
const myBus = new Bus("Mercedes");

We inherit other classes using the extends keyword.

super() will simply execute the parent class’s constructor. If you are inheriting from other classes and you use the constructor in your child class, then you have to call super() inside the constructor of your child class otherwise it will throw an error.

We already know that if we define any property other than a normal function in the class body it will be moved to the instance instead of prototype. So we define noOfWheel on Bus.prototype.

Inside your class body if you want to execute parent class’s method you can do that using super.parentClassMethod().

Output:

The above output looks similar to our previous function based approach in Fig: 7.

Wrapping up

So should you use new class syntax or old constructor based syntax? I guess there is no definite answer to this question. It depends on your use case.

Σε αυτό το άρθρο, για το τμήμα των τάξεων μόλις έδειξα πώς μπορείτε να επιτύχετε πρωτότυπα μαθήματα κληρονομιάς. Υπάρχουν περισσότερα που πρέπει να γνωρίζετε σχετικά με τα μαθήματα JavaScript, αλλά αυτό δεν εμπίπτει στο πεδίο αυτού του άρθρου. Δείτε την τεκμηρίωση των τάξεων στο MDN. Ή θα προσπαθήσω να γράψω ένα ολόκληρο άρθρο για μαθήματα κάποια στιγμή.

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

Εάν θέλετε να γράψω για κάποιο άλλο θέμα, ενημερώστε με στις απαντήσεις.

Μπορείτε επίσης να συνδεθείτε μαζί μου μέσω του LinkedIn.

Ευχαριστούμε που το διαβάσατε. :)