כשמדברים על תכנות אי אפשר שלא לדבר גם על פונקציות. במאמר זה ננסה להבין מדוע חיוני מאוד בתכנות השימוש בפונקציות וכיצד משתמשים בהן בשפת javaScript – סוגי פונקציות, פונקציה טהורה ופונקציה לא טהורה, hoisting, closure.
אחת ממטרות העל של פונקציות בשפות תכנות היא לשמור על הקוד שלנו "Dry" (יבש) כלומר, לא לחזור על שורות קוד, ללא צורך, כאשר רוצים לבצע פעולה פונקציונלית שניתן לאגד אותה תחת כללים קבועים והגדרות שחוזרות על עצמן (חוקיות שחוזרת על עצמה). ע"י כך אנו יוצרים תכנית מורכבת היודעת לטפל בהמון מקרים באמצעות קריאה לשורות קוד החוזרות על עצמן מבחינה פונקציונלית לביצוע הפעולה.
פונקציה "טהורה" – pure function
בתכנות נהוג לקטלג את הפונקציות ל-2 סוגים:
פונקציה טהורה ופונקציה שאינה טהורה: pure function ו- Impure function.
פונקציה טהורה היא פונקציה שמקיימת את 2 התנאים הבאים:
- היא מחזירה תמיד את אותה תוצאה אם אותם ארגומנטים מועברים אליה. איננה תלויה בשום מצב או שינוי בנתונים במהלך הפעלת תכנית, אלא רק בארגומנטים המועברים אליה.
- אין תופעות לוואי צדדיות בשימוש בפונקציה – איננה משנה משתנים סטטיים מקומיים, משתנים לא מקומיים, ארגומנטים זרמי קלט ופלט.
דוגמה לפונקציה טהורה:
הפונקציה תמיד מקבלת 2 מספרים ומחזירה את הסכום שלהם. פרט לארגומנטים המועברים אליה היא לא משתמשת בשום ערך חיצוני, אינה תלויה בשום מצב חיצוני ואין שינויים צדדים בעקבות השימוש בפונקציה של משתנים סטטיים מקומיים וכיו"ב (להרחבה בעניין משתנים var ו- let היכנסו לכאן!).
function add(a, b) { return a + b; }
דוגמה לפונקציה לא טהורה:
כאן הפונקציה תלויה במשתנה ממחלקת Date שהוא כל הפעלה משתנה לזמן העכשווי ביותר, ולכן הפונקציה לא יכולה באופן, דטרמיניסטי, לדעת מה הפונקציה תחזיר בפועל.
function addToArray(arr) { var item = new Date(); arr.push(item); }
declaration Function – פונקציה מוצהרת
ב-JavaScript המבנה הקלאסי של פונקציה הוא:
- הכרזה על פונקציה באמצעות המילה השמורה "function".
- פתיחת סוגריים להעברת ארגומנטים.
function add(a, b) { return a + b; }
מה ההבדל בין פרמטרים לארגומנטים?
פרמטרים הם השמות הגדרתיים לערכים במבנה של פונקציה, שאותם הפונקציה צריכה לקבל – בתחום הסוגריים (a, b).
ארגומנטים אלו הם הערכים האמתיים שיועברו לפונקציה בפועל! למשל, כאן, שמות הפרמטרים שהפונקציה מקבלת הם a ו-b.
הארגומנטים שיועברו בפועל הם (1 ו-2) והפונקציה תחזיר 3.
function add(a, b) { // a = 1, b =2 return a + b; }
a ו-b הם שמות הפרמטרים אותם תמיד הפונקציה חייבת לקבל כדי לפעול: זה השם ההגדרתי לערכים שהפונקציה אמורה לקבל, אולם בפועל, הפונקציה לא תקבל את "a ו -"b, "פשוטו כמשמעו", אלא תקבל את הערך האמתי שלהם: את המספרים "1 "ו -"2."
Expression Function – פונקציה עם ביטוי
פונקציה שהוכרזה בצורה של ביטוי – יצרנו משתנה (ביטוי) וקבענו שהערך שלו יהיה שווה לפונקציה. הפונקציה כאן היא אנונימית אין לה שם משל עצמה ולכן ניתן לגשת אליה באמצעות "ביטוי" שזה בעצם השם של המשתנה שיהיה שווה לפונקציה הזאת.
var func = function (a, b) { return a + b; }; var result = func(1, 2);
המשתנה (הביטוי) "func" שווה לפונקציה.
המשתנה אינו שווה למה שהפונקציה תפעיל ב-"invoke" (ה-return שלה), אלא שווה לפונקציה עצמה – היכולת הפונקציונלית המלאה של הפונקציה (ולכן נדרש להפעיל אותה invoke) .
כאן כדי להפעיל את הפונקציה נצטרך ליצור משתנה שיהיה שווה ל"ביטוי" של הפונקציה האנונימית שנקרא כאן "func" .על-מנת להפעיל את הפונקציה נשלח את הארגומנטים המבוקשים.
ב-result תהיה לנו בעצם התוצאה של מה שיוחזר מהפונקציה (1+2): 3.
לצורך הבנה טובה יותר, כאן כשהשווינו את func2 ל-func (ולא רשמנו ב-func את הסוגריים העגולות – () שמקבלות ארגומנטים ובעצם מעידות שאנו רוצים לבצע invoke (הפעלה של הפונקציה), ( השווינו בעצם את func2 לפונקציה עצמה של func (ליכולת הפונקציונלית שלה). יהיה כאן בעצם Reference לפונקציה.
ובתמונה ניתן לראות ש –func2 שווה לפונקציה ממש:
לעומת זאת, ב-func3 השווינו את הפונקציה ל-return של הפונקציה func! (Reference למה שהפונקציה מחזירה) ולכן קיבלנו 3.
להלן סיכום ההבדלים:
First class citizen – פונקציות "אזרח מדרגה ראשונה"
ב-JavaScript הפונקציות הן "First class citizen functions", כלומר,
ניתן להתייחס לפונקציה כמו כל משתנה אחר: ניתן להעביר את הפונקציה כארגומנט לפונקציה אחרת (כמו שמעבירים משתנים) וניתן להחזיר פונקציה ב-return.
למשל כאן, יצרנו פונקציה ()outerFunc שמכילה פונקציה ()innerFunc שמדפיסה למסך את המילה "inner" ומחזירה את עצמה.
יצרנו משתנה בשם result ושמנו בתוכו את הפונקציה ()outerFunc (כשהיא מופעלת עם invoke) וכשהיא מופעלת חוזרת ממנה הפונקציה innerFunc.
כלומר, result יהיה שווה לפונקציה innerFunc (ליכולת הפונקציונלית שלה) אך לא יופעל, ולכן כשנרצה להפעיל אותו נרשום ככה:()result.
רק לאחר מכן יודפס הכיתוב למסך "inner".
function outerFunc() { function innerFunc() { console.log('inner'); } return innerFunc; } var result = outerFunc(); result();
callback function ו- higher order function
פונקציה שמועברת כארגומנט נקראת callback function.
והפונקציה שמקבלת את הפונקציה כארגומנט נקראת higher order function ("פונקציה מסדר גבוה").
למשל בדוגמא כאן הפונקציה ()func היא פונקציה שאותה אנו שולחים כארגומנט לפונקציה ()showResult.
ופונקציה ()showResult מקבלת את הפונקציה ()func כארגומנט, שומרת את מה שיוחזר מהפעלת הפונקציה ()func במשתנה בשם result ומדפיסה את התוצאה למסך.
()func היא callback function ("פונקציה שקוראים לה") – היא לא מפעילה את עצמה אלא פונקציה אחרת מקבלת אותה כארגומנט ועושה לה בתוכה invoke.
()showResult היא higher order function.
// func is callback function var func = function () { console.log('func'); } // higher order functions function showResult(func) { var result = func(); console.log("Result: " + result); }
Hoisting -"הנפה כלפי מעלה"
JavaScript היא שפה שבד"כ משתמשים בה ב-frontend ואינה בעלת קוד ביניים. הדבר הזה יוצר הקלות רבות כמו שפה פחות בררנית ויותר זורמת היות ובהיעדר קומפילציה אין תקלות בזמן קימפול, אולם אנו עדיין חשופים לתקלות היותר חמורות – תקלות בזמן ריצה!
כדי למנוע מצב של קריסה עקב משתנים לא מוצהרים השפה מעלה אוטומטית את כל המשתנים בפונקציה לשורות הראשונות של תחילת הפונקציה ומצהירה עליהן כ-undefined.
המקרה הקלאסי שבו אנחנו מצהירים על פונקציה פנימית ורק אז עושים לה invoke:
מקרה בו אנו מצהירים על הפונקציה לפני ההכרזה עליה.
בזכות ה-hoisting – מאחורי הקלעים func יועלה לשורה הראשונה של הפונקציה החיצונית ויוכרז כמשתנה undefined. ולכן נקבל הודעת שגיאה ש-func הוא לא פונקציה – היות שהוא מזוהה, תודות ל-hoisting שהקפיץ אותו לראש השורה, כמשתנה.
הודעת השגיאה:
לעומת זאת, אם ננסה לממש פונקציה שלא מוכרזת כלל בקוד נקבל הודעת שגיאה שהמשתנה עצמו בכלל לא מוגדר (כלומר לא קיים בקוד כלל!):
Closure – "סגירת פינות" (lexical scope)
Closure הוא שילוב של פונקציה פנימית עם הפניות להצהרת הפונקציה החיצונית העוטפת אותה.
כמו כן היא מעניקה גישה למידע שמחוץ לפונקציה הפנימית ובתוך הפונקציה החיצונית העוטפת אותה.
ב-JavaScript, מעטפת זו נוצרת בכל פעם שנוצרת פונקציה, בזמן יצירת הפונקציה. המעטפת הזאת שומרת בתוכה את כל הנתונים הקריטיים להפעלתה שמוצהרים רק בפונקציה החיצונית וע"י מבטיחה את הפעלת הפונקציה ללא קריסה.
לצורך דוגמה:
הכרזנו בשורה 3 על משתנה בשם numBigger (לפני פעולת החזרת הפונקציה).
המשתנה הזה קריטי לצורך החזרת הפונקציה שכן בתוך הלוגיקה בפונקציה נעשה בו שימוש בשורה 5. כלומר ,יש לנו פונקציה אנונימית מוחזרת שיש לה reference במקום אחר בקוד מחוץ לפונקציה (בשורה 2).
באופן שגרתי, כשאנחנו מסיימים פעולה של פונקציה או שימוש במשתנים ה-"garbage collector" מנקה לנו את כל המשתנים שאין להם reference לשום מקום אחר בקוד. כלומר, בשורה 10 המשתנה numBigger אמור להתנקות ע"י ה-"garbage collector". אבל יש כאן משהו אחר שקורה.
בשורה 12 ושורה 13 יש 2 פונקציות ביטוי (Expression Functions) : factFunc ו-factFunc1 שעדיין משתמשות בפונקציה ()createFactorial. ולכן, כדי למנוע ניקוי של המשתנה numBigger (וע"י כך לגרום לפונקציות האלה לא לעבוד)ישנה מעטפת שנקרא "Closure" שעוטפת לנו את כל המשתנים והדברים הקריטיים שעדיין יש להם reference בזיכרון אותם היא שומרת ב-scope מיוחד כדי שנוכל להריץ את הקוד ולא נקבל קריסה.
כאן שורות 12-13 מחייבות שמירה של ערך numBigger, אחרת הן לא יוכלו לפעול! (כל הלוגיקה של הקוד תלויה ב-numBigger).
// closure function createFactorial(num) { var numBigger = num * 2 return function () { var n = 1; for (let i = 1; i < numBigger; i++) { n *= i; } return n; } } var factFunc = createFactorial(5); var factFunc1 = createFactorial(6);
לקריאה מורחבת בעניין פונקציות בשפת javaScript ראו כאן!
2 Responses
היי, בסעיף שכתוב על Hoisting כתוב: "מקרה בו אנו מצהירים על הפונקציה לפני ההכרזה עליה."
בטח התכוונת, מקרה בו אנחנו קוראים לפונקציה לפני ההכרזה שלה, אני מניח. פשוט זה קצת בלבל אותי.
אתה יכול למחוק את התגובה כמובן.
כתבה מעולה, תודה!