בפוסט זה אני רוצה להתמקד במבנים שגורים בג'אווהסקריפט והבנה של המשמעות שלהם בפועל: אילו Tradeoffs אתם מבצעים כאשר אתם בוחרים בהם.
שייך לסדרה מבוא מואץ ל JavaScript ו jQuery
מבוא
האם JavaScript היא שפת Object Oriented? - תלוי את מי שואלים.
פורמלית, ג'אווהסקריפט מוגדרת כ Object-Oriented Language מול Class Oriented-Languages שהן ג'אווה ו#C. יש שוני שהדגש הוא על אובייקטים - לא מחלקות. בואו נשים את הפורמליסטיקה בשלב זה בצד.
התשובה שלי היא כזו: ג'אווהסקריפט היא יותר סט של כלים (toolkit) בו ניתן להשתמש בכדי לכתוב קוד OO. ניתן גם לכתוב בה קוד פונקציונלי ואולי אפילו גם קוד לוגי. השפה לא תחייב אתכם ל OO וכמעט לא תסייע או תנחה. זוהי גישה שונה למדי משפות כמו #C או Java שהן שפות OO מוצהרות ומכוונות את המפתח להשתמש בהן ככאלו.
אם תרצו לכתוב OO איכותי בג'אווהסקריפט - תדרשו להפעיל לא מעט משמעת עצמית. הקלות בה ניתן לבצע "תרגיל" (hack) בקוד ג'אווהסקריפט היא מדהימה, אנו נזדקק למנגנונים חברתיים (למשל Code Review) או טריגרים אוטומטיים (JSLint / JSHint) על מנת לשמור על קוד OO נקי ומסודר.
הכמסה
מהו העיקרון החשוב ביותר של Object Oriented? נכון: הכמסה (אם אתם חושבים אחרת - אז זהו פוסט בשבילכם).
הדרך הבסיסית להשיג הכמסה בג'אווהסקריפט היא בעזרת Closure:
הפונקציה העוטפת משמשת לצורך יצירת scope חדש ותו-לא. מכיוון שאין לנו רצון להשתמש בפונקציה כפונקציה - היא אנונימית ומופעלת מיד לאחר הגדרתה (הסוגריים בשורה האחרונה יגרמו להרצתה).
התוצאה: ה scope שיצרנו הוא אנונימי, ואין כל אפשרות לגשת אליו מבחוץ - השגנו הכמסה מוחלטת.
שימו לב לחשיבות להגדיר את המשתנים ואת הפונקציות הפנימיות בעזרת var. ללא var, הפונקציה / המשתנה יוגדרו ב global scope.
באפליקציות ג'אווהסקריפט, רוב הקוד שנכתוב יהיה private - אף אחד לא אמור לקרוא לו. אנו ניגש ל DOM, נייצר UI, נרשום אירועים ונגיב אליהם - את כל זה אפשר לעשות מבלי "ללכלך" את ה global scope. אין סיבה שלא תעטפו את כל הקוד האפליקטיבי שלכם, שאינו מספק שירותים לקוד אחר - בצורה זו.
עבור ספריות שבהן נרצה לחשוף פונקציות לשימוש חיצוני - הסיפור הוא אחר.
שימו לב שג'אווהסקריפט לא סיפקה לנו כלי מפורש ליצירת אזור "private", אולם היא מספיק גמישה על מנת שנוכל ליצור אזור private בקלות וללא קוד מסובך.
יתרונות מבנה זה: בעזרת פעולה פשוטה למדי אנו יכולים להמנע מהתנגשויות של משתנים או פונקציות עם שם זהה.
חסרונות מבנה זה: הכמסה של "הכל או לא-כלום", קשה לעקוב אחר המשתנים הפנימיים בזמן Debug. כרגע - השגנו הכמסה עבור תכנות פרוצדורלי (כלומר רק פונקציות - לא אובייקטים).
Prototype
בפוסט הקודם הזכרתי את המשתנה שנמצא על כל אובייקט ב JavaScript - הרי הוא ה __proto__, או בשמו המלא: ה Prototype. לג'אווהסקריפט יש מודל ייחודי לניהול אובייקטים שלא קיים בשום שפה נפוצה אחרת.
בעוד מפתחי ג'אווה עשויים להיחרד מפרטי המנגנון, מפתחי ג'אווהסקריפט וותיקים דווקא נוטים לאהוב אותו ואת הגמישות הרבה שהוא מספק. אין ויכוח שג'אווהסקריפט היא "אלפא": השאלה האם מדובר ב"גרסת אלפא" קרי לא-יציבה ולא-סגורה, או "זן אלפא" - כלומר זן עליון. עניין של נקודות השקפה, אני מניח.
בואו נתבונן כיצד המילים השמורות prototype ו new בשפה מאפשרת לנו לנהל "אובייקטים":
הורשה בג'אווהסקריפט מבוטאת ע"י שרשרת של קשרי prototype - לכל אובייקט יש אב שמסומן ב property בשם __proto__. ב default זה יהיה האובייקט Object - האב המשותף לכל האובייקטים בשפה. המילה השמורה prototype מייצגת את המשתנה __proto__ (שלא אמורים להשתמש בו ישירות, אך הוא שימושי ל debug).
בשלב הבא אנו מחליפים את ה prototype של הפונקציה Calculator (שהיה עד כה האובייקט Object) לאובייקט שאנו מגדירים (בעזרת תחביר של object literal). ההחלפה תשפיע על כל instance חדש שייווצר מ Calculator בעזרת המילה השמורה new.
לבסוף, אנו מוסיפים מתודה נוספת לאובייקט שנוצר.
כאשר אנחנו קוראים ל new Calculator מתרחשים 2 דברים:
א. הפונקציה תחזיר כערך החזרה את this - מצביע לאובייקט חדש שנוצר.
ב. בתוך הפונקציה, הערך this יתייחס לאובייקט החדש שנוצר (נשמע פשוט, אך בג'אווהסקריפט זה לא מובן מאליו)
לאחר הגדרה זו, אנו יכולים לייצר instances ולהפעילם בצורה הבאה:
למרות שכמה ספריות נפוצות (למשל Prototype.js) עושות תרגילים שכאלו - זו נחשבת התנהגות לא רצויה ולא מומלצת בעליל. כדאי להימנע ממנה לגמרי.
רק להזכיר סיבוך אחד אפשרי: בג'אווהסקריפט אובייקטים הם "שקים" של properties (מעין HashTable או Map) - ולעתים קרובות אנו מתייחסים אליהם ככאלו. כל אובייקט יציג את ה properties שהוגדרו עליו ישירות וגם על כל שרשרת ה prototypes שהוא קשור בה. שינוי (הוספה / מחיקה) של property לאחד ה prototypes - יכול לשבור קוד קיים במספר תסריטים. ההמלצה היא לנקוט באחת משתי גישות:
הרצון באובייקטים+הכמסה = Module
עד עכשיו השגנו או הכמסה, או אובייקטים - אך לא את שניהם ביחד. בואו נבחן מבנה שיאפשר לנו לקבל את שניהם:
הפונקציה Calculator (=קונסטרקטור) מגדירה משתנים ופונקציות בתוך עצמה, בתוך ה Closure. בנוסף היא מייצרת אובייקט (בעזרת Object Literal) שחוזר למי שקורא לה - במקום this. לא ציינתי קודם, אך קריאה ל new תחזיר את this רק במידה ולא הוגדר return value מפורש. אם הוגדר return - אזי הוא מה שיחזור.
שימו לב שבעזרת החזרה זו נוצר ההבדל מהדוגמה הקודמת: הערך החוזר מהקונסטרקטור הוא לא instance של הפונקציה Calculator עצמה, אלא אובייקט שאנו יצרנו ואנו שולטים בו. אובייקט זה מייצג את החלקים ה public של האובייקט שלנו. כמובן שבג'אווהסקריפט אין type safety ואין שום בעיה שקונסטרקטור יחזיר אובייקט מ"טיפוס" אחר.
כיוון שיש לנו פונקציה בתוך פונקציה, ויש reference מהפונקציה addBy למשתנה value - מובטח לנו שהמשתנה value ימשיך לחיות גם לאחר שהפונקציה Calculator הסתיימה. אם אתם לא זוכרים מדוע - חזרו להסבר על Closure בפוסט הקודם.
באופן זה, מי ששמר Reference לאובייקט שחזר יכול לבצע פעולות על האזורים הציבוריים, בעוד האזור הפרטיים "סגורים" בתוך ה Closure. עצם ההצבעה של ה Object Literal שחזר אל המשתנים הפנימיים - שומר על אורך חייהם.
שימו לב שמבנה זה, בניגוד לקודם, אין להשתמש ב this כדי להתייחס למשתנים של ה Closure.
החיסרון העיקרי כאן, שקשה לשים אליו לב, הוא שפונקציות שמוגדרות בחלק הפרטי של האובייקט יהיו מוגדרות מחדש בכל קריאה ל new. המשמעות היא שכל אובייקט מסוג Calculator יאכסן בזיכרון עותק של קוד הפונקציות addBy ו multiplyBy. במקרים רבים שווה לספוג עלות זו על מנת להשתמש במבנה חביב זה.
מבנה זה הוא דיי מקובל ונקרא "Module".
הנה וריאציה קצת שונה של Module:
בנוסף עשיתי עוד שדרוג קטן והגדרתי את המודול בתוך namespace. באופן זה אני מצמצם משמעותית את היכולת של ה Constructor להידרס ע"י מפתח אחר שגם במקרה בחר בשם Calculator. ייתרון נוסף ב namespace הוא ב Debugging, כאשר אוכל למצוא בקלות את המשתנים שלי בתוך ה namespace - ולא בערמה אחת עם כל משתני המערכת. ייתרון זה בא לידי ביטוי במיוחד כאשר יש כמה מפתחים על אותו הקוד. עדיין במודול יש Closure ומשתנים פרטיים של Closure לא נראים ברוב ה debuggers ללא breakpoint בתוך הקוד של יצירת ה Closure. לא נורא.
שימו לב שעלי להגדיר את myNS.Calculator ללא var - מכיוון שבג'אווהסקריפט אסורות הגדרות של משתנה עם "." בשם. הגדרתי את myNS מבעוד מועד - וחסכתי בעיות. ה namespace נדרש בקריאה לקונסטרטור - אך זו תוצאה רצויה ע"מ לוודא שאני באמת קורא לקונסטרקטור שלי, ולא של מישהו אחר.
באופן אישי, מודול נראה לי הדרך המועדפת ל"תיאור מחלקות ב JavaScript", החיסרון העיקרי הוא שכל אובייקט מכיל עותק של הפונקציות בזיכרון. לא נורא אם לא מדובר בהרבה אובייקטים.
כמה משחקים אחרונים
דוגמת ה prototype וה Module הן כנראה הדוגמאות הנפוצות. בכל זאת - לא תמיד הן יהיו בדיוק כפי שהצגתי. למתכנת יש יכולת לבצע עליהן וריאציות שונות שמשנות מעט את ההתנהגות. על מנת לחדד את החושים שלכם להבדלים הקטנים הללו - אציג כמה "מיני וריאציות" של ה Module וה Prototype. כשתתקלו בקוד אמיתי, דוגמאות אלו יכינו אתכם לכך שיש להתבונן בכמה פרטים ולא ישר להניח שאם אתם מזהים את המבנה הכללי - אזי אתם יודעים בדיוק במה מדובר.
כמעט מיותר לציין שחלק גדול מהקוד שכתוב ב JavaScript הוא:
א. מבנים פרימיטיביים ולא אופטימליים (אם ניתן לקרוא לזה מבנים)
ב. Copy-Paste של מבנים שחשבו עליהם - אך מי שהעתיק לא צלל למשמעות המלאה של המבנה. JavaScript Kiddies.
בואו נתבונן על הווריאנט הבא:
שמתם לב להבדל? הוא קטן למדי ולכן הדגשתי אותו ב Bold.
הסוגריים בסוף הביטוי גורמים לכך שהמשתנה calculator מכיל את תוצאת הרצת הפונקציה ולא מצביע לפונקציה, כפי שהיה קודם. לכן, ע"פ קונבנציה, יש לקרוא לו calculator ב c קטנה.
האם אתם יכולים לעצור לרגע ולחשוב מה ההשלכות של שינוי זה?
כיצד תתבצע קריאה ל"מחלקה" שכזו?
ובכן, Singleton הוא דיי נפוץ בפיתוח אפליקציות Client Side. השינוי שביצענו בעצם הוא דרך לבטא Singleton. בניגוד לצד השרת בו דיי נדיר למצוא Singleton ברמת שפה - אנו לרוב מגדירים singleton ברמת הDependency Injection Framework או Service Layer. בג'אווהסקריפט לא נראה לי שיש דDI או Service Layer ואנו מגדירים Singleton ברמת השפה.
הקריאה לקוד, אם כן, תראה משהו כזה:
ניתן גם לממש Singleton בצורה יותר "קלאסית" (דומה לג'אווה): לאכסון את המצביע ל instance היחיד על ה prototype ולהחזיר אותו בפונקציית getInstance. אני חושב שהדרך שהצגתי פה היא משמעותית יותר נפוצה בעולם הג'אווהסקריפט. שימו לב לדקויות כמו אם שם הפונקציה מתחילה ב Capital Letter או האם יש סוגריים של הפעלה בסוף הגדרת הפונקציה. פעמים רבות הסוגריים לא יהיה ריקים אלא יגדירו פרמטר שמועבר לקונסטרקטור - אולי ערך שמחושב בזמן יצירתה.
הנה מבנה קצת מורכב שמשלב אלמנטים שונים מהדוגמאות השונות:
האם אתם יכולים לעצור לדקה, לקרוא את הקוד, ולחשוב מה המשמעות שלו?
חזרנו למבנה מפוצל של קונסטרקטור לחוד וגוף האובייקט לחוד - שזה פחות טוב. מצד שני, בזכות השימוש ב prototype - החלק השני שמכיל את הפונקציות לא ישוכפל לכל אובייקט. יש לנו גם הכמסה והפרדה בין private ל public.
המבנה הזה בעצם מחלק את ה"מחלקה" ל2 חלקים:
קונסרקטור - שמגדיר את החלקים שנרצה לשכפל, קרי משתנים של האובייקט.
פרוטוטייפ - שמגדיר את החלקים שנרצה להגדיר פעם אחת, קרי הפונקציות.
המשתנים שמוגדרים בתוך הקונסטרקטור צריכים להיות מוגדרים ביחס ל this - כך בפעולת new הם יהיו משויכים ל instance שנוצר. ללא this הם פשוט "ימחקו" בסוף הרצת הקונסטרקטור - לא דבר שאנחנו רוצים.
גם ההתייחסות בחלק הפרוטוטייפ צריכה להיות ל this - מסיבה זהה.
this בג'אווהסקריפט מתנהג בצורה עקלקלה. על מנת להבין אותו לעומק כדאי לקרוא את הפוסט שעוסק ב this, בהמשך הסדרה.
מה הלאה?
זהו. סיימנו רק 2 פוסטים בנושא, אך אני מרגיש כאילו עברנו כברת דרך משמעותית בדיון על ג'אווהסקריפט. מלבד הכרת הדפדפן, ה DOM וספריות כגון jQuery (שמכילות המון פרטים שאפשר ללמוד), נראה לי שהעיסוק בניתוח מבנים בג'אווהסקריפט הוא הקפיצה הגדולה בהבנת השפה ואופייה.
שייך לסדרה מבוא מואץ ל JavaScript ו jQuery
מבוא
האם JavaScript היא שפת Object Oriented? - תלוי את מי שואלים.
פורמלית, ג'אווהסקריפט מוגדרת כ Object-Oriented Language מול Class Oriented-Languages שהן ג'אווה ו#C. יש שוני שהדגש הוא על אובייקטים - לא מחלקות. בואו נשים את הפורמליסטיקה בשלב זה בצד.
התשובה שלי היא כזו: ג'אווהסקריפט היא יותר סט של כלים (toolkit) בו ניתן להשתמש בכדי לכתוב קוד OO. ניתן גם לכתוב בה קוד פונקציונלי ואולי אפילו גם קוד לוגי. השפה לא תחייב אתכם ל OO וכמעט לא תסייע או תנחה. זוהי גישה שונה למדי משפות כמו #C או Java שהן שפות OO מוצהרות ומכוונות את המפתח להשתמש בהן ככאלו.
אם תרצו לכתוב OO איכותי בג'אווהסקריפט - תדרשו להפעיל לא מעט משמעת עצמית. הקלות בה ניתן לבצע "תרגיל" (hack) בקוד ג'אווהסקריפט היא מדהימה, אנו נזדקק למנגנונים חברתיים (למשל Code Review) או טריגרים אוטומטיים (JSLint / JSHint) על מנת לשמור על קוד OO נקי ומסודר.
הכמסה
מהו העיקרון החשוב ביותר של Object Oriented? נכון: הכמסה (אם אתם חושבים אחרת - אז זהו פוסט בשבילכם).
הדרך הבסיסית להשיג הכמסה בג'אווהסקריפט היא בעזרת Closure:
(function() {
var n = 2;
var addBy = function(num, x) { return num + x; }
var multiplyBy = function(num, x) { return num * x; }
n = addBy(n, 2);
n = multiplyBy(n, 3);
console.log(n);
}());
המבנה של קוד זה עשוי להראות מעט מוזר למפתחי ג'אווה - אך הוא שימושי למדי!var n = 2;
var addBy = function(num, x) { return num + x; }
var multiplyBy = function(num, x) { return num * x; }
n = addBy(n, 2);
n = multiplyBy(n, 3);
console.log(n);
}());
הפונקציה העוטפת משמשת לצורך יצירת scope חדש ותו-לא. מכיוון שאין לנו רצון להשתמש בפונקציה כפונקציה - היא אנונימית ומופעלת מיד לאחר הגדרתה (הסוגריים בשורה האחרונה יגרמו להרצתה).
התוצאה: ה scope שיצרנו הוא אנונימי, ואין כל אפשרות לגשת אליו מבחוץ - השגנו הכמסה מוחלטת.
שימו לב לחשיבות להגדיר את המשתנים ואת הפונקציות הפנימיות בעזרת var. ללא var, הפונקציה / המשתנה יוגדרו ב global scope.
באפליקציות ג'אווהסקריפט, רוב הקוד שנכתוב יהיה private - אף אחד לא אמור לקרוא לו. אנו ניגש ל DOM, נייצר UI, נרשום אירועים ונגיב אליהם - את כל זה אפשר לעשות מבלי "ללכלך" את ה global scope. אין סיבה שלא תעטפו את כל הקוד האפליקטיבי שלכם, שאינו מספק שירותים לקוד אחר - בצורה זו.
עבור ספריות שבהן נרצה לחשוף פונקציות לשימוש חיצוני - הסיפור הוא אחר.
שימו לב שג'אווהסקריפט לא סיפקה לנו כלי מפורש ליצירת אזור "private", אולם היא מספיק גמישה על מנת שנוכל ליצור אזור private בקלות וללא קוד מסובך.
יתרונות מבנה זה: בעזרת פעולה פשוטה למדי אנו יכולים להמנע מהתנגשויות של משתנים או פונקציות עם שם זהה.
חסרונות מבנה זה: הכמסה של "הכל או לא-כלום", קשה לעקוב אחר המשתנים הפנימיים בזמן Debug. כרגע - השגנו הכמסה עבור תכנות פרוצדורלי (כלומר רק פונקציות - לא אובייקטים).
Prototype
בפוסט הקודם הזכרתי את המשתנה שנמצא על כל אובייקט ב JavaScript - הרי הוא ה __proto__, או בשמו המלא: ה Prototype. לג'אווהסקריפט יש מודל ייחודי לניהול אובייקטים שלא קיים בשום שפה נפוצה אחרת.
בעוד מפתחי ג'אווה עשויים להיחרד מפרטי המנגנון, מפתחי ג'אווהסקריפט וותיקים דווקא נוטים לאהוב אותו ואת הגמישות הרבה שהוא מספק. אין ויכוח שג'אווהסקריפט היא "אלפא": השאלה האם מדובר ב"גרסת אלפא" קרי לא-יציבה ולא-סגורה, או "זן אלפא" - כלומר זן עליון. עניין של נקודות השקפה, אני מניח.
בואו נתבונן כיצד המילים השמורות prototype ו new בשפה מאפשרת לנו לנהל "אובייקטים":
var Calculator = function () { // constructor
this.value = 0;
};
Calculator.prototype = { // prototype = Object Literal
addBy: function (x) {
this.value += x;
console.log('value = ' + this.value);
}
};
// alternate way to define method, less recommended
Calculator.prototype.multiplyBy = function (x) {
this.value *= x;
console.log('value = ' + this.value);
};
ה Constructor הוא בעצם פונקציה רגילה לכל דבר ועניין. השימוש ב C גדולה הוא קונבנציה שמתארת שאני מייעד פונקציה זו להיות Constructor.this.value = 0;
};
Calculator.prototype = { // prototype = Object Literal
addBy: function (x) {
this.value += x;
console.log('value = ' + this.value);
}
};
// alternate way to define method, less recommended
Calculator.prototype.multiplyBy = function (x) {
this.value *= x;
console.log('value = ' + this.value);
};
הורשה בג'אווהסקריפט מבוטאת ע"י שרשרת של קשרי prototype - לכל אובייקט יש אב שמסומן ב property בשם __proto__. ב default זה יהיה האובייקט Object - האב המשותף לכל האובייקטים בשפה. המילה השמורה prototype מייצגת את המשתנה __proto__ (שלא אמורים להשתמש בו ישירות, אך הוא שימושי ל debug).
בשלב הבא אנו מחליפים את ה prototype של הפונקציה Calculator (שהיה עד כה האובייקט Object) לאובייקט שאנו מגדירים (בעזרת תחביר של object literal). ההחלפה תשפיע על כל instance חדש שייווצר מ Calculator בעזרת המילה השמורה new.
לבסוף, אנו מוסיפים מתודה נוספת לאובייקט שנוצר.
כאשר אנחנו קוראים ל new Calculator מתרחשים 2 דברים:
א. הפונקציה תחזיר כערך החזרה את this - מצביע לאובייקט חדש שנוצר.
ב. בתוך הפונקציה, הערך this יתייחס לאובייקט החדש שנוצר (נשמע פשוט, אך בג'אווהסקריפט זה לא מובן מאליו)
לאחר הגדרה זו, אנו יכולים לייצר instances ולהפעילם בצורה הבאה:
var calc = new Calculator();
calc.addBy(4);
calc.multiplyBy(6);
> 24
calc.addBy(4);
calc.multiplyBy(6);
> 24
בואו נסכם מה קיבלנו:
- יצרנו מעין "מחלקה"* שניתן לייצר instances שונים שמתנהגים אותו הדבר. השימוש במילה "מחלקה" היא הרגל מג'אווה, הוא לא מדויק כאשר אנחנו מדברים על ג'אווהסקריפט. בעצם יצרנו אובייקט Prototype (אב-טיפוס) שבקריאה ל new ייווצר instance חדש שלו. סלחו לי אם אני ממשיך להשתמש ב"מחלקה" - הרי זה פוסט למפתחי ג'אווה / #C.
- אנו יכולים להוסיף דינמית ל"מחלקה" זו מתודות או משתנים ע"י שימוש במילת ה prototype. האמת, כל אחד יכול. גם מפתח אחר שכותב קוד בקובץ אחר משלכם ומודע למחלקה בדרך-לא-דרך.
- חשוב!: השימוש ב this בתוך המתודה, כמו addBy, הוא חיוני על מנת לגשת ל instance / אובייקט.
- הקוד של פונקציות (addBy) נטענות לזיכרון פעם אחת בלבד. זה עשוי להישמע מוזר, אך במבנים אחרים, שנגיע אליהם עוד מעט, הגדרת הפונקציה תוכפל בזיכרון עבור כל אובייקט שנייצר. ההשלכה היא זמן יצירה ארוך יותר של אובייקטים ותפוסת זיכרון גדולה יותר - בעיה משמעותית אם אנו עומדים לייצר מאות או אלפי אובייקטים מאותו הסוג.
- בעזרת השימוש ב Object Literal ה"מחלקה" מוגדרת בשני חלקים. ניתן גם להגדיר על פונקציה כהשמה חדשה ל Prototype וכך ליצור אובייקט שהגדרתו אינה בהכרח רציפה בקוד. רציפות זו כמובן רצוייה - ועל כן צורת ה Object Literal נראית לי עדיפה.
היכולת לשנות בזמן ריצה, בעזרת המילה השמורה prototype, מחלקות אינה מוגבלת למחלקות שכתב המשתמש. אדרבא, ניתן לבצע שינויים בכל זמן ועל כל מחלקה, גם מחלקות של השפה עצמה כמו String או Object. ניתן לשנות את Function - האב של כל הפונקציות, כפי שנעשה בדוגמה שהצגתי בתחילת הפוסט הקודם.
למרות שכמה ספריות נפוצות (למשל Prototype.js) עושות תרגילים שכאלו - זו נחשבת התנהגות לא רצויה ולא מומלצת בעליל. כדאי להימנע ממנה לגמרי.
רק להזכיר סיבוך אחד אפשרי: בג'אווהסקריפט אובייקטים הם "שקים" של properties (מעין HashTable או Map) - ולעתים קרובות אנו מתייחסים אליהם ככאלו. כל אובייקט יציג את ה properties שהוגדרו עליו ישירות וגם על כל שרשרת ה prototypes שהוא קשור בה. שינוי (הוספה / מחיקה) של property לאחד ה prototypes - יכול לשבור קוד קיים במספר תסריטים. ההמלצה היא לנקוט באחת משתי גישות:
- בכל גישה ל property של אובייקט - לבדוק שה property באמת שלו (קריאת hasOwnProperty)
- להימנע לחלוטין משינויים ב prototypes מלבד הגדרת ה"מחלקה".
אתם בוודאי מנחשים איזו גישה קלה יותר ליישום.
הרצון באובייקטים+הכמסה = Module
עד עכשיו השגנו או הכמסה, או אובייקטים - אך לא את שניהם ביחד. בואו נבחן מבנה שיאפשר לנו לקבל את שניהם:
var Calculator = function () {
// private members
var value = 0;
return {
// public members
addBy : function (x) {
value += x;
console.log('value = ' + value);
},
multiplyBy : function (x) {
value *= x;
console.log('value = ' + value);
}
};
};
var calc = new Calculator();
calc.addBy(4);
calc.multiplyBy(6); // 24
הבסיס כאן הוא דיי פשוט: נשתמש ב Closure על מנת לבצע הכמסה ונשתמש בערך החזרה מסוג Object Literal על מנת לאפיין את המחלקה.// private members
var value = 0;
return {
// public members
addBy : function (x) {
value += x;
console.log('value = ' + value);
},
multiplyBy : function (x) {
value *= x;
console.log('value = ' + value);
}
};
};
var calc = new Calculator();
calc.addBy(4);
calc.multiplyBy(6); // 24
הפונקציה Calculator (=קונסטרקטור) מגדירה משתנים ופונקציות בתוך עצמה, בתוך ה Closure. בנוסף היא מייצרת אובייקט (בעזרת Object Literal) שחוזר למי שקורא לה - במקום this. לא ציינתי קודם, אך קריאה ל new תחזיר את this רק במידה ולא הוגדר return value מפורש. אם הוגדר return - אזי הוא מה שיחזור.
שימו לב שבעזרת החזרה זו נוצר ההבדל מהדוגמה הקודמת: הערך החוזר מהקונסטרקטור הוא לא instance של הפונקציה Calculator עצמה, אלא אובייקט שאנו יצרנו ואנו שולטים בו. אובייקט זה מייצג את החלקים ה public של האובייקט שלנו. כמובן שבג'אווהסקריפט אין type safety ואין שום בעיה שקונסטרקטור יחזיר אובייקט מ"טיפוס" אחר.
כיוון שיש לנו פונקציה בתוך פונקציה, ויש reference מהפונקציה addBy למשתנה value - מובטח לנו שהמשתנה value ימשיך לחיות גם לאחר שהפונקציה Calculator הסתיימה. אם אתם לא זוכרים מדוע - חזרו להסבר על Closure בפוסט הקודם.
באופן זה, מי ששמר Reference לאובייקט שחזר יכול לבצע פעולות על האזורים הציבוריים, בעוד האזור הפרטיים "סגורים" בתוך ה Closure. עצם ההצבעה של ה Object Literal שחזר אל המשתנים הפנימיים - שומר על אורך חייהם.
שימו לב שמבנה זה, בניגוד לקודם, אין להשתמש ב this כדי להתייחס למשתנים של ה Closure.
החיסרון העיקרי כאן, שקשה לשים אליו לב, הוא שפונקציות שמוגדרות בחלק הפרטי של האובייקט יהיו מוגדרות מחדש בכל קריאה ל new. המשמעות היא שכל אובייקט מסוג Calculator יאכסן בזיכרון עותק של קוד הפונקציות addBy ו multiplyBy. במקרים רבים שווה לספוג עלות זו על מנת להשתמש במבנה חביב זה.
מבנה זה הוא דיי מקובל ונקרא "Module".
הנה וריאציה קצת שונה של Module:
var myNS = myNS || {};
myNS.Calculator = function () {
// private members
var value = 0;
var addBy = function (x) {
value += x;
console.log('value = ' + value);
};
var multiplyBy = function (x) {
value *= x;
console.log('value = ' + value);
};
return { // public parts (aka interface)
addBy : addBy,
multiplyBy : multiplyBy
};
};
var calc = new myNS.Calculator();
calc.addBy(4);
calc.multiplyBy(6); // 24
היתרון העיקרי בווריאציה זו בא לידי ביטוי כאשר הקוד ארוך יותר. במקום שהמקום הפיסי בקובץ בו מוגדרת הפונקציה הוא שיכתיב את היותה ציבורית או פרטית (התבוננו בדוגמה הקודמת), כאן ההחלטה לגבי "הציבוריות" נלקחת מבלי לחייב את גוף הפונקציה להיות כתוב במקום מסוים. סגנון זה יותר נקי ובדרך זו הרבה יותר קל לנו להבחין מה ציבורי ומה פרטי.myNS.Calculator = function () {
// private members
var value = 0;
var addBy = function (x) {
value += x;
console.log('value = ' + value);
};
var multiplyBy = function (x) {
value *= x;
console.log('value = ' + value);
};
return { // public parts (aka interface)
addBy : addBy,
multiplyBy : multiplyBy
};
};
var calc = new myNS.Calculator();
calc.addBy(4);
calc.multiplyBy(6); // 24
בנוסף עשיתי עוד שדרוג קטן והגדרתי את המודול בתוך namespace. באופן זה אני מצמצם משמעותית את היכולת של ה Constructor להידרס ע"י מפתח אחר שגם במקרה בחר בשם Calculator. ייתרון נוסף ב namespace הוא ב Debugging, כאשר אוכל למצוא בקלות את המשתנים שלי בתוך ה namespace - ולא בערמה אחת עם כל משתני המערכת. ייתרון זה בא לידי ביטוי במיוחד כאשר יש כמה מפתחים על אותו הקוד. עדיין במודול יש Closure ומשתנים פרטיים של Closure לא נראים ברוב ה debuggers ללא breakpoint בתוך הקוד של יצירת ה Closure. לא נורא.
שימו לב שעלי להגדיר את myNS.Calculator ללא var - מכיוון שבג'אווהסקריפט אסורות הגדרות של משתנה עם "." בשם. הגדרתי את myNS מבעוד מועד - וחסכתי בעיות. ה namespace נדרש בקריאה לקונסטרטור - אך זו תוצאה רצויה ע"מ לוודא שאני באמת קורא לקונסטרקטור שלי, ולא של מישהו אחר.
באופן אישי, מודול נראה לי הדרך המועדפת ל"תיאור מחלקות ב JavaScript", החיסרון העיקרי הוא שכל אובייקט מכיל עותק של הפונקציות בזיכרון. לא נורא אם לא מדובר בהרבה אובייקטים.
כמה משחקים אחרונים
דוגמת ה prototype וה Module הן כנראה הדוגמאות הנפוצות. בכל זאת - לא תמיד הן יהיו בדיוק כפי שהצגתי. למתכנת יש יכולת לבצע עליהן וריאציות שונות שמשנות מעט את ההתנהגות. על מנת לחדד את החושים שלכם להבדלים הקטנים הללו - אציג כמה "מיני וריאציות" של ה Module וה Prototype. כשתתקלו בקוד אמיתי, דוגמאות אלו יכינו אתכם לכך שיש להתבונן בכמה פרטים ולא ישר להניח שאם אתם מזהים את המבנה הכללי - אזי אתם יודעים בדיוק במה מדובר.
כמעט מיותר לציין שחלק גדול מהקוד שכתוב ב JavaScript הוא:
א. מבנים פרימיטיביים ולא אופטימליים (אם ניתן לקרוא לזה מבנים)
ב. Copy-Paste של מבנים שחשבו עליהם - אך מי שהעתיק לא צלל למשמעות המלאה של המבנה. JavaScript Kiddies.
בואו נתבונן על הווריאנט הבא:
var calculator = function () {
// private members
var value = 0;
var addBy = function (x) {
value += x;
console.log('value = ' + value);
};
var multiplyBy = function (x) {
value *= x;
console.log('value = ' + value);
};
return { // public parts (aka interface)
addBy : addBy,
multiplyBy : multiplyBy
};
}();
// private members
var value = 0;
var addBy = function (x) {
value += x;
console.log('value = ' + value);
};
var multiplyBy = function (x) {
value *= x;
console.log('value = ' + value);
};
return { // public parts (aka interface)
addBy : addBy,
multiplyBy : multiplyBy
};
}();
שמתם לב להבדל? הוא קטן למדי ולכן הדגשתי אותו ב Bold.
הסוגריים בסוף הביטוי גורמים לכך שהמשתנה calculator מכיל את תוצאת הרצת הפונקציה ולא מצביע לפונקציה, כפי שהיה קודם. לכן, ע"פ קונבנציה, יש לקרוא לו calculator ב c קטנה.
האם אתם יכולים לעצור לרגע ולחשוב מה ההשלכות של שינוי זה?
כיצד תתבצע קריאה ל"מחלקה" שכזו?
ובכן, Singleton הוא דיי נפוץ בפיתוח אפליקציות Client Side. השינוי שביצענו בעצם הוא דרך לבטא Singleton. בניגוד לצד השרת בו דיי נדיר למצוא Singleton ברמת שפה - אנו לרוב מגדירים singleton ברמת הDependency Injection Framework או Service Layer. בג'אווהסקריפט לא נראה לי שיש דDI או Service Layer ואנו מגדירים Singleton ברמת השפה.
הקריאה לקוד, אם כן, תראה משהו כזה:
calculator.addBy(4);
calculator.multiplyBy(6); // 24
calculator.multiplyBy(6); // 24
ניתן גם לממש Singleton בצורה יותר "קלאסית" (דומה לג'אווה): לאכסון את המצביע ל instance היחיד על ה prototype ולהחזיר אותו בפונקציית getInstance. אני חושב שהדרך שהצגתי פה היא משמעותית יותר נפוצה בעולם הג'אווהסקריפט. שימו לב לדקויות כמו אם שם הפונקציה מתחילה ב Capital Letter או האם יש סוגריים של הפעלה בסוף הגדרת הפונקציה. פעמים רבות הסוגריים לא יהיה ריקים אלא יגדירו פרמטר שמועבר לקונסטרקטור - אולי ערך שמחושב בזמן יצירתה.
הנה מבנה קצת מורכב שמשלב אלמנטים שונים מהדוגמאות השונות:
var Calculator = function () { // constructor
// private fields
this._value = 0;
};
Calculator.prototype = function () {
// private functions
var _addBy = function (x) {
this._value += x;
console.log('value = ' + this._value);
};
var _multiplyBy = function (x) {
this._value *= x;
console.log('value = ' + this._value);
};
return { // interface
addBy : _addBy,
multiplyBy : _multiplyBy
};
}();
var calc = new Calculator();
calc.addBy(4);
calc.multiplyBy(6); // 24
במה מדובר פה?// private fields
this._value = 0;
};
Calculator.prototype = function () {
// private functions
var _addBy = function (x) {
this._value += x;
console.log('value = ' + this._value);
};
var _multiplyBy = function (x) {
this._value *= x;
console.log('value = ' + this._value);
};
return { // interface
addBy : _addBy,
multiplyBy : _multiplyBy
};
}();
var calc = new Calculator();
calc.addBy(4);
calc.multiplyBy(6); // 24
האם אתם יכולים לעצור לדקה, לקרוא את הקוד, ולחשוב מה המשמעות שלו?
חזרנו למבנה מפוצל של קונסטרקטור לחוד וגוף האובייקט לחוד - שזה פחות טוב. מצד שני, בזכות השימוש ב prototype - החלק השני שמכיל את הפונקציות לא ישוכפל לכל אובייקט. יש לנו גם הכמסה והפרדה בין private ל public.
המבנה הזה בעצם מחלק את ה"מחלקה" ל2 חלקים:
קונסרקטור - שמגדיר את החלקים שנרצה לשכפל, קרי משתנים של האובייקט.
פרוטוטייפ - שמגדיר את החלקים שנרצה להגדיר פעם אחת, קרי הפונקציות.
המשתנים שמוגדרים בתוך הקונסטרקטור צריכים להיות מוגדרים ביחס ל this - כך בפעולת new הם יהיו משויכים ל instance שנוצר. ללא this הם פשוט "ימחקו" בסוף הרצת הקונסטרקטור - לא דבר שאנחנו רוצים.
גם ההתייחסות בחלק הפרוטוטייפ צריכה להיות ל this - מסיבה זהה.
this בג'אווהסקריפט מתנהג בצורה עקלקלה. על מנת להבין אותו לעומק כדאי לקרוא את הפוסט שעוסק ב this, בהמשך הסדרה.
מה הלאה?
זהו. סיימנו רק 2 פוסטים בנושא, אך אני מרגיש כאילו עברנו כברת דרך משמעותית בדיון על ג'אווהסקריפט. מלבד הכרת הדפדפן, ה DOM וספריות כגון jQuery (שמכילות המון פרטים שאפשר ללמוד), נראה לי שהעיסוק בניתוח מבנים בג'אווהסקריפט הוא הקפיצה הגדולה בהבנת השפה ואופייה.
גם בשפת JavaScript עצמה יש עוד לא מעט ללמוד. אם שרדתם את המדריך הזה הייתי ממליץ להמשיך לאחד או יותר מהמקורות הבאים:
- JavaScript Garden - ריכוז מצוין של נושאים מבלבלים / בעייתיים בג'אווהסקריפט. אתם תזהו כמה דוגמאות שלקחתי משם וקצת פישטתי.
- Learning Advanced JavaScript - מדריך מאת ג'ון רזיג לקידום ספרו "סודות הג'אווהסקריפט נינג'ה". אם אתם זוכרים את קוד ה C-Syntax הלא ברור בעליל בתחילת הפוסט הראשון - מדריך זה הולך צעד אחר צעד להסביר אותו - ואתם אמורים להיות מוכנים "לרוץ" עליו.
- Learning JavaScript Design Patterns - מדריך קצת יותר ארוך שעוסק במבנים בג'אווהסקריפט, גם הוא כאמצעי לקידום ספר שיצא בקרוב. כל עניין ונושא בשפה (למשל namespace) מוגדר במדריך זה כ "pattern" - אבל נו טוב, אני מניח שככה מוכרים הרבה עותקים של ספר תכנות.
אם אתם רוצים לבדוק את הקוד ולבצע debug בסביבה קצת יותר רצינית מהדפדפן, אז שווה לנסות את jsFddle או ישר לקפוץ ל IDE מלא כמו Netbeans, Aptana או WebStorm.
הערות / השגות / מחשבות, כרגיל, יתקבלו בשמחה.
שיהיה לכם המון בהצלחה!
פוסט מעולה כרגיל. תודה.
השבמחקלאחר שקראתי על הפינות האפלות וההתנהגויות הלא צפויות (אלא לאחר טיפוס ארוך בעקומת הלמידה התלולה) עולה השאלה: האם לא כדאי לדלג על כל הבלאגן ולהתחיל לכתוב ישר בשפות כמו קופי-סקריפט או דארט? הרי ניתן לקמפל אותן לג׳אווה-סקריפט וכך להנות מכל היתרונות רק עם פחות הפתעות.
היי משה,
השבמחקתודה על התגובה.
הנקודה שאתה מעלה היא מצויינת, הנה מה שיש לי לומר:
על CoffeeScript:
כשהתחלתי לעסוק ב Client Side עשיתי את אותו שיקול שאתה מציין ו"קפצתי" ישר ל CoffeeScript. לקח לי כמה שעות של למידת CoffeeScript להבין שחסרה לי הבנה ב JavaScript. קופיסקריפט היא Productivity Tool מעולה - אבל לא מצליחה "להעלים" את המוזריות של JS לגמרי. זה לא מפתיע כי הרי היא מתקמפלת ל JS. למרות שיש היום Source Maps (כלומר, ניתן לדבג קופיסקריפט בדפדפן) - הדפדפן לא מודע אליה ולעיתים שווה לצלול לתוצר ה JavaScript.
בנוסף, כאשר אני עובד ב WebStorm IDE - לג'אווהסקריפט יש הרבה כלים שאין לקופיסקריפט ובהחלט אני מרגיש בחסרונם. לסיכום: נראה לי ש CoffeeScript היא טובה למפתחי JavaScript אך לא מספיק מנותקת ע"מ שתוכל לוותר על הבנת השפה - לפחות ברמה מסויימת. דיי מזכיר לי כלי WYSIWYG שבהם הסיפור דומה.
לגבי DART:
גוגל עצמה מתארת את DART כ pre-Alpha, עובדה שחשוב לקחת בחשבון. אמנם ל DART נראית משמעותית יותר עצמאית מ CoffeeScript, אך עדיין קשה לי לומר אם ניתן לפתח בה ללא ידיעת JavaScript. היה לי נסיון דיי משמעותי ב GWT - וגם היא לא הצליחה להגן עלינו בפני שפת JAvaScript.
אם למישהו יש ניסיון מעשי ב DART והוא יוכל להעיר - אשמח לשמוע. בנתיים אני מניח שגם ב DART כדאי שיהיה מישהו או שניים בצוות שמבין JavaScript.
ליאור