Πώς να κάνετε διάκριση μεταξύ βαθιών και ρηχών αντιγράφων σε JavaScript

Το νέο είναι πάντα καλύτερο!

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

Πρώτα απ 'όλα, τι είναι ένα αντίγραφο;

Ένα αντίγραφο μοιάζει με το παλιό, αλλά δεν είναι. Όταν αλλάζετε το αντίγραφο, περιμένετε το αρχικό πράγμα να παραμείνει το ίδιο, ενώ το αντίγραφο αλλάζει.

Στον προγραμματισμό, αποθηκεύουμε τιμές σε μεταβλητές. Η δημιουργία αντιγράφου σημαίνει ότι ξεκινάτε μια νέα μεταβλητή με τις ίδιες τιμές. Ωστόσο, υπάρχει μια μεγάλη πιθανή παγίδα που πρέπει να λάβετε υπόψη: αντιγραφή σε βάθος έναντι ρηχής αντιγραφής . Ένα βαθύ αντίγραφο σημαίνει ότι όλες οι τιμές της νέας μεταβλητής αντιγράφονται και αποσυνδέονται από την αρχική μεταβλητή. Ένα ρηχό αντίγραφο σημαίνει ότι ορισμένες (υπο-) τιμές εξακολουθούν να συνδέονται με την αρχική μεταβλητή.

Για να κατανοήσετε πραγματικά την αντιγραφή, πρέπει να μάθετε πώς αποθηκεύει τις τιμές τις τιμές JavaScript.

Πρωτόγονοι τύποι δεδομένων

Οι πρωτόγονοι τύποι δεδομένων περιλαμβάνουν τα ακόλουθα:

  • Αριθμός - π.χ. 1
  • Συμβολοσειρά - π.χ. 'Hello'
  • Boolean - π.χ. true
  • undefined
  • null

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

const a = 5
let b = a // this is the copy
b = 6
console.log(b) // 6
console.log(a) // 5

Με την εκτέλεση b = a, δημιουργείτε το αντίγραφο. Τώρα, όταν εκχωρείτε ξανά μια νέα τιμή b, την τιμή των bαλλαγών, αλλά όχι του a.

Τύποι σύνθετων δεδομένων - Αντικείμενα και πίνακες

Τεχνικά, οι πίνακες είναι επίσης αντικείμενα, έτσι συμπεριφέρονται με τον ίδιο τρόπο. Θα εξετάσω και τα δύο λεπτομερώς αργότερα.

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

Τώρα, αν φτιάξουμε ένα αντίγραφο b = aκαι αλλάξουμε κάποια ένθετη τιμή b, αλλάζει aκαι την ένθετη τιμή, καθώς aκαι bστην πραγματικότητα δείχνουν το ίδιο πράγμα. Παράδειγμα:

const a = {
 en: 'Hello',
 de: 'Hallo',
 es: 'Hola',
 pt: 'Olà'
}
let b = a
b.pt = 'Oi'
console.log(b.pt) // Oi
console.log(a.pt) // Oi

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

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

Αντικείμενα

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

Διαχειριστής Spread

Παρουσιάζεται με το ES2015, αυτός ο χειριστής είναι απίστευτος, γιατί είναι τόσο σύντομος και απλός. Διαχέει όλες τις τιμές σε ένα νέο αντικείμενο. Μπορείτε να το χρησιμοποιήσετε ως εξής:

const a = {
 en: 'Bye',
 de: 'Tschüss'
}
let b = {...a}
b.de = 'Ciao'
console.log(b.de) // Ciao
console.log(a.de) // Tschüss

Μπορείτε επίσης να το χρησιμοποιήσετε για να συγχωνεύσετε δύο αντικείμενα, για παράδειγμα const c = {...a, ...b}.

Object.assign

