JavaScript για προχωρημένους: Function Closures

Κώδικας, πληροφορίες, ερωτήσεις και απαντήσεις σχετικές με την JavaScript.

Συντονιστές: WebDev Moderators, Super-Moderators

Απάντηση
Άβαταρ μέλους
skeftomilos
Script Master
Δημοσιεύσεις: 2888
Εγγραφή: 07 Ιαν 2005 07:22
Τοποθεσία: Αθήνα

JavaScript για προχωρημένους: Function Closures

Δημοσίευση από skeftomilos » 24 Μάιος 2005 11:50

Η εύκολη εκκίνηση είναι ένα από τα θετικά σημεία μίας γλώσσας προγραμματισμού. Το να δίνει δηλαδή τη δυνατότητα σε έναν αρχάριο να φτιάχνει χρήσιμα προγράμματα, ακόμα κι όταν γνωρίζει τη γλώσσα σε στοιχειώδες επίπεδο. Αυτό το πλεονέκτημα ήταν εξαρχής βασική απαίτηση για μία γλώσσα ευρείας κατανάλωσης όπως η JavaScript, που προοριζόταν να γίνει αντικείμενο μεταχείρισης (και κακομεταχείρισης!) από πολυάριθμους ερασιτέχνες χρήστες του Internet. Πραγματικά αρκεί η γνώση ελάχιστων εντολών (όπως η alert και η if) για να μπορέσει κάποιος να κάνει σημαντικές βελτιώσεις στη σελίδα του. Υπό αυτές τις συνθήκες δε θα περίμενε κανείς από τη γλώσσα να έχει ιδιαίτερο βάθος, και εντυπωσιάζεται όταν ανακαλύπτει συνεχώς νέες δυνατότητες που ούτε καν τις είχε φανταστεί.

Και ενώ ήμουν βέβαιος ότι η γλώσσα μου είχε φανερώσει όλα τα μυστικά της, βρέθηκα ξαφνικά έκπληκτος μπροστά στις function closures. Ένα πολύ ισχυρό χαρακτηριστικό της γλώσσας, που κανένα από τα (πολλά!) βιβλία για JavaScript που έχω διαβάσει δεν το αναφέρει καν. Closure με απλά λόγια ονομάζεται μία function που θυμάται το "περιβάλλον" στο οποίο δηλώθηκε. Που αποθηκεύει δηλαδή τις τρέχουσες τιμές των εξωτερικών μεταβλητών την ώρα της δημιουργίας της, οι οποίες μπορεί να έχουν αλλάξει τιμές όταν έρθει η ώρα που η ρουτίνα θα τρέξει. Όμως ο καλύτερος τρόπος να γίνουν κατανοητά τα closures είναι με ένα παράδειγμα. Έστω ότι έχουμε μία σελίδα με δύο ή περισσότερα κουμπιά, τα οποία εμφανίζουν ένα μήνυμα όταν πατηθούν. Η πρώτη και πιο απλοϊκή εκδοχή της σελίδας μας είναι η παρακάτω:

Κώδικας: Επιλογή όλων

<html>
  <body>
    <button onclick="alert&#40;'Microsoft'&#41;">Company 1</button>
    <button onclick="alert&#40;'IBM'&#41;">Company 2</button>
  </body>
</html>
Εικόνα

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

Κώδικας: Επιλογή όλων

<html>
  <head>
    <script type="text/javascript">
      function click1&#40;&#41; &#123;
        alert&#40;'Microsoft'&#41;
      &#125;
      function click2&#40;&#41; &#123;
        alert&#40;'IBM'&#41;
      &#125;
    </script>
  </head>
  <body>
    <button onclick="click1&#40;&#41;">Company 1</button>
    <button onclick="click2&#40;&#41;">Company 2</button>
  </body>
</html>
Φαινομενικά καταφέραμε να πετύχουμε το διαχωρισμό, αλλά ουσιαστικά δεν έχει γίνει καμία πρόοδος. Η παραπάνω μέθοδος επιτρέπει την αποσυμφόρηση των HTML attributes και είναι πραγματικά χρήσιμη όταν ο κώδικας είναι πολύς, αλλά εξακολουθούν να υπάρχουν τόσα θραύσματα διασκορπισμένου κώδικα όσα και πριν. Π.χ. το string "click1()" δεν παύει να είναι κώδικας JavaScript, ακόμα κι αν είναι ελάχιστος σε μέγεθος. Για να απαλλάξουμε πραγματικά την HTML από εμβόλιμο κώδικα JavaScript, πρέπει να χρησιμοποιήσουμε άλλη μέθοδο. Θα ορίσουμε δυναμικά τις ρουτίνες των συμβάντων κλικ των κουμπιών, και ο πιο κατάλληλος χρόνος για να γίνει αυτό είναι το συμβάν onload της σελίδας:

Κώδικας: Επιλογή όλων

<html>
  <head>
    <script type="text/javascript">
      window.onload = function&#40;&#41; &#123;
        document.getElementById&#40;"a1"&#41;.onclick = function&#40;&#41; &#123;
          alert&#40;'Microsoft'&#41;
        &#125;
        document.getElementById&#40;"a2"&#41;.onclick = function&#40;&#41; &#123;
          alert&#40;'IBM'&#41;
        &#125;
      &#125;
    </script>
  </head>
  <body>
    <button id="a1">Company 1</button>
    <button id="a2">Company 2</button>
  </body>
</html>
Τώρα ο διαχωρισμός είναι αληθινός. Ολόκληρη η JavaScript βρίσκεται ανάμεσα στα tags script, και η μεταφορά της σε εξωτερικό αρχείο είναι υπόθεση ρουτίνας. Η τεχνική αυτή έχει το εγγενές μειονέκτημα ότι πρέπει να ολοκληρωθεί το φόρτωμα της σελίδας για να αρχίσουν να λειτουργούν τα κουμπιά. Θα αγνοήσουμε εδώ αυτό το πρόβλημα, εκτιμώντας ότι τα οφέλη που αποκομίζουμε από το διαχωρισμό είναι πολύ σημαντικότερα. Παρατηρούμε ότι χρειάστηκε να προσθέσουμε από ένα id σε κάθε κουμπί, κάτι που αφ' ενός δεν είναι εντελώς απαραίτητο, αλλά είναι ενδεχομένως χρήσιμο και για τη μορφοποίηση με CSS. Ο κώδικας JavaScript είναι πιο περίπλοκος, κυρίως γιατί κάνουμε χειροκίνητα αυτό που στην προηγούμενη περίπτωση έγινε αυτόματα για χάρη μας, δηλαδή τη δημιουργία των ρουτινών που χειρίζονται τα συμβάντα onclick των κουμπιών. Αν ρίξετε μια επιπόλαιη ματιά στον κώδικα μπορεί να νομίσετε ότι η εντολή π.χ. alert('Microsoft') θα εκτελεστεί με το φόρτωμα του παραθύρου, όμως αυτό δε συμβαίνει. Αυτό που συμβαίνει με το φόρτωμα είναι η δημιουργία των functions που θα εκτελεστούν όταν και αν γίνει κλικ σε κάποιο κουμπί, πολύ αργότερα δηλαδή. Αυτό είναι ένα δύσκολο σημείο, γιατί ενδεχομένως δεν έχετε συνηθίσει την ιδέα να αντιμετωπίζετε μία function ως τιμή μίας μεταβλητής. Ένα παράδειγμα μπορεί να ρίξει περισσότερο φως:

Κώδικας: Επιλογή όλων

<html>
  <head>
    <script type="text/javascript">
      alert&#40;window.onload&#41;
      window.onload = function&#40;&#41; &#123;
        alert&#40;"Page has just loaded!"&#41;
      &#125;
      alert&#40;window.onload&#41;
    </script>
  </head>
</html>
Ο κώδικας εμφανίζει την τιμή της ιδιότητας window.onload πριν και αφού της αποδώσουμε μία τιμή τύπου function. Αν τρέξουμε το παράδειγμα θα εμφανιστούν τα παρακάτω popups (με αυτή τη σειρά):

Εικόνα

Εικόνα

Εικόνα

Το τελευταίο προκλήθηκε από το ίδιο το φόρτωμα της σελίδας, αρκετά αργότερα δηλαδή από τη στιγμή που έτρεξε ο κώδικας στο τμήμα head. Η ιδιότητα onload του αντικειμένου window επιφανειακά δε διαφέρει από άλλες ιδιότητες όπως location και history, όμως ο browser την καλεί αυτόματα μόλις φορτωθεί η σελίδα και επομένως πρέπει να περιέχει μία function. Για παράδειγμα θα ήταν νόμιμο αλλά παράλογο να γράφαμε:

