Το JavaScript μπορεί να φαίνεται σαν μια πολύ εύκολη γλώσσα για πρώτη φορά. Ίσως λόγω της ευέλικτης σύνταξής του. Ή ίσως λόγω της ομοιότητάς του με άλλες γνωστές γλώσσες όπως η Java. Ή ίσως επειδή έχει τόσο λίγους τύπους δεδομένων σε σύγκριση με γλώσσες όπως Java, Ruby ή .NET.
Αλλά στην πραγματικότητα, το JavaScript είναι πολύ λιγότερο απλοϊκό και πιο αποχρωματισμένο από τα περισσότερα προγραμματιστές αρχικά συνειδητοποιούν. Ακόμα και για προγραμματιστές με περισσότερη εμπειρία , μερικές από τις πιο εμφανείς λειτουργίες του JavaScript εξακολουθούν να παρανοούνται και να οδηγούν σε σύγχυση. Ένα τέτοιο χαρακτηριστικό είναι ο τρόπος με τον οποίο πραγματοποιούνται οι αναζητήσεις δεδομένων (ιδιότητες και μεταβλητές) και οι επιπτώσεις της απόδοσης JavaScript που πρέπει να γνωρίζετε.
Στο JavaScript, οι αναζητήσεις δεδομένων διέπονται από δύο πράγματα: πρωτότυπη κληρονομιά και αλυσίδα πεδίου . Ως προγραμματιστής, η σαφής κατανόηση αυτών των δύο μηχανισμών είναι απαραίτητη, καθώς κάτι τέτοιο μπορεί να βελτιώσει τη δομή και συχνά την απόδοση του κώδικα σας.
Κατά την πρόσβαση σε μια ιδιότητα σε μια γλώσσα που βασίζεται σε πρωτότυπα όπως το JavaScript, πραγματοποιείται μια δυναμική αναζήτηση που περιλαμβάνει διαφορετικά επίπεδα μέσα στο πρωτότυπο δέντρο του αντικειμένου.
Σε JavaScript, κάθε συνάρτηση είναι ένα αντικείμενο. Όταν καλείται μια συνάρτηση με το new
τελεστής, δημιουργείται ένα νέο αντικείμενο. Για παράδειγμα:
function Person(firstName, lastName) { this.firstName = firstName; this.lastName = lastName; } var p1 = new Person('John', 'Doe'); var p2 = new Person('Robert', 'Doe');
Στο παραπάνω παράδειγμα, p1
και p2
είναι δύο διαφορετικά αντικείμενα, το καθένα δημιουργήθηκε χρησιμοποιώντας το Person
λειτουργούν ως κατασκευαστής. Είναι ανεξάρτητες περιπτώσεις Person
, όπως αποδεικνύεται από αυτό το απόσπασμα κώδικα:
console.log(p1 instanceof Person); // prints 'true' console.log(p2 instanceof Person); // prints 'true' console.log(p1 === p2); // prints 'false'
Δεδομένου ότι οι λειτουργίες JavaScript είναι αντικείμενα, μπορούν να έχουν ιδιότητες. Μια ιδιαίτερα σημαντική ιδιότητα που έχει κάθε συνάρτηση ονομάζεται prototype
.
prototype
, το οποίο είναι το ίδιο αντικείμενο, κληρονομεί από το πρωτότυπο του γονέα του, το οποίο κληρονομεί από το πρωτότυπο του γονέα του και ούτω καθεξής. Αυτό συχνά αναφέρεται ως το πρωτότυπη αλυσίδα . Object.prototype
, που βρίσκεται πάντα στο τέλος της πρωτότυπης αλυσίδας (δηλαδή, στην κορυφή του πρωτοτύπου δέντρου κληρονομιάς), περιέχει μεθόδους όπως toString()
, hasProperty()
, isPrototypeOf()
, και ούτω καθεξής.
Το πρωτότυπο κάθε συνάρτησης μπορεί να επεκταθεί για να ορίσει τις δικές του προσαρμοσμένες μεθόδους και ιδιότητες.
Όταν δημιουργείτε ένα αντικείμενο (επικαλούμενος τη συνάρτηση χρησιμοποιώντας τον τελεστή new
), κληρονομεί όλες τις ιδιότητες στο πρωτότυπο αυτής της συνάρτησης. Λάβετε υπόψη, ωστόσο, ότι αυτές οι περιπτώσεις δεν θα έχουν άμεση πρόσβαση στο prototype
αντικείμενο αλλά μόνο στις ιδιότητές του. Για παράδειγμα:
// Extending the Person prototype from our earlier example to // also include a 'getFullName' method: Person.prototype.getFullName = function() { return this.firstName + ' ' + this.lastName; } // Referencing the p1 object from our earlier example console.log(p1.getFullName()); // prints 'John Doe' // but p1 can’t directly access the 'prototype' object... console.log(p1.prototype); // prints 'undefined' console.log(p1.prototype.getFullName()); // generates an error
Υπάρχει ένα σημαντικό και κάπως λεπτό σημείο εδώ: Ακόμα κι αν p1
δημιουργήθηκε πριν από το getFullName
ορίστηκε μέθοδος, θα εξακολουθεί να έχει πρόσβαση σε αυτό, επειδή το πρωτότυπο του είναι το Person
πρωτότυπο.
(Αξίζει να σημειωθεί ότι τα προγράμματα περιήγησης αποθηκεύουν επίσης μια αναφορά στο πρωτότυπο οποιουδήποτε αντικειμένου σε μια ιδιότητα __proto__
, αλλά είναι πραγματικά κακή πρακτική για άμεση πρόσβαση στο πρωτότυπο μέσω του __proto__
ιδιοκτησία, καθώς δεν αποτελεί μέρος του προτύπου Προδιαγραφή γλώσσας ECMAScript , οπότε μην το κάνετε! )
Από το p1
παρουσία του Person
το αντικείμενο δεν έχει το ίδιο άμεση πρόσβαση στο prototype
αντικείμενο, αν θέλουμε αντικατάσταση getFullName
στο p1
, θα το κάναμε ως εξής:
// We reference p1.getFullName, *NOT* p1.prototype.getFullName, // since p1.prototype does not exist: p1.getFullName = function(){ return 'I am anonymous'; }
Τώρα p1
έχει το δικό του getFullName
ιδιοκτησία. Αλλά το p2
Το παράδειγμα (δημιουργήθηκε στο προηγούμενο παράδειγμα) δεν έχει οποιαδήποτε τέτοια ιδιοκτησία. Επομένως, επικαλούμενη p1.getFullName()
έχει πρόσβαση στο getFullName
μέθοδος του p1
το ίδιο το παράδειγμα, κατά την επίκληση p2.getFullName()
ανεβαίνει την πρωτότυπη αλυσίδα στο Person
πρωτότυπο αντικείμενο προς επίλυση getFullName
:
console.log(p1.getFullName()); // prints 'I am anonymous' console.log(p2.getFullName()); // prints 'Robert Doe'
Ένα άλλο σημαντικό πράγμα που πρέπει να γνωρίζετε είναι ότι είναι επίσης δυνατό δυναμικά αλλάξτε το πρωτότυπο ενός αντικειμένου. Για παράδειγμα:
function Parent() { this.someVar = 'someValue'; }; // extend Parent’s prototype to define a 'sayHello' method Parent.prototype.sayHello = function(){ console.log('Hello'); }; function Child(){ // this makes sure that the parent's constructor is called and that // any state is initialized correctly. Parent.call(this); }; // extend Child's prototype to define an 'otherVar' property... Child.prototype.otherVar = 'otherValue'; // ... but then set the Child's prototype to the Parent prototype // (whose prototype doesn’t have any 'otherVar' property defined, // so the Child prototype no longer has ‘otherVar’ defined!) Child.prototype = Object.create(Parent.prototype); var child = new Child(); child.sayHello(); // prints 'Hello' console.log(child.someVar); // prints 'someValue' console.log(child.otherVar); // prints 'undefined'
Όταν χρησιμοποιείτε πρωτότυπη κληρονομιά, θυμηθείτε να ορίσετε ιδιότητες στο πρωτότυπο μετά είτε κληρονόμησε από τη γονική τάξη είτε έχει καθορίσει ένα εναλλακτικό πρωτότυπο.
Συνοψίζοντας, οι αναζητήσεις ιδιοτήτων μέσω της αλυσίδας πρωτότυπου JavaScript λειτουργούν ως εξής:
hasOwnProperty
μέθοδος μπορεί να χρησιμοποιηθεί για να ελέγξει εάν ένα αντικείμενο έχει μια συγκεκριμένη ονομασία ιδιότητα.)Object.prototype
έχει επιτευχθεί και δεν διαθέτει ούτε την ιδιότητα, η ιδιότητα θεωρείται undefined
.Η κατανόηση του τρόπου λειτουργίας των πρωτότυπων κληρονομιών και αναζητήσεων ιδιοκτησίας είναι γενικά σημαντική για τους προγραμματιστές, αλλά είναι επίσης απαραίτητη λόγω των (μερικές φορές σημαντικών) επιπτώσεων της απόδοσης JavaScript. Όπως αναφέρεται στην τεκμηρίωση για V8 (Ανοιχτός κώδικας Google, μηχανή JavaScript υψηλής απόδοσης), οι περισσότερες μηχανές JavaScript χρησιμοποιούν δομή δεδομένων τύπου λεξικού για την αποθήκευση ιδιοτήτων αντικειμένου. Επομένως, κάθε πρόσβαση ιδιοκτησίας απαιτεί μια δυναμική αναζήτηση σε αυτήν τη δομή δεδομένων για την επίλυση της ιδιότητας. Αυτή η προσέγγιση καθιστά την πρόσβαση σε ιδιότητες σε JavaScript συνήθως πολύ πιο αργή από την πρόσβαση σε μεταβλητές παρουσίας σε γλώσσες προγραμματισμού όπως Java και Smalltalk.
Ένας άλλος μηχανισμός αναζήτησης στο JavaScript βασίζεται στο πεδίο εφαρμογής.
Για να κατανοήσετε πώς λειτουργεί αυτό, είναι απαραίτητο να εισαγάγετε την έννοια του πλαίσιο εκτέλεσης .
είναι το llc μου s ή c corp;
Στο JavaScript, υπάρχουν δύο τύποι πλαισίων εκτέλεσης:
Τα περιβάλλοντα εκτέλεσης οργανώνονται σε μια στοίβα. Στο κάτω μέρος της στοίβας, υπάρχει πάντα το παγκόσμιο πλαίσιο, το οποίο είναι μοναδικό για κάθε πρόγραμμα JavaScript. Κάθε φορά που συναντά μια συνάρτηση, δημιουργείται ένα νέο πλαίσιο εκτέλεσης και ωθείται στην κορυφή της στοίβας. Μόλις ολοκληρωθεί η εκτέλεση της λειτουργίας, το περιβάλλον της βγαίνει από τη στοίβα.
Εξετάστε τον ακόλουθο κώδικα:
// global context var message = 'Hello World'; var sayHello = function(n){ // local context 1 created and pushed onto context stack var i = 0; var innerSayHello = function() { // local context 2 created and pushed onto context stack console.log((i + 1) + ': ' + message); // local context 2 popped off of context stack } for (i = 0; i Σε κάθε πλαίσιο εκτέλεσης υπάρχει ένα ειδικό αντικείμενο που ονομάζεται a αλυσίδα πεδίου που χρησιμοποιείται για την επίλυση μεταβλητών. Μια αλυσίδα πεδίων είναι ουσιαστικά μια στοίβα προσβάσιμων περιοχών, από το πιο άμεσο πλαίσιο έως το παγκόσμιο πλαίσιο. (Για να είμαστε λίγο πιο ακριβείς, το αντικείμενο στην κορυφή της στοίβας ονομάζεται Αντικείμενο ενεργοποίησης που περιέχει αναφορές στις τοπικές μεταβλητές για τη συνάρτηση που εκτελείται, τα ορίσματα συνάρτησης με όνομα και δύο 'ειδικά' αντικείμενα: this
και arguments
.) Για παράδειγμα:

Σημειώστε στο παραπάνω διάγραμμα πώς this
δείχνει το window
αντικείμενο από προεπιλογή και επίσης πώς το καθολικό περιβάλλον περιέχει παραδείγματα άλλων αντικειμένων όπως console
και location
.
Όταν προσπαθείτε να επιλύσετε μεταβλητές μέσω της αλυσίδας πεδίου, το άμεσο περιβάλλον ελέγχεται πρώτα για μια αντίστοιχη μεταβλητή. Εάν δεν βρεθεί αντιστοίχιση, ελέγχεται το επόμενο αντικείμενο περιβάλλοντος στην αλυσίδα πεδίου και ούτω καθεξής, έως ότου βρεθεί μια αντιστοίχιση. Εάν δεν βρεθεί αντιστοιχία, a ReferenceError
ρίχνεται.
Είναι επίσης σημαντικό να σημειωθεί ότι ένα νέο πεδίο προστίθεται στην αλυσίδα πεδίου όταν ένα try-catch
μπλοκ ή a with
συναντάμε μπλοκ. Σε οποιαδήποτε από αυτές τις περιπτώσεις, δημιουργείται ένα νέο αντικείμενο και τοποθετείται στην κορυφή της αλυσίδας πεδίου:
function Person(firstName, lastName) { this.firstName = firstName; this.lastName = lastName; }; function persist(person) { with (person) { // The 'person' object was pushed onto the scope chain when we // entered this 'with' block, so we can simply reference // 'firstName' and 'lastName', rather than person.firstName and // person.lastName if (!firstName) { throw new Error('FirstName is mandatory'); } if (!lastName) { throw new Error('LastName is mandatory'); } } try { person.save(); } catch(error) { // A new scope containing the 'error' object is accessible here console.log('Impossible to store ' + person + ', Reason: ' + error); } } var p1 = new Person('John', 'Doe'); persist(p1);
Για να κατανοήσετε πλήρως τον τρόπο με τον οποίο εμφανίζονται οι μεταβλητές αναζητήσεις βάσει εύρους, είναι σημαντικό να έχετε υπόψη ότι στο JavaScript δεν υπάρχουν επί του παρόντος πεδία σε επίπεδο μπλοκ. Για παράδειγμα:
for (var i = 0; i <10; i++) { /* ... */ } // 'i' is still in scope! console.log(i); // prints '10'
Στις περισσότερες άλλες γλώσσες, ο παραπάνω κώδικας θα οδηγούσε σε σφάλμα επειδή η 'ζωή' (δηλαδή, πεδίο εφαρμογής) της μεταβλητής i
θα περιοριζόταν στο μπλοκ για. Αυτό όμως δεν συμβαίνει στο JavaScript. Αντίθετα, i
προστίθεται στο αντικείμενο ενεργοποίησης στην κορυφή της αλυσίδας πεδίου και θα παραμείνει εκεί μέχρι να αφαιρεθεί το αντικείμενο από το πεδίο εφαρμογής, το οποίο συμβαίνει όταν το αντίστοιχο περιβάλλον εκτέλεσης αφαιρεθεί από τη στοίβα. Αυτή η συμπεριφορά είναι γνωστή ως μεταβλητή ανύψωση.
Αξίζει να σημειωθεί, ωστόσο, ότι η υποστήριξη για πεδία σε επίπεδο μπλοκ μπαίνει στο JavaScript μέσω του νέου let
λέξη-κλειδί. Το let
Η λέξη-κλειδί είναι ήδη διαθέσιμη στο JavaScript 1.7 και πρόκειται να γίνει επίσημα υποστηριζόμενη λέξη-κλειδί JavaScript από το ECMAScript 6.
Επιπτώσεις απόδοσης JavaScript
Ο τρόπος με τον οποίο η ιδιότητα και οι μεταβλητές αναζητήσεις, χρησιμοποιώντας πρωτότυπη αλυσίδα και αλυσίδα πεδίου, αντίστοιχα, λειτουργούν σε JavaScript είναι ένα από τα βασικά χαρακτηριστικά της γλώσσας, αλλά είναι ένα από τα πιο δύσκολα και πιο διακριτικά.
Οι λειτουργίες αναζήτησης που έχουμε περιγράψει σε αυτό το παράδειγμα, είτε βασίζονται στην πρωτότυπη αλυσίδα είτε στην αλυσίδα πεδίου, επαναλαμβάνονται κάθε ώρα πρόσβασης σε μια ιδιότητα ή μεταβλητή. Όταν αυτή η αναζήτηση πραγματοποιείται εντός βρόχων ή άλλων εντατικών λειτουργιών, μπορεί να έχει σημαντικές επιπτώσεις απόδοσης JavaScript, ειδικά υπό το πρίσμα της μονής σπείρας της γλώσσας που εμποδίζει την ταυτόχρονη εμφάνιση πολλαπλών λειτουργιών.
Εξετάστε το ακόλουθο παράδειγμα:
var start = new Date().getTime(); function Parent() { this.delta = 10; }; function ChildA(){}; ChildA.prototype = new Parent(); function ChildB(){} ChildB.prototype = new ChildA(); function ChildC(){} ChildC.prototype = new ChildB(); function ChildD(){}; ChildD.prototype = new ChildC(); function ChildE(){}; ChildE.prototype = new ChildD(); function nestedFn() { var child = new ChildE(); var counter = 0; for(var i = 0; i <1000; i++) { for(var j = 0; j < 1000; j++) { for(var k = 0; k < 1000; k++) { counter += child.delta; } } } console.log('Final result: ' + counter); } nestedFn(); var end = new Date().getTime(); var diff = end - start; console.log('Total time: ' + diff + ' milliseconds');
Σε αυτό το παράδειγμα, έχουμε ένα μακρύ δέντρο κληρονομιάς και τρεις ένθετους βρόχους. Μέσα στον βαθύτερο βρόχο, η μεταβλητή μετρητή αυξάνεται με την τιμή delta
. Αλλά delta
βρίσκεται σχεδόν στην κορυφή του δέντρου κληρονομιάς! Αυτό σημαίνει ότι κάθε φορά child.delta
έχει πρόσβαση, το πλήρες δέντρο πρέπει να πλοηγηθεί από κάτω προς τα πάνω. Αυτό μπορεί να έχει πολύ αρνητικό αντίκτυπο στην απόδοση.
Κατανοώντας αυτό, μπορούμε εύκολα να βελτιώσουμε την απόδοση των παραπάνω nestedFn
λειτουργία χρησιμοποιώντας ένα τοπικό delta
μεταβλητή για την προσωρινή αποθήκευση της τιμής στο child.delta
(και έτσι αποφεύγεται η ανάγκη επαναλαμβανόμενης διέλευσης ολόκληρου του δέντρου κληρονομιάς) ως εξής:
function nestedFn() { var child = new ChildE(); var counter = 0; var delta = child.delta; // cache child.delta value in current scope for(var i = 0; i <1000; i++) { for(var j = 0; j < 1000; j++) { for(var k = 0; k < 1000; k++) { counter += delta; // no inheritance tree traversal needed! } } } console.log('Final result: ' + counter); } nestedFn(); var end = new Date().getTime(); var diff = end - start; console.log('Total time: ' + diff + ' milliseconds');
Φυσικά, αυτή η συγκεκριμένη τεχνική είναι βιώσιμη μόνο σε ένα σενάριο όπου είναι γνωστό ότι η τιμή child.delta
δεν θα αλλάξει κατά την εκτέλεση των βρόχων. Διαφορετικά, το τοπικό αντίγραφο θα πρέπει να ενημερωθεί με την τρέχουσα τιμή.
Εντάξει, ας τρέξουμε και τις δύο εκδόσεις του nestedFn
μέθοδο και δείτε εάν υπάρχει σημαντική διαφορά απόδοσης μεταξύ των δύο.
Θα ξεκινήσουμε τρέχοντας το πρώτο παράδειγμα στο a node.js REPL :
[email protected] :~$ node test.js Final result: 10000000000 Total time: 8270 milliseconds
Χρειάζεται περίπου 8 δευτερόλεπτα για να τρέξει. Αυτό είναι πολύ καιρό.
Τώρα ας δούμε τι συμβαίνει όταν εκτελούμε τη βελτιστοποιημένη έκδοση:
[email protected] :~$ node test2.js Final result: 10000000000 Total time: 1143 milliseconds
Αυτή τη φορά χρειάστηκε μόλις ένα δευτερόλεπτο. Πολύ πιο γρήγορα!
Σημειώστε ότι η χρήση τοπικών μεταβλητών για την αποφυγή δαπανηρών αναζητήσεων είναι μια τεχνική που μπορεί να εφαρμοστεί τόσο για αναζήτηση ιδιοτήτων (μέσω της αλυσίδας πρωτοτύπων) όσο και για μεταβλητές αναζητήσεις (μέσω της αλυσίδας πεδίου).
Επιπλέον, αυτός ο τύπος 'προσωρινής αποθήκευσης' τιμών (δηλαδή, σε μεταβλητές στο τοπικό εύρος) μπορεί επίσης να είναι επωφελής όταν χρησιμοποιείτε μερικές από τις πιο κοινές βιβλιοθήκες JavaScript. Παίρνω jQuery , για παράδειγμα. Το jQuery υποστηρίζει την έννοια των 'επιλογών', οι οποίοι βασικά είναι ένας μηχανισμός για την ανάκτηση ενός ή περισσότερων στοιχείων που ταιριάζουν στο ΚΡΙΣΗ . Η ευκολία με την οποία μπορεί κανείς να καθορίσει τους επιλογείς στο jQuery μπορεί να κάνει κάποιον να ξεχάσει πόσο δαπανηρό (από άποψη απόδοσης) μπορεί να είναι κάθε αναζήτηση επιλογέα. Κατά συνέπεια, η αποθήκευση των αποτελεσμάτων αναζήτησης επιλογών σε μια τοπική μεταβλητή μπορεί να είναι εξαιρετικά επωφελής για την απόδοση. Για παράδειγμα:
// this does the DOM search for $('.container') 'n' times for (var i = 0; i ”); } // this accomplishes the same thing... // but only does the DOM search for $('.container') once, // although it does still modify the DOM 'n' times var $container = $('.container'); for (var i = 0; i '); } // or even better yet... // this version only does the DOM search for $('.container') once // AND only modifies the DOM once var $html = ''; for (var i = 0; i '; } $('.container').append($html);
Ειδικά σε μια ιστοσελίδα με μεγάλο αριθμό στοιχείων, η δεύτερη προσέγγιση στο παραπάνω δείγμα κώδικα μπορεί δυνητικά να οδηγήσει σε σημαντικά καλύτερη απόδοση από την πρώτη.
Τύλιξε
Η αναζήτηση δεδομένων σε JavaScript είναι πολύ διαφορετική από ό, τι στις περισσότερες άλλες γλώσσες και έχει πολύ αποχρώσεις. Είναι επομένως απαραίτητο να κατανοήσουμε πλήρως και σωστά αυτές τις έννοιες για να μάθουμε πραγματικά τη γλώσσα. Αναζήτηση δεδομένων και άλλα κοινά λάθη JavaScript πρέπει να αποφεύγεται όποτε είναι δυνατόν. Αυτή η κατανόηση είναι πιθανό να αποδώσει καθαρότερο, πιο ισχυρό κώδικα που επιτυγχάνει βελτιωμένη απόδοση JavaScript.
Σχετίζεται με: Ως προγραμματιστής JS, αυτό είναι που με κρατάει τη νύχτα / Κάνοντας την αίσθηση της σύγχυσης ES6 Class