מבוא מואץ ל JavaScript עבור מפתחי Java / #C מנוסים – חלק 2

בפוסט זה אני רוצה להתמקד במבנים שגורים בג'אווהסקריפט והבנה של המשמעות שלהם בפועל: אילו 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:

(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);
}());

המבנה של קוד זה עשוי להראות מעט מוזר למפתחי ג'אווה – אך הוא שימושי למדי!
הפונקציה העוטפת משמשת לצורך יצירת 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.

הורשה בג'אווהסקריפט מבוטאת ע"י שרשרת של קשרי 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
  • יצרנו מעין "מחלקה"* שניתן לייצר 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 על מנת לאפיין את המחלקה.
הפונקציה 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

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

בנוסף עשיתי עוד שדרוג קטן והגדרתי את המודול בתוך 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
  };
}();

שמתם לב להבדל? הוא קטן למדי ולכן הדגשתי אותו ב 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

ניתן גם לממש 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

במה מדובר פה?
האם אתם יכולים לעצור לדקה, לקרוא את הקוד, ולחשוב מה המשמעות שלו?

חזרנו למבנה מפוצל של קונסטרקטור לחוד וגוף האובייקט לחוד – שזה פחות טוב. מצד שני, בזכות השימוש ב 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.
הערות / השגות / מחשבות, כרגיל, יתקבלו בשמחה.שיהיה לכם בהצלחה!

מבוא מואץ ל JavaScript עבור מפתחי Java / #C מנוסים – חלק 1

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

שייך לסדרה מבוא מואץ ל JavaScript ו jQuery

 

למה JavaScript?

חלקכם אולי עשוי לחשוב שג'אווהסקריפט היא היסטוריה ולכן – השקעה בלימוד השפה איננה פעולה משתלמת.
ג'אווהסקריפט אמנם איננה שפה חדשה (היא הופיעה ב95' ביחד עם ג'אווה) – אך בעוד ג'אווה בדעיכה, ג'אווהסקריפט תופסת תאוצה. אני רואה לכך כמה סיבות:

  • בעוד ה JVM של ג'אווה רץ על "כל שרת" – עובדה שסייעה לג'אווה להפוך למאוד נפוצה, ג'אווהסקריפט יכולה לרוץ על כל פלטפורמות ה JVM (בעזרת Rhino) ובנוסף – גם על דפדפנים ועל iOS -> שתי פלטפורמות אסטרטגיות שחשיבותן רק הולכת ועולה. תאהבו את זה או לא, אבל ג'אווהסקריפט היא שפת התכנות האוניברסלית ביותר שקיימת כיום.
  • ג'אווהסקריפט היא "מונופוליסטית": אין שפה אחרת שנתמכת ע"ג כל הדפדפנים החשובים.
    בניגוד ל JVM וה CLI שמריצים ByteCode ולא מודעים לשפה שבהם ה ByteCode נכתב (וכך נפתח פתח ל JRuby, סקאלה ועוד) JavaScript היא שפה מפוענחת (interpreted) והמנועים של ג'אווהסקריפט שפרוסים על הדפדפנים יודעים להריץ רק אותה.
  • ג'אווהסקריפט טובה לבניית UI. בניגוד לג'אווה שבמשך שנים ניסתה וניסתה – אך נכשלה, בג'אווהסקריפט ניתן לכתוב UI יפה ובצורה קלה. (על בסיס HTML ו CSS – כמובן).
  • לג'אווהסקריפט יש בסיס מתכנתים גדול. הרבה מפתחי #PHP, Ruby, C וג'אווה – יודעים גם קצת ג'אווהסקריפט. יש איזה ייתרון בלהיות שפה זקנה בת 17.
  • לג'אווהסקריפט אין הרבה מתנגדים. אין פה מלחמה בין אורקל למייקרוסופט, בין גוגל לפייסבוק וכו'. ג'אווהסקריפט נמצאת בעין הסערה.