Αυτό χρησιμοποιήθηκε ως επί το πλείστον πριν ο τελεστής διασποράς ήταν γύρω, και ουσιαστικά κάνει το ίδιο πράγμα. Πρέπει όμως να είστε προσεκτικοί, καθώς το πρώτο επιχείρημα στη Object.assign()μέθοδο τροποποιείται και επιστρέφεται. Γι 'αυτό βεβαιωθείτε ότι μεταβιβάζετε το αντικείμενο για αντιγραφή τουλάχιστον ως το δεύτερο όρισμα. Κανονικά, θα περάσατε απλά ένα κενό αντικείμενο ως το πρώτο όρισμα για να αποτρέψετε την τροποποίηση τυχόν υπαρχόντων δεδομένων.

const a = {
 en: 'Bye',
 de: 'Tschüss'
}
let b = Object.assign({}, a)
b.de = 'Ciao'
console.log(b.de) // Ciao
console.log(a.de) // Tschüss

Pitfall: Ένθετα αντικείμενα

Όπως αναφέρθηκε προηγουμένως, υπάρχει μια μεγάλη προειδοποίηση κατά την αντιγραφή αντικειμένων, η οποία ισχύει και για τις δύο μεθόδους που αναφέρονται παραπάνω. Όταν έχετε ένα ένθετο αντικείμενο (ή πίνακα) και το αντιγράφετε, τα ένθετα αντικείμενα μέσα σε αυτό το αντικείμενο δεν θα αντιγραφούν, καθώς είναι μόνο δείκτες / αναφορές. Επομένως, εάν αλλάξετε το ένθετο αντικείμενο, θα το αλλάξετε και για τις δύο περιπτώσεις, πράγμα που σημαίνει ότι θα καταλήξετε να κάνετε ένα ρηχό αντίγραφο ξανά . Παράδειγμα: // ΚΑΚΟ ΠΑΡΑΔΕΙΓΜΑ

const a = {
 foods: {
 dinner: 'Pasta'
 }
}
let b = {...a}
b.foods.dinner = 'Soup' // changes for both objects
console.log(b.foods.dinner) // Soup
console.log(a.foods.dinner) // Soup

Για να δημιουργήσετε ένα βαθύ αντίγραφο ένθετων αντικειμένων , θα πρέπει να το σκεφτείτε. Ένας τρόπος για να αποτρέψετε αυτό είναι η μη αυτόματη αντιγραφή όλων των ένθετων αντικειμένων:

const a = {
 foods: {
 dinner: 'Pasta'
 }
}
let b = {foods: {...a.foods}}
b.foods.dinner = 'Soup'
console.log(b.foods.dinner) // Soup
console.log(a.foods.dinner) // Pasta

Σε περίπτωση που αναρωτιέστε τι να κάνετε όταν το αντικείμενο έχει περισσότερα πλήκτρα παρά μόνο foods, μπορείτε να χρησιμοποιήσετε το πλήρες δυναμικό του χειριστή spread. Όταν περνούν περισσότερες ιδιότητες μετά το ...spread, αντικαθιστούν τις αρχικές τιμές, για παράδειγμα const b = {...a, foods: {...a.foods}}.

Δημιουργία βαθιών αντιγράφων χωρίς σκέψη

Τι γίνεται αν δεν γνωρίζετε πόσο βαθιά είναι οι ένθετες κατασκευές; Μπορεί να είναι πολύ κουραστικό να χειρίζεστε χειροκίνητα μεγάλα αντικείμενα και να αντιγράφετε κάθε ένθετο αντικείμενο με το χέρι. Υπάρχει ένας τρόπος να αντιγράψετε τα πάντα χωρίς να σκεφτείτε. Απλά stringifyτο αντικείμενο σας και parseαμέσως μετά:

const a = {
 foods: {
 dinner: 'Pasta'
 }
}
let b = JSON.parse(JSON.stringify(a))
b.foods.dinner = 'Soup'
console.log(b.foods.dinner) // Soup
console.log(a.foods.dinner) // Pasta

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

Πίνακες

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

Διαχειριστής Spread

Όπως και με τα αντικείμενα, μπορείτε να χρησιμοποιήσετε τον τελεστή επέκτασης για να αντιγράψετε έναν πίνακα:

const a = [1,2,3]
let b = [...a]
b[1] = 4
console.log(b[1]) // 4
console.log(a[1]) // 2

Λειτουργίες συστοιχίας - χάρτης, φίλτρο, μείωση

Αυτές οι μέθοδοι θα επιστρέψουν έναν νέο πίνακα με όλες (ή μερικές) τιμές της αρχικής. Ενώ το κάνετε αυτό, μπορείτε επίσης να τροποποιήσετε τις τιμές, οι οποίες είναι πολύ βολικές:

const a = [1,2,3]
let b = a.map(el => el)
b[1] = 4
console.log(b[1]) // 4
console.log(a[1]) // 2

Εναλλακτικά μπορείτε να αλλάξετε το επιθυμητό στοιχείο κατά την αντιγραφή:

const a = [1,2,3]
const b = a.map((el, index) => index === 1 ? 4 : el)
console.log(b[1]) // 4
console.log(a[1]) // 2

Array.slice

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

const a = [1,2,3]
let b = a.slice(0)
b[1] = 4
console.log(b[1]) // 4
console.log(a[1]) // 2

Ένθετες συστοιχίες

Παρόμοια με τα αντικείμενα, η χρήση των παραπάνω μεθόδων για την αντιγραφή ενός πίνακα με έναν άλλο πίνακα ή αντικείμενο μέσα θα δημιουργήσει ένα ρηχό αντίγραφο . Για να το αποτρέψετε, χρησιμοποιήστε επίσης JSON.parse(JSON.stringify(someArray)).

BONUS: αντιγραφή παρουσίας προσαρμοσμένων κλάσεων

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

Όπως αναφέρθηκε προηγουμένως, δεν μπορείτε απλώς να το κάνετε πιο αυστηρό + να το αναλύσετε, καθώς θα χάσετε τις μεθόδους της τάξης σας. Αντ 'αυτού, θα θέλατε να προσθέσετε μια προσαρμοσμένη copyμέθοδο για να δημιουργήσετε μια νέα παρουσία με όλες τις παλιές τιμές. Ας δούμε πώς λειτουργεί:

class Counter {
 constructor() {
 this.count = 5
 }
 copy() {
 const copy = new Counter()
 copy.count = this.count
 return copy
 }
}
const originalCounter = new Counter()
const copiedCounter = originalCounter.copy()
console.log(originalCounter.count) // 5
console.log(copiedCounter.count) // 5
copiedCounter.count = 7
console.log(originalCounter.count) // 5
console.log(copiedCounter.count) // 7

Για να αντιμετωπίσετε αντικείμενα και πίνακες που αναφέρονται μέσα στην περίπτωσή σας, θα πρέπει να εφαρμόσετε τις νέες γνώσεις σας σχετικά με τη βαθιά αντιγραφή ! Θα προσθέσω απλώς μια τελική λύση για τη copyμέθοδο προσαρμοσμένου κατασκευαστή για να την κάνω πιο δυναμική:

Με αυτήν τη μέθοδο αντιγραφής, μπορείτε να βάλετε όσες τιμές θέλετε στον κατασκευαστή σας, χωρίς να χρειάζεται να αντιγράψετε τα πάντα με μη αυτόματο τρόπο!

Σχετικά με τον συγγραφέα: Ο Lukas Gisder-Dubé συν-ίδρυσε και ηγήθηκε μιας εκκίνησης ως CTO για 1 1/2 χρόνια, οικοδομώντας την τεχνολογική ομάδα και την αρχιτεκτονική. Αφού αποχώρησε από την εκκίνηση, δίδαξε κωδικοποίηση ως Lead Instructor στο Ironhack και τώρα δημιουργεί ένα Startup Agency & Consultancy στο Βερολίνο. Ρίξτε μια ματιά στο dube.io για να μάθετε περισσότερα.