Connect with us

קטגוריות

Javascript

Javascript: מה זה Generators ואיך אנחנו יכולים להשתמש בהם?

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

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

אתם חוזרים לספר, אתם לא מתחילים לקרוא את הספר מהתחלה, אתם ממשיכים מאותה נקודה שסימנתם.

ובאותו רגע, התנהגתם בדיוק כמו פונקציות Generators.

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

בהגדרה, פונקציות Generators הן פונקציות מיוחדות שיכולות לעצור "באמצע הדרך" ולהמשיך מאותה נקודה בדיוק.

בואו נקח את הדוגמא הבאה:

function* whatToSay() {
  yield 'CodeHub';
  yield 'is';
  yield 'AWESOME';
}

let speak = whatToSay(); // {} 

console.log(speak); // {} 
console.log(speak.next()); // { value: 'CodeHub', done: false }
console.log(speak.next()); // { value: 'is', done: false }
console.log(speak.next()); // { value: 'AWESOME', done: false }
console.log(speak.next()); // { value: undefined, done: true }

שימו לב שפונקציה מסוג ג׳נרטור מוצהרת על ידי כוכבית (function*) ובתוך גוף הפונקציה אין לנו return, במקום זה יש לנו המילה השמורה yield, בעזרת yield הג׳נרטור יכול להשהות את עצמו.

בכל פעם שהג׳נרטור מגיע ל-yield הוא מחזיר אובייקט מהצורה (shape) הבאה:

{ 
  value: Any,
  done: Boolean 
} 
  1. done: אם הוא true, הג׳נרטור אומר לנו שאין לו מה להחזיר.
  2. value: הערך שהג׳נרטור החזיר לנו, יכול להיות כל דבר.

גנרטורים מממשים את הפרוטוקול iteration החדש של Javascript ותמיד יחזירו אובייקט מסוג Generator, זה אומר שאנחנו יכולים להשתמש עם האובייקט בלולאת for…of למשל.

אנחנו יכולים ליצור Iterator משלנו בצורה הבאה:

function fakeGenerator(arr) {
  let id = 0;
  return {
    next: () => {
      return id < arr.length ? {
        value: arr[id++],
        done: false
      } : { value: undefined, done: true };
    }
  };
}
let fakeOne = fakeGenerator(['CodeHub', 'is', 'AWESOME']);”

// ואז נוציא את הערכים

console.log(fakeOne); // { next: [Function: next] }
console.log(fakeOne.next()); // { value: 'CodeHub', done: false }
console.log(fakeOne.next()); // { value: 'is', done: false }
console.log(fakeOne.next()); // { value: 'AWESOME', done: false }
console.log(fakeOne.next()); // { value: undefined, done: true }”

אם תשימו לב, התוצאות הן כמעט זהות. עם הבדל חשוב אחד: Iterator שלנו הוא בסך הכל אובייקט המכיל פונקצייה next, ה-Iterator חייב לעשות את ״העבודה הקשה״ ולשמור על ה-state הפנימי שלו (בדוגמא שלנו: לנהל את המשתנה id). 

Generator הוא ידאג בשבילנו למימוש של Symbol.iterator,  יבצע את האימפלימנטציה של next ו-done ואנחנו לא צריכים לדאוג לשמירת ה-state. אפשר להגיד שGenerators הם פס ייצור ל-Iterators.

ניתן לייצר Generator אין סופי שיזרים לנו מידע, לדוגמא, נניח ונרצה לכתוב פונקציה שתחזיר לנו מספרי id, נוכל לעשות זאת כך:

function * generateId() {
  let id = 0;
  while (true) {
   yield ++id;
  }
}

כתבנו ג׳נרטור בשם generateId, ובתוכו יש לנו לולאת while אין סופית. בתוך הלולאה אנחנו מחזירים (yield) את ה-id במקרה שלנו את הערך 1, כשהג׳נרטור מחזיר ערך הוא משהה את עצמו, וכשאנחנו שוב פעם מבצעים קריאה עם פונקציית next הג׳נרטור חוזר לחיים וממשיך מאותה נקודה שבה הוא השהה את עצמו עד שהוא שוב פעם מגיע ל- yield ++id (כי אנחנו נמצאים בלולאה אין סופית) ומחזיר את הערך 2 וכך הלאה.

ואם נשתמש בג׳נרטור לייצר id's נקבל את פלט הבא:

כמו שאתם רואים בדוגמא למעלה איתחלתי את הג׳נרטור עם ערך 21 ובכל קריאה ל-next אנחנו נקבל את הערך הבא.

החלק הטריקי ב-Generator

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

כשאנחנו חוזרים לג׳נרטור באמצעות next אנחנו יכולים להעביר ארגומנטים, בואו ניקח את הדוגמא הבאה:

function * calculation(i) {
  	console.log(i);

	const j = 2 * (yield (i * 5));
	console.log(j);

	const k = yield(10 * j / 2);
	console.log(k);

}
// איתחול הג׳נרטור
const value = calculation(5);

value.next(10); // פלט: {value: 25, done: false}
value.next(2); // פלט: {value: 20, done: false}
value.next(1); // פלט: {value: undefined, done: true}

אנחנו מתחילים באתחול הג׳נרטור עם הערך 5, בשלב הזה עדיין לא חוזר לנו ערך מכיוון שפונקציות Generators מחזירות ערך על ידי שימוש בפונקציית next, אך הערך 5 הועבר לג׳נרטור והוא זוכר אותו.

כשאנחנו קוראים לג׳נרטור בפעם הראשונה על ידי שימוש בפונקציית next עם הערך 10, הארגומנט i מכיל את הערך 5, הג׳נרטור מבצע את כל הפקודות עד קריאת ה-yield הראשונה שמבצעת את החישוב i * 5, בפלט אנחנו יכולים לראות שה-value שלנו הוא באמת 25.

כשאנחנו חוזרים לג׳נרטור בפעם השניה עם הערך 2, הערך 10 (שהעברנו בקריאה הקודמת) החליף את על הביטוי של yield, דמיינו משהו כמו yield i * 5) = 10), אז לפי החישוב 10 * 2 זה הערך של j, הג׳נרטור מתקדם ומבצע את החישוב 10 * j / 2 ובאמת חוזר אלינו {value: 20, done: false}.

בפעם הבאה שנחזור לג׳נרטור עם הערך 1, כל הביטוי yield(10 * j / 2) יוחלף בערך 2 (אותו העברנו בקריאה הקודמת), אך אין לנו קריאת yield נוספת וכתוצאה מכך אנחנו נקבל {value: undefined, done: true}.

לסיכום

אני מקווה שהצלחתי להסביר בצורה טובה מה זה בכלל Generators ואת השימוש הבסיסי בהם, יש שימושים יותר מתקדמים ב-Generators שאסביר אולי במאמר נוסף.

עד אז, כמו שרינגו סטאר אומר Peace & love.

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

עוד ב Javascript

Meni Edri היי אני מני, מתכנת מנוסה ומאוד אוהב קוד ואת עולם התכנות, הקמתי את CodeHub על מנת שתיהיה לי פינה שבה אוכל לכתוב, ללמד וללמוד, מקווה שתימצאו את הבלוג הזה שימושי.
קריאה מהנה.

מאמרים פופולריים

נושאים פופולריים

חזרה למעלה