ע"פ ה HTTP Archive, למשל, פרויקט שעוקב אחרת מגמות באתרי האינטרנט הגדולים, כמות הג'אווהסקריפט בשימוש באתרים שבמדגם זינקה ב 50% בשנה האחרונה בלבד.

כמה ג'אווהסקריפט יש בדפי אינטרנט של אתרים מפורסמים? הממ… זה גדל! מקור: pingdom.com

כיצד לומדים ג'אווהסקריפט?

יש 3 טעויות שמפתח ג'אווה ותיק עלול לעשות לגבי שפת ג'אווהסקריפט. אני, לפחות, עשיתי את כולן:

טעות מס' 1: "אני מכיר את ה Syntax". זהו C-Syntax. אני אסתדר Along the way.

המציאות: Java ו JavaScript דומים אחד לשני כמו "Car" ו "Carpet": בעיקר בשם.
הקוד הבא אכן נראה מוכר למדי:

for (i = 0; i< set.length; i++) {
  set[i].doSomething();
}

האם אתם יכולים לומר בוודאות באיזו שפה כתוב קוד זה? אני בספק.

אם אתם מדמיינים כך את הג'אווהסקריפט שתתקלו בו – אתם טועים. מהר מאוד תתקלו בביטויים כמו prototype, הסימן $ שמופיע המון, משתנים עם קווים תחתונים __ ובכלל – סגנון זר ולא מוכר. זהו אמנם C-Sytnax – אך יש בו מספיק אלמנטים לא מוכרים על מנת לבלבל אתכם לגמרי.

למשל, האם אתם יכולים להסביר מה עושה הקוד הבא (שכתוב ב C-Syntax)?

Function.prototype.bind = function(){
  var fn = this, args = Array.prototype.slice.call(arguments), object = args.shift();
  return function(){
    return fn.apply(object, args.concat(Array.prototype.slice.call(arguments)));
  };
};

טעות מס' 2: לחפש דוגמאות קוד והסברים באינטרנט.

המציאות: פעם סיפרתי איך שמם הטוב של האקרים נהרס בגלל הפצה של כלים פשוטים לפריצה – שהמשתמשים בהם לא ידעו כמעט כלום על Hacking באמת. ההאקרים קראו לאלה Script Kiddies – מריצי סקריפטים.
בעולם הג'אווהסקריפט יש תופעה דומה. המון ממשתמשי הג'אווהסקריפט הם לא מתכנתים מקצועיים – אלא משתמשים לא מקצועיים ש"בונים אתרי אינטרנט". אני קורא להם: ה-JS Kiddies.
התוצאה היא שיש המון חומר ברמה נמוכה באינטרנט, בפורומים ובכלל. אם תחפשו סתם כך בגוגל יש סיכוי גבוה למדי שתקבלו עצות חלקיות, תקראו tutorials שלא מגרדים את עומק השפה ותראו דוגמאות קוד לא מקצועיות.

קחו לדוגמה את האתר W3Schools – אתר reference לטכנולוגיות ווב המוכר. לאתר, מסתבר, אין שום קשר ל W3C והוא התבקש ע"י ה W3C, כמה פעמים, לשנות את שמו. מדוע?
האתר כולל שגיאות גסות בנוגע ל HTML, CSS וג'אווהסקריפט בכלל. אמנם הוא עושה רושם רציני – אך העובדות מדברות בשם עצמן. W3Fools – אתר שאוסף (חלק מ-) השגיאות וחוסרי הדיוקים של W3Schools, ממליץ למשל לחפש בגוגל עם "w3schools-", כלומר: רק תוצאות שלא כוללות את W3Schools. בהסתמך על הפופולאריות של W3Schools וכמות הפעמים שהוא מופיע בגוגל – זו עצה לא כל-כך רעה…

טעות מס' 3: "הבנתי. משהו פה לא עובד. נקרא את התיעוד הרשמי מקצה-לקצה כך שלא יהיו לנו הפתעות".

