Μάθετε πώς οι Iterator Helpers της JavaScript βελτιώνουν δραστικά την απόδοση, εξαλείφοντας ενδιάμεσους πίνακες μέσω του stream fusion και της lazy evaluation.
Το Επόμενο Άλμα της JavaScript στην Απόδοση: Μια Εις Βάθος Ανάλυση του Stream Fusion με Iterator Helpers
Στον κόσμο της ανάπτυξης λογισμικού, η αναζήτηση για καλύτερη απόδοση είναι ένα συνεχές ταξίδι. Για τους προγραμματιστές JavaScript, ένα κοινό και κομψό μοτίβο για τον χειρισμό δεδομένων περιλαμβάνει την αλυσιδωτή χρήση μεθόδων πινάκων όπως .map(), .filter() και .reduce(). Αυτό το "fluent API" είναι ευανάγνωστο και εκφραστικό, αλλά κρύβει ένα σημαντικό εμπόδιο στην απόδοση: τη δημιουργία ενδιάμεσων πινάκων. Κάθε βήμα στην αλυσίδα δημιουργεί έναν νέο πίνακα, καταναλώνοντας μνήμη και κύκλους CPU. Για μεγάλα σύνολα δεδομένων, αυτό μπορεί να αποβεί καταστροφικό για την απόδοση.
Εδώ έρχεται η πρόταση TC39 Iterator Helpers, μια πρωτοποριακή προσθήκη στο πρότυπο ECMAScript που είναι έτοιμη να επαναπροσδιορίσει τον τρόπο με τον οποίο επεξεργαζόμαστε συλλογές δεδομένων στη JavaScript. Στην καρδιά της βρίσκεται μια ισχυρή τεχνική βελτιστοποίησης γνωστή ως stream fusion (ή operation fusion). Αυτό το άρθρο παρέχει μια ολοκληρωμένη εξερεύνηση αυτού του νέου παραδείγματος, εξηγώντας πώς λειτουργεί, γιατί έχει σημασία και πώς θα δώσει τη δυνατότητα στους προγραμματιστές να γράφουν πιο αποδοτικό, φιλικό προς τη μνήμη και ισχυρό κώδικα.
Το Πρόβλημα με την Παραδοσιακή Αλυσιδωτή Χρήση: Μια Ιστορία Ενδιάμεσων Πινάκων
Για να εκτιμήσουμε πλήρως την καινοτομία των iterator helpers, πρέπει πρώτα να κατανοήσουμε τους περιορισμούς της τρέχουσας προσέγγισης που βασίζεται στους πίνακες. Ας εξετάσουμε μια απλή, καθημερινή εργασία: από μια λίστα αριθμών, θέλουμε να βρούμε τους πρώτους πέντε άρτιους αριθμούς, να τους διπλασιάσουμε και να συλλέξουμε τα αποτελέσματα.
Η Συμβατική Προσέγγιση
Χρησιμοποιώντας τις τυπικές μεθόδους πινάκων, ο κώδικας είναι καθαρός και διαισθητικός:
const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, ...]; // Φανταστείτε έναν πολύ μεγάλο πίνακα
const result = numbers
.filter(n => n % 2 === 0) // Βήμα 1: Φιλτράρισμα για άρτιους αριθμούς
.map(n => n * 2) // Βήμα 2: Διπλασιασμός τους
.slice(0, 5); // Βήμα 3: Λήψη των πρώτων πέντε
Αυτός ο κώδικας είναι απόλυτα ευανάγνωστος, αλλά ας αναλύσουμε τι κάνει η μηχανή της JavaScript στο παρασκήνιο, ειδικά αν ο πίνακας numbers περιέχει εκατομμύρια στοιχεία.
- Επανάληψη 1 (
.filter()): Η μηχανή διατρέχει ολόκληρο τον πίνακαnumbers. Δημιουργεί έναν νέο ενδιάμεσο πίνακα στη μνήμη, ας τον ονομάσουμεevenNumbers, για να κρατήσει όλους τους αριθμούς που περνούν τον έλεγχο. Αν οnumbersέχει ένα εκατομμύριο στοιχεία, αυτός θα μπορούσε να είναι ένας πίνακας με περίπου 500.000 στοιχεία. - Επανάληψη 2 (
.map()): Η μηχανή τώρα διατρέχει ολόκληρο τον πίνακαevenNumbers. Δημιουργεί έναν δεύτερο ενδιάμεσο πίνακα, ας τον ονομάσουμεdoubledNumbers, για να αποθηκεύσει το αποτέλεσμα της λειτουργίας map. Αυτός είναι ένας ακόμη πίνακας 500.000 στοιχείων. - Επανάληψη 3 (
.slice()): Τέλος, η μηχανή δημιουργεί έναν τρίτο, τελικό πίνακα παίρνοντας τα πρώτα πέντε στοιχεία από τονdoubledNumbers.
Το Κρυφό Κόστος
Αυτή η διαδικασία αποκαλύπτει αρκετά κρίσιμα ζητήματα απόδοσης:
- Υψηλή Κατανομή Μνήμης: Δημιουργήσαμε δύο μεγάλους προσωρινούς πίνακες που απορρίφθηκαν αμέσως. Για πολύ μεγάλα σύνολα δεδομένων, αυτό μπορεί να οδηγήσει σε σημαντική πίεση στη μνήμη, προκαλώντας πιθανώς την επιβράδυνση ή ακόμη και την κατάρρευση της εφαρμογής.
- Επιβάρυνση του Garbage Collector: Όσο περισσότερα προσωρινά αντικείμενα δημιουργείτε, τόσο πιο σκληρά πρέπει να δουλέψει ο garbage collector για να τα καθαρίσει, εισάγοντας παύσεις και προβλήματα στην απόδοση.
- Σπατάλη Υπολογιστικής Ισχύος: Διατρέξαμε εκατομμύρια στοιχεία πολλές φορές. Ακόμη χειρότερα, ο τελικός μας στόχος ήταν να πάρουμε μόνο πέντε αποτελέσματα. Ωστόσο, οι μέθοδοι
.filter()και.map()επεξεργάστηκαν ολόκληρο το σύνολο δεδομένων, εκτελώντας εκατομμύρια περιττούς υπολογισμούς πριν η.slice()απορρίψει το μεγαλύτερο μέρος της δουλειάς.
Αυτό είναι το θεμελιώδες πρόβλημα που οι Iterator Helpers και το stream fusion έχουν σχεδιαστεί για να λύσουν.
Παρουσιάζοντας τους Iterator Helpers: Ένα Νέο Παράδειγμα για την Επεξεργασία Δεδομένων
Η πρόταση Iterator Helpers προσθέτει μια σουίτα γνωστών μεθόδων απευθείας στο Iterator.prototype. Αυτό σημαίνει ότι οποιοδήποτε αντικείμενο είναι iterator (συμπεριλαμβανομένων των generators και του αποτελέσματος μεθόδων όπως το Array.prototype.values()) αποκτά πρόσβαση σε αυτά τα ισχυρά νέα εργαλεία.
Μερικές από τις βασικές μεθόδους περιλαμβάνουν:
.map(mapperFn).filter(filterFn).take(limit).drop(limit).flatMap(mapperFn).reduce(reducerFn, initialValue).toArray().forEach(fn).some(fn).every(fn).find(fn)
Ας ξαναγράψουμε το προηγούμενο παράδειγμά μας χρησιμοποιώντας αυτούς τους νέους βοηθούς:
const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, ...];
const result = numbers.values() // 1. Λήψη ενός iterator από τον πίνακα
.filter(n => n % 2 === 0) // 2. Δημιουργία ενός filter iterator
.map(n => n * 2) // 3. Δημιουργία ενός map iterator
.take(5) // 4. Δημιουργία ενός take iterator
.toArray(); // 5. Εκτέλεση της αλυσίδας και συλλογή αποτελεσμάτων
Με μια πρώτη ματιά, ο κώδικας μοιάζει εντυπωσιακά παρόμοιος. Η βασική διαφορά είναι το σημείο εκκίνησης—numbers.values()—το οποίο επιστρέφει έναν iterator αντί για τον ίδιο τον πίνακα, και η τελική λειτουργία—.toArray()—η οποία καταναλώνει τον iterator για να παράγει το τελικό αποτέλεσμα. Η πραγματική μαγεία, ωστόσο, βρίσκεται στο τι συμβαίνει μεταξύ αυτών των δύο σημείων.
Αυτή η αλυσίδα δεν δημιουργεί κανέναν ενδιάμεσο πίνακα. Αντ' αυτού, κατασκευάζει έναν νέο, πιο σύνθετο iterator που περιτυλίγει τον προηγούμενο. Ο υπολογισμός αναβάλλεται. Τίποτα δεν συμβαίνει στην πραγματικότητα μέχρι να κληθεί μια τελική μέθοδος όπως το .toArray() ή το .reduce() για να καταναλώσει τις τιμές. Αυτή η αρχή ονομάζεται lazy evaluation.
Η Μαγεία του Stream Fusion: Επεξεργασία ενός Στοιχείου τη Φορά
Το stream fusion είναι ο μηχανισμός που κάνει τη lazy evaluation τόσο αποδοτική. Αντί να επεξεργάζεται ολόκληρη τη συλλογή σε ξεχωριστά στάδια, επεξεργάζεται κάθε στοιχείο ξεχωριστά μέσα από ολόκληρη την αλυσίδα των λειτουργιών.
Η Αναλογία της Γραμμής Συναρμολόγησης
Φανταστείτε ένα εργοστάσιο παραγωγής. Η παραδοσιακή μέθοδος με πίνακες μοιάζει με το να έχουμε ξεχωριστά δωμάτια για κάθε στάδιο:
- Δωμάτιο 1 (Φιλτράρισμα): Όλες οι πρώτες ύλες (ολόκληρος ο πίνακας) εισάγονται. Οι εργάτες φιλτράρουν τις κακές. Οι καλές τοποθετούνται όλες σε ένα μεγάλο δοχείο (ο πρώτος ενδιάμεσος πίνακας).
- Δωμάτιο 2 (Mapping): Ολόκληρο το δοχείο με τα καλά υλικά μεταφέρεται στο επόμενο δωμάτιο. Εδώ, οι εργάτες τροποποιούν κάθε αντικείμενο. Τα τροποποιημένα αντικείμενα τοποθετούνται σε ένα άλλο μεγάλο δοχείο (ο δεύτερος ενδιάμεσος πίνακας).
- Δωμάτιο 3 (Λήψη): Το δεύτερο δοχείο μεταφέρεται στο τελικό δωμάτιο, όπου ένας εργάτης απλώς παίρνει τα πρώτα πέντε αντικείμενα από την κορυφή και απορρίπτει τα υπόλοιπα.
Αυτή η διαδικασία είναι σπάταλη όσον αφορά τη μεταφορά (κατανομή μνήμης) και την εργασία (υπολογισμοί).
Το stream fusion, που τροφοδοτείται από τους iterator helpers, μοιάζει με μια σύγχρονη γραμμή συναρμολόγησης:
- Ένας ενιαίος ιμάντας μεταφοράς περνάει από όλους τους σταθμούς.
- Ένα αντικείμενο τοποθετείται στον ιμάντα. Μετακινείται στον σταθμό φιλτραρίσματος. Αν αποτύχει, αφαιρείται. Αν περάσει, συνεχίζει.
- Αμέσως μετακινείται στον σταθμό mapping, όπου τροποποιείται.
- Στη συνέχεια μετακινείται στον σταθμό καταμέτρησης (take). Ένας επόπτης το μετράει.
- Αυτό συνεχίζεται, ένα αντικείμενο τη φορά, μέχρι ο επόπτης να μετρήσει πέντε επιτυχημένα αντικείμενα. Σε εκείνο το σημείο, ο επόπτης φωνάζει "ΣΤΑΜΑΤΗΣΤΕ!" και ολόκληρη η γραμμή συναρμολόγησης κλείνει.
Σε αυτό το μοντέλο, δεν υπάρχουν μεγάλα δοχεία με ενδιάμεσα προϊόντα, και η γραμμή σταματά τη στιγμή που η δουλειά ολοκληρώνεται. Έτσι ακριβώς λειτουργεί το stream fusion με τους iterator helpers.
Ανάλυση Βήμα προς Βήμα
Ας παρακολουθήσουμε την εκτέλεση του παραδείγματός μας με τον iterator: numbers.values().filter(...).map(...).take(5).toArray().
.toArray()καλείται. Χρειάζεται μια τιμή. Ζητά από την πηγή του, τονtake(5)iterator, για το πρώτο του στοιχείο.- Ο
take(5)iterator χρειάζεται ένα στοιχείο για να μετρήσει. Ζητά από την πηγή του, τονmapiterator, για ένα στοιχείο. - Ο
mapiterator χρειάζεται ένα στοιχείο για να μετασχηματίσει. Ζητά από την πηγή του, τονfilteriterator, για ένα στοιχείο. - Ο
filteriterator χρειάζεται ένα στοιχείο για να ελέγξει. Τραβά την πρώτη τιμή από τον iterator του αρχικού πίνακα:1. - Το Ταξίδι του '1': Το φίλτρο ελέγχει
1 % 2 === 0. Αυτό είναι false. Ο filter iterator απορρίπτει το1και τραβά την επόμενη τιμή από την πηγή:2. - Το Ταξίδι του '2':
- Το φίλτρο ελέγχει
2 % 2 === 0. Αυτό είναι true. Περνά το2επάνω στονmapiterator. - Ο
mapiterator λαμβάνει το2, υπολογίζει το2 * 2, και περνά το αποτέλεσμα,4, επάνω στονtakeiterator. - Ο
takeiterator λαμβάνει το4. Μειώνει τον εσωτερικό του μετρητή (από 5 σε 4) και αποδίδει το4στονtoArray()καταναλωτή. Το πρώτο αποτέλεσμα βρέθηκε.
- Το φίλτρο ελέγχει
- Το
toArray()έχει μία τιμή. Ζητά από τοtake(5)την επόμενη. Η όλη διαδικασία επαναλαμβάνεται. - Το φίλτρο τραβά το
3(αποτυγχάνει), μετά το4(περνά). Το4αντιστοιχίζεται στο8, το οποίο λαμβάνεται. - Αυτό συνεχίζεται μέχρι το
take(5)να έχει αποδώσει πέντε τιμές. Η πέμπτη τιμή θα προέρχεται από τον αρχικό αριθμό10, ο οποίος αντιστοιχίζεται στο20. - Μόλις ο
take(5)iterator αποδώσει την πέμπτη του τιμή, ξέρει ότι η δουλειά του έχει τελειώσει. Την επόμενη φορά που θα του ζητηθεί μια τιμή, θα σηματοδοτήσει ότι έχει ολοκληρώσει. Ολόκληρη η αλυσίδα σταματά. Οι αριθμοί11,12, και τα εκατομμύρια άλλοι στον αρχικό πίνακα δεν εξετάζονται ποτέ.
Τα οφέλη είναι τεράστια: κανένας ενδιάμεσος πίνακας, ελάχιστη χρήση μνήμης, και οι υπολογισμοί σταματούν το συντομότερο δυνατό. Αυτή είναι μια μνημειώδης αλλαγή στην αποδοτικότητα.
Πρακτικές Εφαρμογές και Κέρδη στην Απόδοση
Η δύναμη των iterator helpers εκτείνεται πολύ πέρα από τον απλό χειρισμό πινάκων. Ανοίγει νέες δυνατότητες για την αποδοτική διαχείριση σύνθετων εργασιών επεξεργασίας δεδομένων.
Σενάριο 1: Επεξεργασία Μεγάλων Συνόλων Δεδομένων και Ροών (Streams)
Φανταστείτε ότι πρέπει να επεξεργαστείτε ένα αρχείο καταγραφής πολλών gigabyte ή μια ροή δεδομένων από ένα network socket. Η φόρτωση ολόκληρου του αρχείου σε έναν πίνακα στη μνήμη είναι συχνά αδύνατη.
Με τους iterators (και ειδικά τους async iterators, στους οποίους θα αναφερθούμε αργότερα), μπορείτε να επεξεργαστείτε τα δεδομένα κομμάτι-κομμάτι.
// Εννοιολογικό παράδειγμα με μια γεννήτρια που αποδίδει γραμμές από ένα μεγάλο αρχείο
function* readLines(filePath) {
// Υλοποίηση που διαβάζει ένα αρχείο γραμμή-γραμμή χωρίς να το φορτώνει όλο
// yield line;
}
const errorCount = readLines('huge_app.log').values()
.map(line => JSON.parse(line))
.filter(logEntry => logEntry.level === 'error')
.take(100) // Βρίσκει τα πρώτα 100 σφάλματα
.reduce((count) => count + 1, 0);
Σε αυτό το παράδειγμα, μόνο μία γραμμή του αρχείου βρίσκεται στη μνήμη κάθε φορά καθώς περνά μέσα από την αλυσίδα επεξεργασίας. Το πρόγραμμα μπορεί να επεξεργαστεί terabytes δεδομένων με ελάχιστο αποτύπωμα μνήμης.
Σενάριο 2: Πρόωρος Τερματισμός και Βραχυκύκλωμα (Short-Circuiting)
Το είδαμε ήδη αυτό με το .take(), αλλά ισχύει και για μεθόδους όπως το .find(), το .some(), και το .every(). Σκεφτείτε την εύρεση του πρώτου χρήστη σε μια μεγάλη βάση δεδομένων που είναι διαχειριστής.
Βασισμένο σε πίνακα (αναποτελεσματικό):
const firstAdmin = users.filter(u => u.isAdmin)[0];
Εδώ, το .filter() θα διατρέξει ολόκληρο τον πίνακα users, ακόμη και αν ο πρώτος κιόλας χρήστης είναι διαχειριστής.
Βασισμένο σε iterator (αποδοτικό):
const firstAdmin = users.values().find(u => u.isAdmin);
Ο βοηθός .find() θα ελέγξει κάθε χρήστη έναν προς έναν και θα σταματήσει ολόκληρη τη διαδικασία αμέσως μόλις βρει το πρώτο ταίριασμα.
Σενάριο 3: Εργασία με Άπειρες Ακολουθίες
Η lazy evaluation καθιστά δυνατή την εργασία με δυνητικά άπειρες πηγές δεδομένων, κάτι που είναι αδύνατο με τους πίνακες. Οι generators είναι ιδανικοί για τη δημιουργία τέτοιων ακολουθιών.
function* fibonacci() {
let a = 0, b = 1;
while (true) {
yield a;
[a, b] = [b, a + b];
}
}
// Βρίσκει τους πρώτους 10 αριθμούς Fibonacci μεγαλύτερους από 1000
const result = fibonacci()
.filter(n => n > 1000)
.take(10)
.toArray();
// το result θα είναι [1597, 2584, 4181, 6765, 10946, 17711, 28657, 46368, 75025, 121393]
Αυτός ο κώδικας εκτελείται τέλεια. Η γεννήτρια fibonacci() θα μπορούσε να τρέχει για πάντα, αλλά επειδή οι λειτουργίες είναι lazy και το .take(10) παρέχει μια συνθήκη τερματισμού, το πρόγραμμα υπολογίζει μόνο όσους αριθμούς Fibonacci είναι απαραίτητοι για να ικανοποιήσει το αίτημα.
Μια Ματιά στο Ευρύτερο Οικοσύστημα: Async Iterators
Η ομορφιά αυτής της πρότασης είναι ότι δεν ισχύει μόνο για σύγχρονους (synchronous) iterators. Ορίζει επίσης ένα παράλληλο σύνολο βοηθών για Async Iterators στο AsyncIterator.prototype. Αυτό αλλάζει τα δεδομένα για τη σύγχρονη JavaScript, όπου οι ασύγχρονες ροές δεδομένων είναι πανταχού παρούσες.
Φανταστείτε την επεξεργασία ενός σελιδοποιημένου API, την ανάγνωση μιας ροής αρχείου από το Node.js, ή τον χειρισμό δεδομένων από ένα WebSocket. Όλα αυτά αναπαρίστανται φυσικά ως ασύγχρονες ροές. Με τους async iterator helpers, μπορείτε να χρησιμοποιήσετε την ίδια δηλωτική σύνταξη .map() και .filter() σε αυτά.
// Εννοιολογικό παράδειγμα επεξεργασίας ενός σελιδοποιημένου API
async function* fetchAllUsers() {
let url = '/api/users?page=1';
while (url) {
const response = await fetch(url);
const data = await response.json();
for (const user of data.users) {
yield user;
}
url = data.nextPageUrl;
}
}
// Βρίσκει τους πρώτους 5 ενεργούς χρήστες από μια συγκεκριμένη χώρα
const activeUsers = await fetchAllUsers()
.filter(user => user.isActive)
.filter(user => user.country === 'DE')
.take(5)
.toArray();
Αυτό ενοποιεί το μοντέλο προγραμματισμού για την επεξεργασία δεδομένων στη JavaScript. Είτε τα δεδομένα σας βρίσκονται σε έναν απλό πίνακα στη μνήμη είτε σε μια ασύγχρονη ροή από έναν απομακρυσμένο διακομιστή, μπορείτε να χρησιμοποιήσετε τα ίδια ισχυρά, αποδοτικά και ευανάγνωστα μοτίβα.
Ξεκινώντας και Τρέχουσα Κατάσταση
Στις αρχές του 2024, η πρόταση Iterator Helpers βρίσκεται στο Στάδιο 3 της διαδικασίας TC39. Αυτό σημαίνει ότι ο σχεδιασμός έχει ολοκληρωθεί και η επιτροπή αναμένει να συμπεριληφθεί σε ένα μελλοντικό πρότυπο ECMAScript. Τώρα αναμένει την υλοποίηση στις κύριες μηχανές JavaScript και την ανατροφοδότηση από αυτές τις υλοποιήσεις.
Πώς να Χρησιμοποιήσετε τους Iterator Helpers Σήμερα
- Περιβάλλοντα Εκτέλεσης σε Browser και Node.js: Οι τελευταίες εκδόσεις των μεγάλων browsers (όπως Chrome/V8) και του Node.js αρχίζουν να υλοποιούν αυτά τα χαρακτηριστικά. Μπορεί να χρειαστεί να ενεργοποιήσετε μια συγκεκριμένη σημαία (flag) ή να χρησιμοποιήσετε μια πολύ πρόσφατη έκδοση για να έχετε πρόσβαση σε αυτά εγγενώς. Πάντα να ελέγχετε τους τελευταίους πίνακες συμβατότητας (π.χ., στο MDN ή στο caniuse.com).
- Polyfills: Για περιβάλλοντα παραγωγής που πρέπει να υποστηρίζουν παλαιότερα runtimes, μπορείτε να χρησιμοποιήσετε ένα polyfill. Ο πιο συνηθισμένος τρόπος είναι μέσω της βιβλιοθήκης
core-js, η οποία συχνά περιλαμβάνεται από transpilers όπως το Babel. Διαμορφώνοντας το Babel και τοcore-js, μπορείτε να γράψετε κώδικα χρησιμοποιώντας iterator helpers και να μετατραπεί σε ισοδύναμο κώδικα που λειτουργεί σε παλαιότερα περιβάλλοντα.
Συμπέρασμα: Το Μέλλον της Αποδοτικής Επεξεργασίας Δεδομένων στη JavaScript
Η πρόταση Iterator Helpers είναι κάτι περισσότερο από ένα απλό σύνολο νέων μεθόδων. Αντιπροσωπεύει μια θεμελιώδη στροφή προς πιο αποδοτική, επεκτάσιμη και εκφραστική επεξεργασία δεδομένων στη JavaScript. Αγκαλιάζοντας τη lazy evaluation και το stream fusion, λύνει τα μακροχρόνια προβλήματα απόδοσης που σχετίζονται με την αλυσιδωτή χρήση μεθόδων πινάκων σε μεγάλα σύνολα δεδομένων.
Τα βασικά σημεία που πρέπει να κρατήσει κάθε προγραμματιστής είναι:
- Απόδοση εξ Ορισμού: Η αλυσιδωτή χρήση μεθόδων iterator αποφεύγει τις ενδιάμεσες συλλογές, μειώνοντας δραστικά τη χρήση μνήμης και το φορτίο του garbage collector.
- Βελτιωμένος Έλεγχος με το Laziness: Οι υπολογισμοί εκτελούνται μόνο όταν χρειάζεται, επιτρέποντας τον πρόωρο τερματισμό και τον κομψό χειρισμό άπειρων πηγών δεδομένων.
- Ένα Ενοποιημένο Μοντέλο: Τα ίδια ισχυρά μοτίβα ισχύουν τόσο για σύγχρονα όσο και για ασύγχρονα δεδομένα, απλοποιώντας τον κώδικα και διευκολύνοντας την κατανόηση σύνθετων ροών δεδομένων.
Καθώς αυτό το χαρακτηριστικό γίνεται τυπικό μέρος της γλώσσας JavaScript, θα ξεκλειδώσει νέα επίπεδα απόδοσης και θα δώσει τη δυνατότητα στους προγραμματιστές να δημιουργούν πιο στιβαρές και επεκτάσιμες εφαρμογές. Είναι καιρός να αρχίσετε να σκέφτεστε σε ροές (streams) και να ετοιμαστείτε να γράψετε τον πιο αποδοτικό κώδικα επεξεργασίας δεδομένων της καριέρας σας.