עברית

מדריך מקיף להבנה ומימוש של פרוטוקול האיטרטור ב-JavaScript, המאפשר לכם ליצור איטרטורים מותאמים אישית לטיפול משופר בנתונים.

פישוט פרוטוקול האיטרטור של JavaScript ואיטרטורים מותאמים אישית

פרוטוקול האיטרטור של JavaScript מספק דרך סטנדרטית לעבור על מבני נתונים. הבנת פרוטוקול זה מאפשרת למפתחים לעבוד ביעילות עם איטרבילים (iterables) מובנים כמו מערכים ומחרוזות, וליצור איטרבילים מותאמים אישית משלהם המותאמים למבני נתונים ודרישות יישום ספציפיות. מדריך זה מספק בחינה מקיפה של פרוטוקול האיטרטור וכיצד לממש איטרטורים מותאמים אישית.

מהו פרוטוקול האיטרטור?

פרוטוקול האיטרטור מגדיר כיצד ניתן לעבור על אובייקט, כלומר, כיצד ניתן לגשת לאלמנטים שלו באופן סדרתי. הוא מורכב משני חלקים: פרוטוקול Iterable ופרוטוקול Iterator.

פרוטוקול Iterable

אובייקט נחשב Iterable (ניתן לאיטרציה) אם יש לו מתודה עם המפתח Symbol.iterator. מתודה זו חייבת להחזיר אובייקט התואם לפרוטוקול Iterator.

במהותו, אובייקט איטרבילי יודע כיצד ליצור איטרטור עבור עצמו.

פרוטוקול Iterator

פרוטוקול ה-Iterator מגדיר כיצד לשלוף ערכים מרצף. אובייקט נחשב לאיטרטור אם יש לו מתודת next() שמחזירה אובייקט עם שתי תכונות:

מתודת next() היא סוס העבודה של פרוטוקול האיטרטור. כל קריאה ל-next() מקדמת את האיטרטור ומחזירה את הערך הבא ברצף. כאשר כל הערכים הוחזרו, next() מחזירה אובייקט עם done שמוגדר ל-true.

איטרבילים מובנים

JavaScript מספקת מספר מבני נתונים מובנים שהם איטרביליים מטבעם. אלה כוללים:

ניתן להשתמש באיטרבילים אלה ישירות עם לולאת for...of, תחביר הפיזור (...), ומבנים אחרים המסתמכים על פרוטוקול האיטרטור.

דוגמה עם מערכים:


const myArray = ["apple", "banana", "cherry"];

for (const item of myArray) {
  console.log(item); // Output: apple, banana, cherry
}

דוגמה עם מחרוזות:


const myString = "Hello";

for (const char of myString) {
  console.log(char); // Output: H, e, l, l, o
}

לולאת ה-for...of

לולאת ה-for...of היא מבנה רב עוצמה לאיטרציה על אובייקטים איטרביליים. היא מטפלת אוטומטית במורכבויות של פרוטוקול האיטרטור, מה שמקל על הגישה לערכים ברצף.

התחביר של לולאת for...of הוא:


for (const element of iterable) {
  // Code to be executed for each element
}

לולאת ה-for...of שולפת את האיטרטור מהאובייקט האיטרבילי (באמצעות Symbol.iterator), וקוראת שוב ושוב למתודת next() של האיטרטור עד ש-done הופך ל-true. בכל איטרציה, המשתנה element מקבל את ערך התכונה value המוחזר על ידי next().

יצירת איטרטורים מותאמים אישית

בעוד ש-JavaScript מספקת איטרבילים מובנים, הכוח האמיתי של פרוטוקול האיטרטור טמון ביכולת להגדיר איטרטורים מותאמים אישית עבור מבני נתונים משלכם. זה מאפשר לכם לשלוט כיצד הנתונים שלכם נסרקים ונגישים.

כך יוצרים איטרטור מותאם אישית:

  1. הגדירו מחלקה או אובייקט המייצגים את מבנה הנתונים המותאם אישית שלכם.
  2. ממשו את מתודת Symbol.iterator במחלקה או באובייקט שלכם. מתודה זו צריכה להחזיר אובייקט איטרטור.
  3. אובייקט האיטרטור חייב לכלול מתודת next() שמחזירה אובייקט עם התכונות value ו-done.

דוגמה: יצירת איטרטור לטווח פשוט

בואו ניצור מחלקה בשם Range המייצגת טווח של מספרים. נממש את פרוטוקול האיטרטור כדי לאפשר איטרציה על המספרים בטווח.


class Range {
  constructor(start, end) {
    this.start = start;
    this.end = end;
  }

  [Symbol.iterator]() {
    let currentValue = this.start;
    const that = this; // Capture 'this' for use inside the iterator object

    return {
      next() {
        if (currentValue <= that.end) {
          return {
            value: currentValue++,
            done: false,
          };
        } else {
          return {
            value: undefined,
            done: true,
          };
        }
      },
    };
  }
}

const myRange = new Range(1, 5);

for (const number of myRange) {
  console.log(number); // Output: 1, 2, 3, 4, 5
}

הסבר:

דוגמה: יצירת איטרטור עבור רשימה מקושרת

בואו נבחן דוגמה נוספת: יצירת איטרטור עבור מבנה נתונים של רשימה מקושרת. רשימה מקושרת היא רצף של צמתים (nodes), כאשר כל צומת מכיל ערך והפניה (מצביע) לצומת הבא ברשימה. לצומת האחרון ברשימה יש הפניה ל-null (או undefined).


class LinkedListNode {
    constructor(value, next = null) {
        this.value = value;
        this.next = next;
    }
}

class LinkedList {
    constructor() {
        this.head = null;
    }