המציאות: כשלמדתי Java באמת הלכתי ל Spec – אך זו הייתה שפה ראשונה והייתי סטודנט צעיר. כשניסיתי לקרוא את התיעוד המומלץ של ג'אווהסקריפט – מצאתי את עצמי משתעמם מהר מאוד. רוב מה שכתוב שם הוא ברור מאליו למי שמכיר את ג'אווה או #C לעומק.

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

אם אתם רוצים לכתוב בדפדפן, ידיעת ג'אווהסקריפט היא רק חלק מהתמונה. כנראה שתרצו ללמוד גם jQuery לעומק (סימן ה $ שהזכרתי קודם), קצת על ה DOM ולהכיר ספריות רלוונטיות ושימושיות.

בהמשך הסדרה אני עוסק ב jQuery, מקור מקיף לגבי ה DOM הוא DOM Enlightenment. עוד חומר על פיתוח ווב תוכלו למצוא באתר העברי אינטרנט-ישראל.

כמה הבדלים בסיסיים בין JavaScript וג'אווה:

הערה: אני מקצר וכותב "ג'אווה" בלבד, אך מה כתוב כאן רלוונטי באותה מידה ל #C.

Scope
בג'אווה, Scope נוצר ע"י סוגריים מסולסלים, יהיה זה משפט if, לולאת for או סתם סוגריים שהוכנסו בתוך הקוד.
בג'אווהסקריפט Scope נוצר רק על ידי פונקציה. הנה דוגמה להתנהגות הצפויה (מקור: javaScriptGarden):

function test() { // a scope
  for(var i = 0; i < 10; i++) { // not a scope
    // do something
  }
  console.log(i); // 10
}

Global Scope
מה שלא מוגדר בתוך ה Scope של הפונקציה – שייך ל Scope הגלובלי. דריסות הדדיות הן תקלה שכיחה.

// Defined in Global Scope
foo = '42';

// Defined in "current" scope. Either function or global
var foo = '42';

קצת יותר גרוע היא העובדה שאם הגדרתם משתנה בתוך פונקציה ללא המילה השמורה "var" – אותו משתנה יוגדר אוטומטית ב Scope הגלובלי.
שכחתם? התבלבלתם? בזמן Refactoring מחקתם את ה "var" – בעיה שלכם.

הנה דוגמה מרגיזה: כתבתם אפליקציה לניהול חתונות. נשיא וארה"ב ואשתו אישרו הגעה. הפקיד ראה הודעות Alert ברורות של אישור ההשתתפות על מסך-המחשב, אבל המקומות לא נשמרו והם נאלצו לאכול בעמידה (כלומר, הורי החתן נאלצו לאכול בעמידה – כמובן).

weddingParticipantsCounter = 100;

function acceptParticipation(name) {
  weddingParticipnatsCounter = weddingParticipantsCounter + 1;
  alert('Registration Accepted: ' + name);
}

acceptParticipation("Mr. President");
acceptParticipation("The First Lady");
console.log(weddingParticipantsCounter); // -> 100?!

האם אתם יכולים להסביר מה השתבש? [א]

איך מתמודדים עם pitfall שכזה שבנוי בתוך השפה? כלי וריפיקציה בשם JSLint יספק הזהרה, אם אתם עובדים ב IDE שמריץ אותו תוך כדי כתיבה – יש סיכוי שתצליחו להימנע מכמה תקלות. הבעיה ב JSLint (או חברו JSHint) הוא שההזהרות שהם מספקים נכונות רק ל 90% המקרים ואתם נשארים עם רשימה של Warnings לא מטופלים – גם כאשר יש לכם קוד נפלא.
CoffeScript היא דרך נוספת להתמודדות. הוא לדוגמא מוסיף var מאחורי הקלעים לכל הגדרה.

Hoisting
חשבתם שהדברים מסתדרים? שימו לב לכלל הבא: הקוד בג'אווהסקריפט לא ירוץ ע"פ הסדר שבו הוא נכתב.
אני חוזר: הקוד בג'אווהסקריפט לא ירוץ ע"פ הסדר שבו הוא נכתב.

