קלוז'רים בג'אווהסקריפט מוסברים בפשטות¶
קלוז'ר (closure) הוא אחד המושגים שגורמים לכי אנשים מתחילים לפחד מראיונות עבודה, כי הוא מוסבר בדרך כלל בשפה אקדמית מסובכת שגורמת לרעיון פשוט להישמע כמו קסם שחור. בואו נעשה את זה הפוך - קודם דוגמה, ואז ההסבר.
דוגמה קודם, הסבר אחר כך¶
function createCounter() {
let count = 0;
return function () {
count += 1;
return count;
};
}
const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3
עצרו רגע ותחשבו כמה זה מוזר. הפונקציה createCounter רצה, מגדירה משתנה count, ומחזירה פונקציה פנימית. ברגע שהפונקציה createCounter מסתיימת, לפי היגיון בסיסי, count היה אמור "להיעלם" - הרי היא היתה משתנה מקומי בתוך פונקציה שכבר סיימה לרוץ. אבל זה לא מה שקורה. הפונקציה הפנימית ש-createCounter החזירה "זוכרת" את count, וממשיכה לגשת אליו ולעדכן אותו, גם הרבה אחרי ש-createCounter כבר לא רצה.
זה קלוז'ר. זהו, זה כל הרעיון.
אז מה זה בעצם, בהגדרה פשוטה¶
קלוז'ר הוא בעצם פונקציה שזוכרת את הסביבה שבה היא נוצרה, כולל את המשתנים שהיו זמינים שם, גם אחרי שהסביבה ההיא "נגמרה" מבחינה טכנית. ג'אווהסקריפט לא מוחק משתנים ברגע שהפונקציה שיצרה אותם מסיימת לרוץ, אם יש עדיין פונקציה אחרת ש"מחזיקה" בהם ומשתמשת בהם.
זה לא תכונה נדירה או מיוחדת שצריך להפעיל במפורש - זה קורה כל הזמן, בכל פעם שאתם מגדירים פונקציה בתוך פונקציה אחרת. רוב האנשים משתמשים בקלוז'רים המון בלי לדעת שיש להם שם.
למה זה שימושי בפועל, לא רק תיאוריה¶
קלוז'רים נותנים לכם דרך ליצור "משתנים פרטיים" שאי אפשר לגשת אליהם ישירות מבחוץ, רק דרך פונקציות ספציפיות שאתם חושפים. בדוגמה של הדוגמה למעלה, אין דרך לגשת ל-count ישירות מבחוץ - console.log(count) ייתן שגיאה, כי count פרטי לגמרי לפונקציה createCounter. הדרך היחידה לשנות אותו היא דרך הפונקציה שהוחזרה, שמכילה בדיוק את ההיגיון שאתם רוצים לאפשר.
זה דפוס שימושי לכל מיני מקרים - ספירת מופעים, שמירת מצב פנימי בלי לחשוף אותו, ויצירת פונקציות "מותאמות אישית" מתוך פונקציה כללית יותר.
איפה זה מופיע בקוד ריאקט שאתם כבר כותבים¶
אם למדתם קצת ריאקט, יש סיכוי טוב שכבר השתמשתם בקלוז'רים בלי לשים לב. כל handler שאתם מגדירים בתוך קומפוננטה הוא בעצם קלוז'ר:
function Counter() {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(count + 1);
};
return <button onClick={handleClick}>{count}</button>;
}
הפונקציה handleClick "זוכרת" את הערך של count מהרינדור הספציפי הזה של הקומפוננטה, בזכות קלוז'ר. זו בעצם הסיבה לתופעה מוזרה שמבלבלת המון מתחילים בריאקט, שנקראת "stale closure" - כשפונקציה שנוצרה ברינדור ישן ממשיכה "לזכור" ערכים ישנים, גם אחרי שה-state התעדכן. להבין קלוז'רים זה בדיוק מה שעוזר להבין למה הבאג המוזר הזה קורה, ואיך להימנע ממנו.
המלכודת הקלאסית של קלוז'רים בלולאות¶
דוגמה נוספת שממחישה את זה טוב:
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 1000);
}
// מדפיס 3, 3, 3 - לא 0, 1, 2 כמו שהיינו מצפים
זה קורה כי var לא יוצר משתנה נפרד לכל איטרציה של הלולאה, אז כל שלוש הפונקציות "רואות" את אותו משתנה i, שבסוף הלולאה שווה 3. הפתרון הפשוט - להשתמש ב-let במקום var, כי let כן יוצר עותק נפרד של המשתנה בכל איטרציה, וכל קלוז'ר "זוכר" את הערך שהיה שייך לו ספציפית.
איך זוכרים את זה לתמיד¶
הדרך הכי טובה לתפוס את הרעיון היא לא לשנן הגדרה, אלא לכתוב כמה דוגמאות בעצמכם ולשחק איתן - לשנות ערכים, להוסיף console.log בכל מקום, ולראות בעצמכם מתי ואיך משתנים "נשמרים" בזיכרון.
בקורס צד לקוח יש פרק שלם על ג'אווהסקריפט מתקדם עם תרגילים שבנויים בדיוק כדי לתפוס את הרגע הזה של הבנה, לא רק לקרוא עליו.
לסיכום¶
קלוז'ר הוא פשוט פונקציה שזוכרת את הסביבה שבה היא נוצרה, כולל המשתנים שהיו שם, גם אחרי שהפונקציה החיצונית סיימה לרוץ. זה לא קונספט נדיר או מסובך, זה קורה בכל פעם שאתם מגדירים פונקציה בתוך פונקציה, כולל בכל handler שאתם כותבים בריאקט.
יש לכם דוגמה מבלבלת של קלוז'ר בקוד שלכם? תביאו אותה לדיסקורד ונפרק אותה ביחד.