Κώδικας: Επιλογή όλων

window.onload = 100
ή

Κώδικας: Επιλογή όλων

window.onload = "Microsoft"
Οι παραπάνω εντολές δεν έχουν κανένα θετικό ή αρνητικό αποτέλεσμα. Μόλις φορτωθεί η σελίδα δε θα γίνει τίποτα απολύτως. Είναι βέβαια αποτελεσματικές αν θέλουμε να ακυρώσουμε ένα συμβάν, αλλά σε αυτή την περίπτωση είναι λογικότερο να γράψουμε:

Κώδικας: Επιλογή όλων

window.onload = undefined
ή

Κώδικας: Επιλογή όλων

window.onload = null
Έχουμε πει τόσα πράγματα κι ακόμα ούτε λέξη για closures. Θα χρησιμοποιήσουμε closures τώρα αμέσως, γιατί θέλουμε να βελτιώσουμε περαιτέρω τον κώδικά μας. Το πρόβλημα είναι ότι επαναλαμβάνεται σε μεγάλο βαθμό το ίδιο μοτίβο. Όλα τα συμβάντα κλικ προκαλούν την εμφάνιση ενός μηνύματος popup, και το μόνο που διαφέρει είναι το περιεχόμενο του μηνύματος. Αναζητώντας τρόπους να απλοποιήσουμε την κατάσταση συλλαμβάνουμε αίφνης την εξής διαστροφική ιδέα: Θα φτιάξουμε μία ρουτίνα που θα επιστρέφει ρουτίνες! Αυτό που θέλουμε είναι μία κάπως διαφορετική ρουτίνα για το χειρισμό του κλικ του κάθε κουμπιού. Θα δημιουργήσουμε λοιπόν ένα μίνι εργοστάσιο παραγωγής ρουτινών:

Κώδικας: Επιλογή όλων

<html>
  <head>
    <script type="text/javascript">
      window.onload = function&#40;&#41; &#123;
        function alert_maker&#40;text&#41; &#123;
          return function&#40;&#41; &#123;
            alert&#40;text&#41;
          &#125;
        &#125;
        document.getElementById&#40;"a1"&#41;.onclick = alert_maker&#40;'Microsoft'&#41;
        document.getElementById&#40;"a2"&#41;.onclick = alert_maker&#40;'IBM'&#41;
      &#125;
    </script>
  </head>
  <body>
    <button id="a1">Company 1</button>
    <button id="a2">Company 2</button>
  </body>
</html>
Η σατανικής εμπνεύσεως ρουτίνα μας είναι η alert_maker(). Δέχεται ως όρισμα ένα string, και επιστρέφει μία συνάρτηση που όταν κληθεί θα εμφανίσει ένα μήνυμα με αυτό το string. Παρατηρήστε πόσο απλοποιήθηκε οπτικά ο κώδικάς μας. Φανταστείτε ότι είχαμε όχι δύο αλλά είκοσι buttons, και το κάθε συμβάν είχε δέκα γραμμές κώδικα. Τώρα για το αν ο κώδικας απλοποιήθηκε και σημειολογικά, αυτό είναι άλλο ζήτημα. Προσωπικά ακόμα με ξενίζει η ιδέα να μεταφέρω συναρτήσεις από δω κι από κει σα να είναι απλά νουμεράκια, αλλά φαντάζομαι ότι είναι θέμα συνήθειας. Υποθέτω ότι οι προγραμματιστές με υπόβαθρο σε functional γλώσσες όπως η Lisp, η Scheme και η Haskell δεν θα έχουν το παραμικρό πρόβλημα με το όλο concept.

Αλλά ας ρίξουμε μία προσεχτικότερη ματιά στον κώδικα. Βλέπετε τίποτα περίεργο? Για σκεφτείτε τι θα συμβεί αν ο χρήστης κάνει κλικ σε ένα κουμπί. Ο browser θα καλέσει αυτόματα την ιδιότητα onclick του κουμπιού, αλλά τι υπάρχει εκεί? Υπάρχει μία ρουτίνα χωρίς όνομα, που το περιεχόμενό της είναι το παρακάτω:

Κώδικας: Επιλογή όλων

alert&#40;text&#41;
Χμ, δεν είναι δυνατό, κάποιο λάθος πρέπει να συμβαίνει. Πώς γνωρίζει η ρουτίνα την τιμή της μεταβλητής text? Η ρουτίνα alert_maker() κλήθηκε διαδοχικά δύο φορές, και τη δεύτερη η μεταβλητή είχε τιμή 'IBM'. Άρα και τα δύο κουμπιά πρέπει να εμφανίζουν τη λέξη 'IBM', ή ίσως και τίποτα. Για να το δοκιμάσουμε ... ας πατήσουμε το πρώτο κουμπί:

Εικόνα

Χμ, χμ, χμ (ξύσιμο του κεφαλιού). Τα κουμπιά δουλεύουν όπως θα θέλαμε να δουλεύουν, αλλά ... τι μπέρδεμα Θεέ μου! Λοιπόν, θα σας λύσω την απορία γιατί δε θέλω να εκραγεί το κεφάλι κανενός και βρω και το μπελά μου. Η ρουτίνες που επιστρέφει η alert_maker() είναι closures! Το οποίο σημαίνει ότι θυμούνται το περιβάλλον τους όταν δημιουργήθηκαν, και το περιβάλλον τους στην προκειμένη περίπτωση είναι η μεταβλητή text. Για την πρώτη ρουτίνα η μεταβλητή έχει τιμή 'Microsoft' και για τη δεύτερη 'IBM'. Το περιβάλλον των ρουτινών θα μπορούσε να είχε περισσότερες μεταβλητές προς διατήρηση:

Κώδικας: Επιλογή όλων

function alert_maker&#40;text, tag&#41; &#123;
  var tag_left = "<" + tag + ">"
  var tag_right = "</" + tag + ">"
  return function&#40;&#41; &#123;
    alert&#40;tag_left + text + tag_right&#41;
  &#125;
&#125;
document.getElementById&#40;"a1"&#41;.onclick = alert_maker&#40;'Microsoft', 'b'&#41;
document.getElementById&#40;"a2"&#41;.onclick = alert_maker&#40;'IBM', 'i'&#41;
Τώρα το πρώτο κουμπί εμφανίζει <b>Microsoft</b> και το δεύτερο <i>IBM</i>. Κάθε ρουτίνα θυμάται τις τιμές και των τεσσάρων μεταβλητών text, tag, tag_left, tag_right, παρόλο που χρησιμοποιεί μόνο τις τρεις.

Τελειώνοντας να πω ότι οι closures είναι χαρακτηριστικό της ECMA τυποποίησης και υποστηρίζεται από όλους τους σύγχρονους browsers. Ήταν πάντα μαζί μας, μόνο που εμείς δεν ξέραμε ότι υπάρχουν. Θαμμένες για χρόνια στα βάθη του υπολογιστή μας, καθώς και στις περιπεπλεγμένες και εν πολλοίς ακατανόητες αράδες της επίσημης ECMA τυποποίησης, που εκτός από τους προγραμματιστές ειδικού λογισμικού δε διαβάζει κανείς. Μήπως ήρθε η ώρα να αρχίσουν δειλά-δειλά να μπαίνουν στις σελίδες μας?

Σχετικά κείμενα:
- Closures and executing JavaScript on page load
- Private Members in JavaScript
- The World's Most Misunderstood Programming Language
- Separating behavior and structure
Τελευταία επεξεργασία από το μέλος skeftomilos την 27 Μάιος 2005 04:42, έχει επεξεργασθεί 1 φορά συνολικά.
The pure and simple truth is rarely pure and never simple. Ο μη νους δε σκέπτεται μη σκέψεις για το τίποτα.

Cmg__
Δημοσιεύσεις: 1710
Εγγραφή: 29 Μαρ 2005 22:40

JavaScript για προχωρημένους: Function Closures

Δημοσίευση από Cmg__ » 26 Μάιος 2005 15:31

:clap: :clap: :clap: :clap: :clap: :clap: :clap: :clap: :clap: :clap:

Μπράβο και πάλι μπράβο Θοδωρή. Παρα πολυ καλο το άρθρο σου , πολυ χρήσιμο, με βοηθητικές εικόνες (skeftomilos style), και κατατοπιστικότατα παραδείγματα!



