Τα τελευταία χρόνια μάς έχουν δείξει ότι η ανάπτυξη ιστοσελίδων αλλάζει. Καθώς πολλά χαρακτηριστικά και APIs προστίθεντο στα προγράμματα περιήγησης, έπρεπε να τα χρησιμοποιούμε με τον σωστό τρόπο. Η γλώσσα στην οποία οφείλουμε αυτή την τιμή ήταν η JavaScript.
Αρχικά, οι προγραμματιστές δεν είχαν πειστεί για τον τρόπο με τον οποίο είχε σχεδιαστεί και είχαν κυρίως αρνητικές εντυπώσεις κατά τη χρήση αυτού του σεναρίου. Με την πάροδο του χρόνου, αποδείχθηκε ότι η γλώσσα αυτή έχει μεγάλες δυνατότητες και τα επόμενα πρότυπα ECMAScript καθιστούν ορισμένους από τους μηχανισμούς πιο ανθρώπινους και, απλώς, καλύτερους. Σε αυτό το άρθρο, ρίχνουμε μια ματιά σε μερικά από αυτά.
Τύποι τιμών στο JS
Η γνωστή αλήθεια για JavaScript είναι ότι τα πάντα εδώ είναι αντικείμενα. Πραγματικά, τα πάντα: πίνακες, συναρτήσεις, συμβολοσειρές, αριθμοί, ακόμη και booleans. Όλοι οι τύποι τιμών αντιπροσωπεύονται από αντικείμενα και έχουν τις δικές τους μεθόδους και πεδία. Ωστόσο, μπορούμε να τα χωρίσουμε σε δύο κατηγορίες: primitives και structurals. Οι τιμές της πρώτης κατηγορίας είναι αμετάβλητες, που σημαίνει ότι μπορούμε να αναθέσουμε εκ νέου σε κάποια μεταβλητή τη νέα τιμή, αλλά δεν μπορούμε να τροποποιήσουμε την ίδια την υπάρχουσα τιμή. Η δεύτερη αντιπροσωπεύει τιμές που μπορούν να τροποποιηθούν, οπότε θα πρέπει να ερμηνεύονται ως συλλογές ιδιοτήτων τις οποίες μπορούμε να αντικαταστήσουμε ή απλά να καλέσουμε τις μεθόδους που είναι σχεδιασμένες για να το κάνουν.
Πεδίο εφαρμογής των δηλωμένων μεταβλητών
Πριν εμβαθύνουμε, ας εξηγήσουμε τι σημαίνει το πεδίο εφαρμογής. Μπορούμε να πούμε ότι η εμβέλεια είναι η μόνη περιοχή όπου μπορούμε να χρησιμοποιήσουμε δηλωμένες μεταβλητές. Πριν από το πρότυπο ES6 μπορούσαμε να δηλώσουμε μεταβλητές με τη δήλωση var και να τους δώσουμε παγκόσμια ή τοπική εμβέλεια. Η πρώτη είναι ένα πεδίο που μας επιτρέπει να έχουμε πρόσβαση σε κάποιες μεταβλητές σε οποιοδήποτε σημείο της εφαρμογής, η δεύτερη είναι απλώς αφιερωμένη σε μια συγκεκριμένη περιοχή - κυρίως σε μια συνάρτηση.
Από το πρότυπο ES2015, JavaScript έχει τρεις τρόπους δήλωσης μεταβλητών που διαφέρουν με τη λέξη-κλειδί. Ο πρώτος περιγράφηκε προηγουμένως: οι μεταβλητές που δηλώνονται με τη λέξη-κλειδί var έχουν εμβέλεια στο σώμα της τρέχουσας συνάρτησης. Το πρότυπο ES6 μας επέτρεψε να δηλώνουμε μεταβλητές με πιο ανθρώπινους τρόπους - αντίθετα με τις δηλώσεις var, οι μεταβλητές που δηλώνονται με τις δηλώσεις const και let έχουν εμβέλεια μόνο στο μπλοκ. Ωστόσο, το JS αντιμετωπίζει τη δήλωση const αρκετά ασυνήθιστα αν συγκριθεί με άλλες γλώσσες προγραμματισμού - αντί για μια μόνιμη τιμή, διατηρεί μια μόνιμη αναφορά στην τιμή. Εν ολίγοις, μπορούμε να τροποποιήσουμε τις ιδιότητες ενός αντικειμένου που δηλώνεται με μια δήλωση const, αλλά δεν μπορούμε να αντικαταστήσουμε την αναφορά αυτής της μεταβλητής. Κάποιοι λένε, ότι η εναλλακτική var στο ES6 είναι στην πραγματικότητα η δήλωση let. Όχι, δεν είναι, και η δήλωση var δεν είναι και πιθανότατα δεν θα αποσυρθεί ποτέ. Μια καλή πρακτική είναι να αποφεύγουμε τη χρήση των δηλώσεων var, γιατί ως επί το πλείστον μας δημιουργούν περισσότερα προβλήματα. Με τη σειρά μας, πρέπει να κάνουμε κατάχρηση των δηλώσεων const, μέχρι να χρειαστεί να τροποποιήσουμε την αναφορά της - τότε θα πρέπει να χρησιμοποιήσουμε την let.
Παράδειγμα απροσδόκητης συμπεριφοράς εμβέλειας
Ας ξεκινήσουμε με τα εξής κωδικός:
(() => {
for (var i = 0; i {
console.log(`Τιμή του "i": ${i}`),
}, 1000);
}
})();
Όταν το εξετάζουμε, φαίνεται ότι ο βρόχος for επαναλαμβάνει την τιμή i και, μετά από ένα δευτερόλεπτο, θα καταγράψει τις τιμές του επαναλήπτη: 1, 2, 3, 4, 5. Λοιπόν, αυτό δεν συμβαίνει. Όπως αναφέραμε παραπάνω, η δήλωση var αφορά τη διατήρηση της τιμής μιας μεταβλητής για όλο το σώμα της συνάρτησης- αυτό σημαίνει ότι στη δεύτερη, τρίτη κ.ο.κ. επανάληψη η τιμή της μεταβλητής i θα αντικατασταθεί με μια επόμενη τιμή. Τέλος, ο βρόχος τελειώνει και τα χρονικά τικ μας δείχνουν το εξής: 5, 5, 5, 5, 5, 5, 5. Ο καλύτερος τρόπος για να διατηρήσετε μια τρέχουσα τιμή του επαναλήπτη είναι να χρησιμοποιήσετε αντί αυτού τη δήλωση let:
(() => {
for (let i = 0; i {
console.log(`Τιμή του "i": ${i}`),
}, 1000);
}
})();
Στο παραπάνω παράδειγμα, κρατάμε την εμβέλεια της τιμής i στο τρέχον μπλοκ επανάληψης, είναι το μόνο πεδίο όπου μπορούμε να χρησιμοποιήσουμε αυτή τη μεταβλητή και τίποτα δεν μπορεί να την παρακάμψει έξω από αυτή την περιοχή. Το αποτέλεσμα σε αυτή την περίπτωση είναι το αναμενόμενο: 1 2 3 4 5. Ας δούμε πώς μπορούμε να χειριστούμε αυτή την κατάσταση με μια δήλωση var:
(() => {
for (var i = 0; i {
setTimeout(() => {
console.log(`Τιμή του "j": ${j}`),
}, 1000);
})(i);
}
})();
Καθώς η δήλωση var αφορά τη διατήρηση της τιμής εντός του μπλοκ συνάρτησης, πρέπει να καλέσουμε μια καθορισμένη συνάρτηση η οποία λαμβάνει ένα όρισμα - την τιμή της τρέχουσας κατάστασης του επαναλήπτη - και στη συνέχεια απλώς να κάνουμε κάτι. Τίποτα εκτός της δηλωμένης συνάρτησης δεν θα παρακάμψει την τιμή j.
Παραδείγματα λανθασμένων προσδοκιών για τις τιμές των αντικειμένων
Το πιο συχνό έγκλημα που παρατήρησα αφορά την αγνόηση της δύναμης των δομικών στοιχείων και την αλλαγή των ιδιοτήτων τους, οι οποίες τροποποιούνται και σε άλλα κομμάτια κώδικα. Ρίξτε μια γρήγορη ματιά:
const DEFAULT_VALUE = {
favoriteBand: 'The Weeknd'
};
const currentValue = DEFAULT_VALUE,
const bandInput = document.querySelector('#favorite-band'),
const restoreDefaultButton = document.querySelector('#restore-button'),
bandInput.addEventListener('input', () => {
currentValue.favoriteBand = bandInput.value,
}, false),
restoreDefaultButton.addEventListener('click', () => {
currentValue = DEFAULT_VALUE,
}, false),
Από την αρχή: ας υποθέσουμε ότι έχουμε ένα μοντέλο με προεπιλεγμένες ιδιότητες, αποθηκευμένο ως αντικείμενο. Θέλουμε να έχουμε ένα κουμπί που να επαναφέρει τις τιμές εισόδου του στις προεπιλεγμένες. Αφού συμπληρώσουμε την είσοδο με κάποιες τιμές, ενημερώνουμε το μοντέλο. Μετά από λίγο, σκεφτόμαστε ότι η προεπιλεγμένη επιλογή ήταν απλά καλύτερη, οπότε θέλουμε να την επαναφέρουμε. Κάνουμε κλικ στο κουμπί... και δεν συμβαίνει τίποτα. Γιατί; Επειδή αγνοούμε τη δύναμη των αναφερόμενων τιμών.
Αυτό το μέρος: const currentValue = DEFAULTVALUE λέει στο JS τα εξής: πάρτε την αναφορά στο DEFAULTVALUE και εκχωρήστε τη στη μεταβλητή currentValue. Η πραγματική τιμή αποθηκεύεται στη μνήμη μόνο μία φορά και οι δύο μεταβλητές δείχνουν σε αυτήν. Η τροποποίηση κάποιων ιδιοτήτων σε ένα μέρος σημαίνει την τροποποίησή τους σε ένα άλλο μέρος. Έχουμε μερικούς τρόπους για να αποφύγουμε τέτοιες καταστάσεις. Ένας από αυτούς που ικανοποιεί τις ανάγκες μας είναι ο τελεστής εξάπλωσης. Ας διορθώσουμε τον κώδικά μας:
const DEFAULT_VALUE = {
favoriteBand: 'The Weeknd'
};
const currentValue = { ...DEFAULT_VALUE },
const bandInput = document.querySelector('#favorite-band'),
const restoreDefaultButton = document.querySelector('#restore-button'),
bandInput.addEventListener('input', () => {
currentValue.favoriteBand = bandInput.value,
}, false),
restoreDefaultButton.addEventListener('click', () => {
currentValue = { ...DEFAULT_VALUE },
}, false),
Σε αυτή την περίπτωση, ο τελεστής εξάπλωσης λειτουργεί ως εξής: παίρνει όλες τις ιδιότητες από ένα αντικείμενο και δημιουργεί ένα νέο αντικείμενο γεμάτο με αυτές. Χάρη σε αυτό, οι τιμές στις currentValue και DEFAULT_VALUE δεν δείχνουν πλέον στο ίδιο σημείο της μνήμης και όλες οι αλλαγές που εφαρμόζονται σε μία από αυτές δεν θα επηρεάσουν τις άλλες.
Εντάξει, οπότε το ερώτημα είναι: είναι όλα σχετικά με τη χρήση του μαγικού τελεστή εξάπλωσης; Σε αυτή την περίπτωση - ναι, αλλά τα μοντέλα μας μπορεί να απαιτούν μεγαλύτερη πολυπλοκότητα από αυτό το παράδειγμα. Σε περίπτωση που χρησιμοποιούμε φωλιασμένα αντικείμενα, πίνακες ή οποιαδήποτε άλλη δομική δομή, ο τελεστής εξάπλωσης της τιμής που αναφέρεται στο ανώτερο επίπεδο θα επηρεάσει μόνο το ανώτερο επίπεδο και οι ιδιότητες που αναφέρονται θα εξακολουθούν να μοιράζονται την ίδια θέση στη μνήμη. Υπάρχουν πολλές λύσεις για τον χειρισμό αυτού του προβλήματος, όλα εξαρτώνται από τις ανάγκες σας. Μπορούμε να κλωνοποιήσουμε αντικείμενα σε κάθε επίπεδο βάθους ή, σε πιο σύνθετες λειτουργίες, να χρησιμοποιήσουμε εργαλεία όπως το immer που μας επιτρέπει να γράψουμε αμετάβλητο κώδικα σχεδόν ανώδυνα.
Ανακατέψτε τα όλα μαζί
Είναι χρήσιμος ένας συνδυασμός γνώσεων σχετικά με τα πεδία εφαρμογής και τους τύπους τιμών; Φυσικά και είναι! Ας φτιάξουμε κάτι που χρησιμοποιεί και τα δύο:
const useValue = (defaultValue) => {
const value = [...defaultValue],
const setValue = (newValue) => {
value.length = 0; // δύσκολος τρόπος εκκαθάρισης του πίνακα
newValue.forEach((item, index) => {
value[index] = item,
});
// κάνουμε κάποια άλλα πράγματα
};
return [value, setValue],
};
const [animals, setAnimals] = useValue(['cat', 'dog']),
console.log(animals); // ['cat', 'dog']
setAnimals(['horse', 'cow']),
console.log(animals); // ['horse', 'cow']),
Ας εξηγήσουμε πώς λειτουργεί αυτός ο κώδικας γραμμή προς γραμμή. Λοιπόν, η συνάρτηση useValue δημιουργεί έναν πίνακα με βάση το όρισμα defaultValue- δημιουργεί μια μεταβλητή και μια άλλη συνάρτηση, τον τροποποιητή της. Αυτός ο τροποποιητής παίρνει μια νέα τιμή η οποία με έναν περίεργο τρόπο εφαρμόζεται στην υπάρχουσα. Στο τέλος της συνάρτησης, επιστρέφουμε την τιμή και τον τροποποιητή της ως τιμές πίνακα. Στη συνέχεια, χρησιμοποιούμε τη συνάρτηση που δημιουργήθηκε - δηλώνουμε τα ζώα και τα setAnimals ως επιστρεφόμενες τιμές. Χρησιμοποιούμε τον τροποποιητή τους για να ελέγξουμε αν η συνάρτηση επηρεάζει τη μεταβλητή animal - ναι, λειτουργεί!
Αλλά περιμένετε, τι ακριβώς είναι τόσο φανταχτερό σε αυτόν τον κώδικα; Η αναφορά διατηρεί όλες τις νέες τιμές και μπορείτε να εισάγετε τη δική σας λογική σε αυτόν τον τροποποιητή, όπως ορισμένα APIs ή μέρος του οικοσυστήματος που τροφοδοτεί τη ροή δεδομένων σας χωρίς καμία προσπάθεια. Αυτό το δύσκολο μοτίβο χρησιμοποιείται συχνά σε πιο σύγχρονες βιβλιοθήκες JS, όπου το λειτουργικό παράδειγμα στον προγραμματισμό μας επιτρέπει να διατηρούμε τον κώδικα λιγότερο πολύπλοκο και πιο ευανάγνωστο από άλλους προγραμματιστές.
Περίληψη
Η κατανόηση του τρόπου με τον οποίο λειτουργούν οι μηχανισμοί της γλώσσας κάτω από την κουκούλα μας επιτρέπει να γράφουμε πιο συνειδητό και ελαφρύ κώδικα. Ακόμη και αν η JS δεν είναι γλώσσα χαμηλού επιπέδου και μας αναγκάζει να έχουμε κάποιες γνώσεις σχετικά με τον τρόπο ανάθεσης και αποθήκευσης της μνήμης, πρέπει να έχουμε το νου μας για απροσδόκητες συμπεριφορές κατά την τροποποίηση αντικειμένων. Από την άλλη πλευρά, η κατάχρηση των κλώνων τιμών δεν είναι πάντα ο σωστός τρόπος και η λανθασμένη χρήση έχει περισσότερα μειονεκτήματα παρά πλεονεκτήματα. Ο σωστός τρόπος σχεδιασμού της ροής δεδομένων είναι να σκεφτείτε τι χρειάζεστε και ποια πιθανά εμπόδια μπορεί να συναντήσετε κατά την υλοποίηση της λογικής της εφαρμογής.
Διαβάστε περισσότερα: