Menu

Closures ב-JavaScript: איך הם עובדים ולמה הם חשובים

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

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

closure היא פונקציה שזוכרת

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

ההדגמה הקצרה ביותר:

makeGreeter רצה, מחזירה פונקציה פנימית ומסתיימת. הייתם מצפים שהמשתנה המקומי שלה name ייעלם, הרי הפונקציה סיימה. אבל הפונקציה הפנימית עדיין משתמשת ב-name, ולכן JavaScript שומרת אותו בחיים. greetAda זוכרת את "Ada". greetBoris זוכרת את "Boris". שני closures, שני ערכים זכורים נפרדים.

scope לקסיקלי הוא המקור של closures

הכלל שמאחורי closures נקרא scope לקסיקלי: פונקציה רואה משתנים מהמקום שבו היא נכתבה, לא מהמקום שבו קוראים לה. "לקסיקלי" פשוט אומר "לפי המיקום שלה בקוד המקור".

show מדפיסה "I'm outside", לא "I'm inside caller". היא נכתבה ליד ה-outer של הרמה העליונה, אז זה מה שהיא רואה. זה שקוראים לה ממקום שבמקרה יש בו outer משלו לא משנה כלום.

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

כל קריאה מקבלת closure משלה

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

הדוגמה הקלאסית היא מונה:

לכל אחד מ-a ו-b יש count משלו. שום דבר מחוץ לפונקציה המוחזרת לא יכול לגעת במשתנים האלה: count פרטי לגמרי. זו לא תכונה של השפה שהפעלנו; זה פשוט נובע מהאופן שבו closures עובדים.

משתנים פרטיים בלי מחלקות

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

balance הוא לא מאפיין של האובייקט המוחזר: הוא גר ב-closure. הדרך היחידה לקרוא או לשנות אותו היא דרך המתודות שחשפתם. גם מחלקות עם שדות #private יכולות לעשות את זה, אבל גרסת ה-closure קדמה להן בעשורים ועדיין מופיעה בכל רחבי האקוסיסטם.

המלכודת הקלאסית בלולאה

closures מכשילים אנשים הכי הרבה בתוך לולאות. ראו מה קורה עם var:

הייתם מצפים ל-0, 1, 2. מקבלים 3, 3, 3. הנה הסיבה: var הוא ברמת הפונקציה, אז יש רק i אחד לכל הלולאה. שלושת ה-closures לכדו את אותו משתנה, ועד שהם רצים הלולאה כבר הסתיימה ו-i הוא 3.

עברו ל-let:

עכשיו זה מדפיס 0, 1, 2. let הוא ברמת הבלוק: כל איטרציה של הלולאה יוצרת binding חדש ל-i, כך שכל closure לוכד ערך משלו. זו הסיבה הכי גדולה להעדיף את let על פני var.

closures לוכדים משתנים, לא ערכים

נקודה עדינה אבל חשובה: closure מחזיק ב_משתנה_, לא בתמונת מצב של הערך שלו ברגע שהפונקציה הוגדרה.

printMessage קוראת את message כשהיא רצה, לא כשהיא נוצרה. אם רוצים תמונת מצב, מעתיקים קודם את הערך למשתנה מקומי, וזה בעצם מה ש-let עושה בתוך לולאת for.

דפוס נפוץ מהעולם האמיתי: once

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

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

מילה על זיכרון

closure שומר את המשתנים שלכד בחיים כל עוד משהו עדיין מפנה אליו. בדרך כלל זה מה שרוצים, אבל אם מחברים closure למשהו שחי הרבה זמן (כמו event listener של ה-DOM או מטמון גלובלי) והוא לוכד משהו גדול, אי אפשר לפנות את הדבר הזה בזמן איסוף זבל עד שה-closure נעלם.

function attach() {
    const hugeData = new Array(1_000_000).fill("...");
    document.addEventListener("click", () => {
        console.log(hugeData.length);
    });
}

כל עוד ה-listener מחובר, hugeData נשאר בזיכרון. הסירו את ה-listener (או הימנעו מללכוד מה שלא צריך) וההפניה משתחררת. לא צריך לנהל את זה ברמת המיקרו, רק לדעת ש-closures וזיכרון קשורים זה בזה.

מה לקחת מכאן

  • closure היא פונקציה יחד עם המשתנים שהיא ראתה כשהוגדרה.
  • כל קריאה לפונקציה חיצונית יוצרת סט חדש של משתנים ל-closures הפנימיים שלה.
  • closures נותנים מצב פרטי בלי צורך במחלקות.
  • בתוך לולאות, השתמשו ב-let כדי שכל איטרציה תקבל binding משלה.
  • closures לוכדים את המשתנה עצמו, לא את הערך ברגע היצירה.

הבא בתור: מילת המפתח this

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

שאלות נפוצות

מה זה closure ב-JavaScript?

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

למה closures שימושיים?

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

למה closures מתנהגים מוזר בתוך לולאות עם var?

var הוא ברמת הפונקציה, כך שכל האיטרציות חולקות את אותו משתנה. כל ה-closures שנוצרים בלולאה מפנים למשתנה היחיד הזה, שעד שהם רצים כבר הגיע לערך הסופי. השתמשו ב-let במקום: הוא ברמת הבלוק, כך שכל איטרציה מקבלת binding משלה.

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

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

להתחיל