<Ξενέρωτη ερώτηση>
document.getElementById(" ")
Το παραπάνω τι ακριβώς κάνει???
(φαντάζομαι βέβαια αλλα ας πάρω μία πιο επαγγελματική άποψη...
</Ξενέρωτη Ερώτηση>

Άβαταρ μέλους
skeftomilos
Script Master
Δημοσιεύσεις: 2888
Εγγραφή: 07 Ιαν 2005 07:22
Τοποθεσία: Αθήνα

JavaScript για προχωρημένους: Function Closures

Δημοσίευση από skeftomilos » 27 Μάιος 2005 04:46

Η document.getElementById() επιστρέφει μία αναφορά στο element της σελίδας με το συγκεκριμένο Id. Είναι εξαιρετικά χρήσιμη μέθοδος γιατί μας δίνει πρόσβαση στις ιδιότητες και μεθόδους όλων των στοιχείων της σελίδας που έχουν Id. Και Id μπορούμε να δώσουμε σε κάθε είδους element, για παράδειγμα p, a, table, tr, td, form, button, input, textarea, img και οτιδήποτε άλλο. Αν την καλέσουμε αλλά δεν υπάρχει στοιχείο με τέτοιο Id, θα επιστρέψει undefined. Μερικές φορές δεν είμαστε απόλυτα βέβαιοι ότι υπάρχει το στοιχείο που ψάχνουμε, και τότε πρέπει να προγραμματίζουμε αμυντικά. Έστω π.χ. ότι θέλουμε να κάνουμε κόκκινο το κείμενο της παρακάτω παραγράφου:

Κώδικας: Επιλογή όλων

<p id="signature">Τα ρακούν ζούνε μέχρι 15 χρόνια.</p>
Ο αμυντικός τρόπος να το κάνουμε είναι:

Κώδικας: Επιλογή όλων

if &#40;document.getElementById&#40;"signature"&#41;&#41; &#123;
  document.getElementById&#40;"signature"&#41;.style.color = "red"
&#125;
Περιπτώσεις που πραγματικά δε μπορούμε να είμαστε βέβαιοι για την ύπαρξη ενός στοιχείου είναι όταν έχουμε να κάνουμε με δυναμικές σελίδες (ASP, PHP κ.λπ.), ή όταν γράφουμε scripts που τρέχουν σε ξένες σελίδες (π.χ. Greasemonnkey scripts).

Μία άλλη παρεμφερής και εξίσου χρήσιμη μέθοδος είναι η document.getElementsByTagName(). Αυτή επιστρέφει μία συλλογή με αναφορές όλων των στοιχείων με συγκεκριμένο tag name. Π.χ. η παρακάτω επιστρέφει όλες τις παραγράφους της σελίδας:

Κώδικας: Επιλογή όλων

document.getElementsByTagName&#40;"p"&#41;
Αν θέλουμε μόνο τις παραγράφους συγκεκριμένης κλάσης, τότε θα πρέπει να ψάξουμε ένα-ένα τα στοιχεία της συλλογής για να δούμε αν έχουν attribute class με την τιμή που θέλουμε:

Κώδικας: Επιλογή όλων

var list = document.getElementsByTagName&#40;"p"&#41;
for &#40;var i = 0; i < list.length; i++&#41; &#123;
  if&#40;list&#91;i&#93;.getAttribute&#40;"class"&#41; == "user"&#41; &#123;
    // κάνουμε κάτι με το στοιχείο list&#91;i&#93; που είναι σίγουρα παράγραφος κλάσης user.
  &#125;
&#125;
Αν θέλουμε με τη μία όλα μα όλα τα στοιχεία της σελίδας, μας τα δίνει η παρακάτω έκφραση:

Κώδικας: Επιλογή όλων

document.getElementsByTagName&#40;"*"&#41;
... η οποία είναι πρακτικά ισοδύναμη με την IE specific ιδιότητα document.all
The pure and simple truth is rarely pure and never simple. Ο μη νους δε σκέπτεται μη σκέψεις για το τίποτα.

Cmg__
Δημοσιεύσεις: 1710
Εγγραφή: 29 Μαρ 2005 22:40

JavaScript για προχωρημένους: Function Closures

Δημοσίευση από Cmg__ » 28 Μάιος 2005 00:19

/me koitaei xamhla kai ena dakry peftei apo ta matia toy giati den yphrxe periptwsh kapoios na toy dwsei pio oloklhrwmenh apanthsh (h analysh) kai na ton kalypsei overallika!!!

klap...klap...klap!

Απάντηση

Επιστροφή στο “JavaScript και Frameworks”

Μέλη σε σύνδεση

Μέλη σε αυτήν τη Δ. Συζήτηση: Δεν υπάρχουν εγγεγραμμένα μέλη και 2 επισκέπτες