    append(value) {
        const newNode = new LinkedListNode(value);
        if (!this.head) {
            this.head = newNode;
            return;
        }

        let current = this.head;
        while (current.next) {
            current = current.next;
        }
        current.next = newNode;
    }

    [Symbol.iterator]() {
        let current = this.head;

        return {
            next() {
                if (current) {
                    const value = current.value;
                    current = current.next;
                    return {
                        value: value,
                        done: false
                    };
                } else {
                    return {
                        value: undefined,
                        done: true
                    };
                }
            }
        };
    }
}

// Example Usage:
const myList = new LinkedList();
myList.append("London");
myList.append("Paris");
myList.append("Tokyo");

for (const city of myList) {
    console.log(city); // Output: London, Paris, Tokyo
}

הסבר:

פונקציות גנרטור

פונקציות גנרטור מספקות דרך תמציתית ואלגנטית יותר ליצור איטרטורים. הן משתמשות במילת המפתח yield כדי להפיק ערכים לפי דרישה.

פונקציית גנרטור מוגדרת באמצעות התחביר function*.

דוגמה: יצירת איטרטור באמצעות פונקציית גנרטור

בואו נשכתב את האיטרטור Range באמצעות פונקציית גנרטור:


class Range {
  constructor(start, end) {
    this.start = start;
    this.end = end;
  }

  *[Symbol.iterator]() {
    for (let i = this.start; i <= this.end; i++) {
      yield i;
    }
  }
}

const myRange = new Range(1, 5);

for (const number of myRange) {
  console.log(number); // Output: 1, 2, 3, 4, 5
}

הסבר:

פונקציות גנרטור מפשטות את יצירת האיטרטורים על ידי טיפול אוטומטי במתודת next() ובדגל done.

דוגמה: גנרטור סדרת פיבונאצ'י

דוגמה נהדרת נוספת לשימוש בפונקציות גנרטור היא יצירת סדרת פיבונאצ'י:


function* fibonacciSequence() {
  let a = 0;
  let b = 1;

  while (true) {
    yield a;
    [a, b] = [b, a + b]; // Destructuring assignment for simultaneous update
  }
}

const fibonacci = fibonacciSequence();

for (let i = 0; i < 10; i++) {
  console.log(fibonacci.next().value); // Output: 0, 1, 1, 2, 3, 5, 8, 13, 21, 34
}

הסבר:

יתרונות השימוש בפרוטוקול האיטרטור

טכניקות איטרטור מתקדמות

שילוב איטרטורים

ניתן לשלב מספר איטרטורים לאיטרטור יחיד. זה שימושי כאשר אתם צריכים לעבד נתונים ממספר מקורות באופן מאוחד.


function* combineIterators(...iterables) {
  for (const iterable of iterables) {
    for (const item of iterable) {
      yield item;
    }
  }
}

const array1 = [1, 2, 3];
const array2 = ["a", "b", "c"];
const string1 = "XYZ";

const combined = combineIterators(array1, array2, string1);

for (const value of combined) {
  console.log(value); // Output: 1, 2, 3, a, b, c, X, Y, Z
}

בדוגמה זו, הפונקציה `combineIterators` מקבלת כל מספר של איטרבילים כארגומנטים. היא עוברת על כל איטרביל ומפיקה (yields) כל פריט. התוצאה היא איטרטור יחיד המפיק את כל הערכים מכל האיטרבילים שהוזנו.

סינון והתמרת איטרטורים

ניתן גם ליצור איטרטורים המסננים או מתמירים את הערכים המופקים על ידי איטרטור אחר. זה מאפשר לכם לעבד נתונים בצינור (pipeline), תוך החלת פעולות שונות על כל ערך בזמן שהוא נוצר.


function* filterIterator(iterable, predicate) {
  for (const item of iterable) {
    if (predicate(item)) {
      yield item;
    }
  }
}

function* mapIterator(iterable, transform) {
  for (const item of iterable) {
    yield transform(item);
    }
}

const numbers = [1, 2, 3, 4, 5, 6];

const evenNumbers = filterIterator(numbers, (x) => x % 2 === 0);
const squaredEvenNumbers = mapIterator(evenNumbers, (x) => x * x);

for (const value of squaredEvenNumbers) {
    console.log(value); // Output: 4, 16, 36
}

כאן, `filterIterator` מקבל איטרביל ופונקציית פרדיקט (predicate). הוא מפיק רק את הפריטים שעבורם הפרדיקט מחזיר `true`. ה-`mapIterator` מקבל איטרביל ופונקציית התמרה. הוא מפיק את תוצאת החלת פונקציית ההתמרה על כל פריט.

יישומים בעולם האמיתי

פרוטוקול האיטרטור נמצא בשימוש נרחב בספריות ופריימוורקים של JavaScript, והוא בעל ערך במגוון יישומים בעולם האמיתי, במיוחד כאשר מתמודדים עם מערכי נתונים גדולים או פעולות אסינכרוניות.

שיטות עבודה מומלצות

סיכום

פרוטוקול האיטרטור של JavaScript מספק דרך עוצמתית וגמישה לעבור על מבני נתונים. על ידי הבנת פרוטוקולי ה-Iterable וה-Iterator, ועל ידי מינוף פונקציות גנרטור, תוכלו ליצור איטרטורים מותאמים אישית לצרכים הספציפיים שלכם. זה מאפשר לכם לעבוד ביעילות עם נתונים, לשפר את קריאות הקוד ולשפר את ביצועי היישומים שלכם. שליטה באיטרטורים פותחת הבנה עמוקה יותר של יכולות JavaScript ומעצימה אתכם לכתוב קוד אלגנטי ויעיל יותר.