closure ב-JavaScript

הסבר על אחד מהמנגנונים המצוינים אך המסוכנים שיש ב-JavaScript ואיך להזהר ממנו ולמה.

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

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

בואו ונגדיר פונקצית jQuery פשוטה שאמורה להיות מובנת לכולם:

קוד פשוט של closures
קוד פשוט של closures - לחצו להגדלה

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

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

שלוש הפונקציות מסומנות
שלוש הפונקציות מסומנות - לחצו להגדלה

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

משתנים בפונקציות האם והסבתא
משתנים בפונקציות האם והסבתא - לחצו להגדלה

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

טוב, אז נושא הקונטקסט מובן כמעט באופן אינסטנקטיבי, במיוחד למי שכבר יצא לו לכתוב כמה וכמה סקריפטים, אז מה הקטע עם ה-closure? ה-closure מאפשר לכל המשתנים בקונטקסט להיות זמינים עבור הפונקציה, במידה וניתן לקרוא לה מבחוץ, גם לאחר שפונקצית האב סיימה לרוץ והחזירה תוצאה. מה זאת אומרת? שימו לב לקוד שלנו – פונקצית הקליק, זו שמקפיצה את האלרט – ניתנת לקריאה מרחוק, הרי זה כל היעוד שלה – כל מי שיפעיל את הקליק יקבל גישה גם אל jLinks וגם אל intLinkIndex, למרות ששתי הפונקציות שהגדירו אותן כבר סיימו לרוץ והחזירו את התוצאה שלהן. הרי הקליק נקרא אחרי שעשינו each ובוודאי אחרי שהפונקציה האנונימית שמעליה סיימה לרוץ – ועדיין לכל קליק יש גישה למשתנים הגלובליים של שתי פונקציות האם. זה אומר שכל קליק הוא closure וכן גם כל each. הנה דוגמה:

ה-closures זמינים

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

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

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

פוסטים נוספים שכדאי לקרוא

גלילה לראש העמוד