Menu

Iterators ו-Generators ב-JavaScript: function*, yield, Symbol.iterator

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

בדף הזה יש עורכים שאפשר להריץ - לערוך, להריץ ולראות את הפלט מיד.

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

הרבה יכולות של JavaScript, כמו for...of, spread (...), destructuring, Array.from ו-Promise.all, חולקות מנגנון אחד שעומד מתחתן: פרוטוקול האיטרטור (iterator protocol). ברגע שמבינים אותו, כולן נראות כמו וריאציות של אותו רעיון.

איטרטור הוא כל אובייקט עם מתודת next() שמחזירה { value, done }:

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

Iterable מול Iterator

יש מושג שני, קשור. Iterable הוא כל דבר שיודע לייצר איטרטור. הוא עושה את זה דרך מתודה ששמורה תחת מפתח מיוחד: Symbol.iterator.

מערכים הם iterables. קריאה ל-numbers[Symbol.iterator]() מחזירה איטרטור חדש. גם מחרוזות, Map, Set ו-arguments הם iterables, ולכן for...of עובדת על כולם.

ההפרדה חשובה: ה_iterable_ הוא האוסף, וה_iterator_ הוא הסמן. אפשר לבקש מ-iterable כמה סמנים עצמאיים שרוצים.

למה for...of עובדת

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

Spread ו-destructuring עושים אותו דבר: הם עוברים על איטרטור עד שהוא מסתיים:

כל אובייקט שתבנו שמממש את Symbol.iterator מקבל בחינם גישה לכל היכולות האלה.

כתיבת Iterable מותאם אישית

בואו ניצור אובייקט range שמפיק מספרים מ-start עד end:

כמה דברים ששווה לשים לב אליהם:

  • [Symbol.iterator]() משתמשת ב_שם מתודה מחושב_. המפתח הוא הסמל עצמו, לא המחרוזת "Symbol.iterator".
  • כל קריאה ל-[Symbol.iterator]() מחזירה איטרטור חדש לגמרי עם current משלו. זה מה שמאפשר לעבור על range פעמיים בלי שהוא "ייגמר".
  • האיטרטור שמוחזר צריך רק next(). זהו.

זה עובד, אבל זה מסורבל. יש דרך הרבה יותר טובה.

הכירו את ה-Generators

פונקציית generator מוצהרת עם function* (שימו לב לכוכבית). במקום לרוץ עד הסוף, היא יכולה לעצור בביטוי yield ולהמשיך אחר כך. קריאה לה לא מריצה את הגוף: היא מחזירה אובייקט generator שהוא גם iterator וגם iterable:

כל next() מריצה את הגוף עד שהיא פוגשת yield, עוצרת ומחזירה { value, done: false }. כשהפונקציה מסתיימת, מקבלים { value: undefined, done: true }.

ומכיוון שגנרטורים הם iterables, הם עובדים עם כל מה שהופיע בחלק הקודם:

כתיבה מחדש של range עם Generator

השוו את הגרסה המסורבלת שלמעלה לזו:

זהו. ה-* לפני [Symbol.iterator] הופכת אותה למתודת generator. yield i מחליפה את כל אובייקט האיטרטור שנכתב ביד. בלי next, בלי done, בלי סיכון לטעות של אחד: רק לולאה רגילה עם yield במקום push.

בגלל זה גנרטורים קיימים. הם הופכים את "לכתוב איטרטור" ל"לכתוב פונקציה שמפיקה ערכים".

yield מול return

yield משהה, return מסיימת. אפשר לעשות yield כמה פעמים שרוצים, והגנרטור ממשיך מהמקום שבו עצר:

return בתוך גנרטור מופיעה כ-{ value: "done", done: true } בקריאה שמסיימת אותו. for...of ו-spread מתעלמים מהערך המוחזר הזה: הם צורכים רק פריטים שבהם done הוא false. אז אל תשתמשו ב-return value כדי להבריח פריט אחרון ללולאה, הוא ידולג.

רצפים עצלים ואינסופיים

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

הלולאה היא ממש while (true), ובכל זאת התוכנית מסתיימת, כי הגנרטור מתקדם רק כשמשהו מבקש את הערך הבא. אפשר לקחת את N הפריטים הראשונים, לעצור, והשאר אף פעם לא ירוץ:

take היא בעצמה גנרטור שעוטף גנרטור אחר. הרכבה של גנרטורים בצורה כזאת היא חלק גדול מהקסם שלהם: חלקים קטנים, שכל אחד מהם עושה דבר אחד.

האצלה עם yield*

אם גנרטור צריך להפיק את כל מה שיש ב-iterable אחר, yield* מאצילה אליו:

yield* עובדת עם כל iterable: מערכים, sets, גנרטורים אחרים, ומעבירה כל פריט אחד אחרי השני. זו המקבילה האיטרטורית של spread.

Async Generators בקצרה

גנרטור שמוצהר כ-async function* יכול לעשות yield לערכים שלוקח להם זמן, וזה שימושי להזרמה מ-API או לקריאת קטעים מקובץ. צורכים אותו עם for await...of:

async function* paginate(url) {
  let next = url;
  while (next) {
    const res = await fetch(next);
    const page = await res.json();
    for (const item of page.items) yield item;
    next = page.nextUrl;
  }
}

for await (const item of paginate("/api/users")) {
  console.log(item);
}

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

מתי להשתמש ב-Generator

השתמשו בו כש:

  • הרצף אינסופי או עשוי להיות כזה: מזהים, חותמות זמן, השהיות בין ניסיונות חוזרים.
  • הפקת כל הערכים יקרה והצרכן עשוי לעצור מוקדם.
  • אתם מממשים Symbol.iterator על אובייקט מותאם אישית. זה כמעט תמיד קצר יותר מכתיבת אובייקט { next() } ביד.
  • אתם רוצים להרכיב טרנספורמציות זורמות (take, filter, map) בלי לבנות מערכי ביניים.

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

הבא בתור: Symbols

Symbol.iterator הוא הסמל הראשון שרוב האנשים פוגשים, אבל הוא רחוק מלהיות היחיד. Symbols הם טיפוס פרימיטיבי שנועד בדיוק לנקודות הרחבה כאלה: מפתחות ייחודיים שמאפשרים לשפה ולקוד שלכם להתחבר לאובייקטים בלי להתנגש בשמות מאפיינים רגילים. זה העמוד הבא.

שאלות נפוצות

מה ההבדל בין iterable ל-iterator ב-JavaScript?

Iterable הוא כל אובייקט עם מתודת Symbol.iterator שמחזירה iterator. Iterator הוא האובייקט שבפועל מייצר ערכים: יש לו מתודת next() שמחזירה { value, done }. מערכים, מחרוזות, Map ו-Set הם iterables, וקריאה למתודת Symbol.iterator שלהם נותנת iterator שאפשר לעבור איתו צעד אחרי צעד.

מה זו פונקציית generator ב-JavaScript?

פונקציה שמוצהרת עם function* ומייצרת ערכים בעצלות בעזרת yield. קריאה לה לא מריצה את הגוף: היא מחזירה אובייקט generator שהוא גם iterator וגם iterable. כל קריאה ל-next() רצה עד ה-yield הבא, עוצרת ומחזירה את הערך שנמסר.

מה ההבדל בין yield ל-return ב-generator?

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

מתי להשתמש ב-generator במקום במערך?

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

איור של שפות התכנות ב-Coddy

ללמוד תכנות עם Coddy

להתחיל