הגדרות var והגדרות של פונקציה, רצות לפני כל שאר הקוד. זה כאילו ה interpreter מחפש את כל ההגדרות הללו ומעביר אותן לראש ה scope הנוכחי, ורק אז מריץ את הקוד. תהליך זה נקרא Hoisting.
Hoisting הרבה פעמים עוזר, וברוב המקרים הגדול לא יהיו איתו בעיות. אבל ברגע שהבעיות יקרו, הן יהיו מאוד קשה לאיתור. עצם העובדה שניתן לכתוב הרבה קוד שעובד כהלכה ללא המודעות לתהליך ה hoisting – מטשטשת את החושים לסכנות שהוא מציג.

התבוננו על דוגמת הקוד הבאה:

var n = 1;

function foo() {
    if (false) { var n = 10; }
    console.log(n);
}

foo();

?מה אתם מצפים שתהייה התוצאה שתכתב ללוג?

זו כמובן דוגמה מופשטת שיכולה לתאר מצב אמיתי ומורכב הרבה יותר. דמיינו את הקושי לאתר תקלות שכאלו בקוד של עשרות של שורות עם לוגיקה מורכבת.אני מניח שהעובדה שללוג נכתב הערך "undefined" – עלולה להיות מבלבלת. "undefined" הוא ה null של ג'אווהסקריפט (יש גם null, אבל לא משתמשים בו הרבה. הוא כמו "ערך נוסף" שמשתמשים בו בכמה מקרים מסוימים – אך כ"קונבנציה". נראה שניתן היה להשתמש ב "undefined" במקרים אלו באותה המידה [ב]).

הקוד שבעצם רץ, לאחר פעולת ה hoisting, הוא זה:

var n;

function foo() {
    var n;
    if (false) { n = 10; }
    console.log(n);
}

n = 1;
foo();
הגדרת המשתנה n, גם בתוך הפונקציה, עולה למעלה. זכרו שהסוגרים המסולסלים של משפט ה if לא מגדירים Scope חדש. כחלק מחוקי ה hoisting, ההשמה נשארת במקומה.
n מוגדר ב scope של הפונקציה ולכן הוא "מחביא" את n שהוגדר ב global scope.
כיוון שהתנאי "false" לא מתרחש במקרה זה, n נשאר undefined עד לנקודה בה הערך נכתב ללוג.
מה עושים?
שומרים במשמעת ברזל על הכלל הבא:
  • כל הגדרות ה var וה function יהיו תמיד בראש ה scope הנוכחי, אם זה הקובץ (גלובלי) או הפונקציה.
אחד הטיעונים שאני זוכר שהועלו כנגד שפת פאסקל הוא "למה אני צריך להגדיר את כל המשתנים בראש הפונקציה? למה הקומפיילר לא יכול לעשות את זה בשבילי?!". על טיעון זה בלבד אני זוכר כמה אנשים שהתרחקו מהשפה.
הכלל של הגדרת משתנים מראש הוא מובנה בשפה – ומתכנתים לומדים אותו בשיעורים הראשונים על השפה.בג'אווהסקריפט ניתן להגדיר משתנים בכל מקום – רק אחרי חודשים רבים של עבודה בשפה מגיעים להבנה שפשוט לא כדאי. השפה לא מספקת שום אזהרה או עצה. בכל זאת, לא שמעתי מתכנת ג'אווהסקריפט אחד מתלונן על הנושא הזה.

מה ההבדל? האם יכול להיות שההבדל הוא שבפאסקל נתקלים בעניין אחרי חצי שעה (אפס השקעה אישית) ובג'אווהסקריפט אחרי חודשים של השקעה אישית? אחרי שיש לכם כבר קוד ולמדתם כמה דברים?
האם שווה "למכור תמונה יפה" ולהשאיר את הצדדים הפחות יפים לאח"כ?

אם כן – זה יכול להיות Case Study טוב ב Pre-Sale. אני מניח שלא מעט תוכנות ארגוניות נהנות מהמנגנון הזה.

סביבת הרצה לג'אווהסקריפט אליה ניתן לגשת בקלות
כמפתחים מנוסים, אתם בוודאי יודעים שאם רק תקראו על שפת תכנות מבלי לנסות אותה – הידע לא יושרש היטב. חשוב "ללכלך את הידיים".

אני יודע (ע"פ סטטיסטיקות של Google Analytics) שאם אתם קוראים את הבלוג הזה יש סיכוי של 50% שאתם משתמשים בכרום, ועוד כ 20% שאתם משתמשים ב FireFox. לשני הדפדפנים הללו יש סביבת פיתוח לא רעה בכלל לג'אווהסקריפט שניתן להתחיל להשתמש בה מיד. אם אתם עדיין תקועים עם IE – השיגו לכם כרום. הגיע הזמן.הנה דוגמא פשוטה כיצד להריץ קוד ג'אווהסקריפט אם אתם רצים בכרום. אתם יכולים לעשות זאת מבלי לאתחל את הדפדפן. פשוט פתחו טאב חדש, לחצו על הכפתור הימני של העכבר בתוך הדף, בחרו את "Inspect Element" – כניסה מהירה לכלי הפיתוח של כרום, עברו ל Console והנה יש לכם Interpreter ג'אווהסקריפט איכותי:

הנה אני מקליד בו איזו פיסת קוד:

יכולת שאני אוהב היא להתבונן על אובייקטים בתוך ה console בכל פעם שהם חוזרים כערך. למשל אנחנו רואים שלאובייקט שיצרנו יש משתנה (סודי) בשם __proto__. הממ… מעניין מה זה אומר…

אובייקטים ופונקציות


Object Literal
{};

.כך יוצרים בג'אווהסקריפט אובייקט. זהו אובייקט ריק

בפועל אובייקט הוא רשימה של תכונות:

var myObj = { a: "Hello", b: "World" };
אם התחביר נראה לכם מוכר, נסו להיזכר מהם ראשי התיבות של JSON. הא!כבר נתקלנו בפונקציות, אך יש כמה עובדות חשובות נוספות שכדאי להכיר לגביהן.

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

function foo(text) { console.log(text); }
foo.x = 4;
foo(foo.x);

שידפיס כמובן את הערך 4.

Anonymous Functions
ג'אווהסקריפט בדפדפן היא שפה עשירה מאוד בשימוש בevents ולכן התחביר של Anonymous Functions הוא קצר ונוח. לדוגמה:
obj.doSomething(function(event) { console.log(event.getMessage()); });
הדרך המומלצת להגדרת פונקציות בג'אווהסקריפט היא להגדיר פונקציות אנונימיות ולבצע השמה למשתנה:
var foo = function(text) { console.log(text); };
בלינק זה תוכלו למצוא כמה דוגמאות מה יכול לקרות לכם אם לא תעשו זאת (מוהא-הא-הא!).
על מנת למנוע דריסה של פונקציות, אפשר להשתמש ב namespace:
var myNS = myNS || {}; // if namespace myNS exists - use it, else create a new namespace.
myNS.foo = function(text) { console.log(text); };

Closure

קלוזור הוא מונח שבוודאי שמעתם עליו, או שתשמעו עליו מיד שתתחילו להתעסק בג'אווהסקריפט. Closure בג'אווהסקריפט הוא Scope בתוך Scope, ומכיוון שבג'אווהסקריפט scope מוגדר רק ע"י פונקציה – הרי Closure הוא פונקציה בתוך פונקציה.

כאשר יש פונקציה בודדת, המשתנים שלה ימחקו מייד לאחר הרצת הפונקציה:

function simpleFunction() {
  var x = 4;
}
// x no longer exists
כאשר יש לי פונקציה פנימית, בתוך הפונקציה, שקוראת למשתנים – הם ישארו לחיות לעד. אני מנחש שזה עניין של pointer counting. הנה דוגמה:
function Counter(start) { // closure
  var count = start;
  return function() { // function with scope
      return ++count;
  }
}

var c = Counter(4);
// count still exists
c(); // 5
c(); // 6

המשתנה c מחזיק reference לפונקציה הפנימית של Counter, והיא מצידה מחזיקה reference למשתנה count – וכך הוא נשמר בחיים.

בואו נסתכל בדוגמא לבעיה שיכולה להיפתר ע"י שימוש ב Closure.

בדוגמת קוד תמימה זו, אני משתמש ב anonymous function מהסוג שהזכרנו קודם לכן על מנת לממש Listener. זוהי פעולה נפוצה למדי בג'אווהסקריפט. אני מגדיר פונקציה פשוטה למדי שתכתוב לערכים לlog.
התוצאה הצפויה היא, אם כן, ספירה מ 1 עד 15 ל log.

var listeners = []; // array

// fill up some listeners
for (var i = 0; i < 15; i++) {
  listeners.push(function() { console.log(i); });
}

// trigger an event to all listeners. Each one counts.
for (var j in listeners) {
  listeners[j](); // execute
}
התוצאה בפועל היא 15 פעמים המספר "15".
הסיבה לכך היא כזו: הפונקציה שנוצרת בתוך לולאת ה for מתייחסת למשתנה i, אבל i הוא משתנה בסביבה הגלובלית. נזכיר שסוגריים מסולסלים לא מייצרים scope. בנוסף – הפונקציה אינה מחושבת בזמן ההגדרה, אז בפועל נוצרות לנו במערך ה listeners חמש עשר עותקים של פונקציה שתוכנן הוא (console.log(i.כיוון שהן מופעלות רק בסוף קטע הקוד, i שהוא משתנה גלובלי – בעל הערך 15, כך שיודפס 15 פעמים הערך 15.
גם יצירה של 15 פונקציות בזיכרון הוא בזבוז לא קטן שהיינו מעדיפים להימנע ממנו.

הדרך לפתרון היא להוסיף Closure שישמור (או "יקפיא") את ערך המשתנה i, עבור הרגע בו הוגדרה הפונקציה:

var listeners = []; // array

function eventHandler(i) { // outer function = closure
  return function() { // inner function
    console.log(i);
  }
}

// fill in the listeners (syntetic)
for (var i = 0; i < 15; i++) {
  listeners.push( eventHandler(i) );
}

// trigger an event to all listeners. Each one counts.
for (var j in listeners) {
  listeners[j](); // execute listener
}

בדוגמה זו, הפונקציה eventHandler מבוצעת בתוך הלולאה. הערך i עובר אליה כפי שהוא, ואז נלכד "לנצח" בתוך ב closure. טוב, לא ממש לנצח – אולי עד שנגלוש לדף ווב אחר.

הפונקציה שנשמרת במערך listeners היא כבר הפונקציה הפנימית והרצתה תגרום לכתיבת הערך המצופה, קרי 0 עד 14.

כיצד ניתן להבטיח שלא תפספסו מקרים פשוטים שכאלו? אם ניחנתם בתסמונת אספרגר – הסיכויים שלכם להתבלבל בפרטים טכניים שכאלו היא קטנה למדי. בכל מקרה אחר – אתם יכולים להשתמש ב JSLint שייתן אזהרה ברורה. אומרים שעם הותק בג'אווהסקריפט הטעויות הללו הולכות ונעלמות – אני עוד לא הגעתי לשם.

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

מקווה שנהניתם!

פוסט ההמשך בו אדבר על מבנים בג'אווהסקריפט ומשמעותם.

[א] קצת אכזרי, אך זו מהמציאות: weddingParticipnatsCounter = weddingParticipantsCounter + 1. ג'אווהסקריפט זיהה משתנה לא מוכר ובמקום להטריד את המתכנת – טרח והגדיר אותו בשבילו. איזה יופי!
IDE טוב היה מספק value highlighting שאולי, היה יכול לעצור מוקדם את התקלה.

[ב] האם זו טעות תכנונית שתעלה למשק העולמי 2 מיליארד דולר?