JavaScript ES6/7/8 – להשלים פערים, ומהר – חלק ג’ ואחרון

פוסט שלישי ואחרון בסדרה:

  • בחלק הראשון – כיסינו את let ו const, חידושים בגזרת הפונקציות, ו spread operator השימושי.
  • בחלק השני – כיסינו Classes ו Modules – וגם הזכרנו בקצרה יכולת ליבה חשובה בשם Promises.
בחלק הזה, נכסה עוד כמה אלמנטים בתחביר שעשויים להיות לא-מובנים, נכסה יכולות מתקדמות כמו Symbols ו Generators – ונסיים ב Async-Await – המנגנון שמפשט את השימוש ב Promises, וכנראה שנשתמש בו ברוב המקרים.
בואו נתחיל!

שימושים שונים ל [ ]

ב ES5 אנו רגילים לסוגריים המרובעים – כמגדירים של רשימה. ב ES6 נראה אותם בעוד כמה וריאציות. למשל:

אמנם Array בשפה הוא iterable, אבל כאשר אנו רוצים להפעיל לולאה עם האינדקס, אנו משתמשים בפונקציה בשם ()entries המחזירה iterator של האיברים עם האינדקס שלהם. כלומר: בכל אינטרקציה אנו נקבל מערך [index, item]. מכאן הכי טבעי להשתמש ב deconstructing assignment על מנת לקבל את הערכים.

שימו לב ש for x… of XList הוא פורמט חדש ללולאה, המבוססת על iterables. ברור.

Computed property names

אופן נוסף שבו נראה סוגריים מרובעים, הוא בהגדרת properties של אובייקט. ב ES6 ניתן לקבוע מפתח של property כתוצאה מחישוב:

  1. יצרנו לאובייקט תכונה ששמה מגיע ממשתנה חיצוני – ולכן עשוי להשתנות עם הזמן.
  2. הנה אנחנו ממש מפעילים חישוב כדי לבנות את שם ה property. אפשר ומותר.
  3. האובייקט שנוצר – מייצג את תוצאות החישוב.
    1. הנה אפשר לגשת לתכונות ששמן נוצר ע”י חישוב – באופן רגיל.
    2. ניתן גם להשתמש בתחביר הסוגריים המרובעים לשלוף תכונה מתוך האובייקט. האמת, שגם ב ES5 ניתן לגשת לתכונה עם שם דינאמי באובייקט, אם כי בצורה קצת אחרת: פשוט צריך להרכיב מחרוזת של השם שלה.
    3. התחביר של computer property name עובד רק ב context של אובייקט. ב contexts אחרים, לסוגריים מרובעים יהיו משמעויות אחרות.
אז מה בעצם השימוש לתחביר החדש הזה של computer properties? האם TC39 ניסו בכח לבלבל אותנו עם משמעויות שונות לסימנים מוכרים?!

אני מקווה שלא.

הנה שימוש טיפוסי אחד: יצירה דינאמית של אובייקטים (או Classes):

וריאציה נוספת, וחשובה יותר של השימוש – קשורה לאייטם הבא.

Symbols

ל ES6 נוסף טיפוס פרימיטיבי חדש בשפה, בשם Symbol.

אולי אתם מכירים Symbol מ Ruby, Elixier או Closure (או Scala. הכל יש בסקאלה). אמנם יש דמיון, אבל זה לא אותו הדבר. ב ES6 השימוש הוא שונה, ובעיקר סובב סביב הוספת metadata לאובייקטים, והוספת יכולות חדשות לשפה בצורה תואמת-לאחור.

נתחיל בדוגמה:

באובייקט customer1 הגדרנו 2 שדות: שם ומזהה לקוח. משום מה, שדה ה customerId לא זמין לרוב הפעולות בשפה – אנחנו רואים רק את השדה name. מוזר!

הפונקציה ()Symbol מייצרת עבורנו מופע חדש (וייחודי, Singleton) של symbol. המחרוזת שמועברת לה – משמשת כמזהה ל Symbol.

Symbol נשמע כ property “בלתי נראה” על האובייקט. לא ניתן property שהוא Symbol מעזרת מפתח שהוא מחרוזת, והוא לא יתנגש עם properties עם “שם” זהה (כפי שאמרנו, אין לו שם). אפשר לחשוב על Symbols כ properties ביקום מקביל.

על Symbol ניתן לבצע את כל הפעולות הרגילות על properties כגון הוספה ומחיקה – כל עוד מעבירים את ה symbol לפעולה (הוספה, מחיקה, וכו’). כל פעולה על Symbol דורשת התייחסות למופע של ה Symbol.

  1. אפשר לשלוף ערך מאובייקט – ע״פ ה Symbol. אם לא קיים – יחזור undefined.
  2. אפשר לקבל את כל שמות ה Symbols על אובייקט – בעזרת הפונקציה ()Object.getOwnPropertySymbols
  3. למרות שהגדרנו 2 symbols שונים עם אותו “מזהה” CUSTOMER_ID – הם שונים. המזהה שלהם הוא המופע של פרמיטיב ה Symbol – ויש לעשות בו שימוש-חוזר! עובדה: יש לנו 2 symbols עם אותו “מזהה” על האובייקט – אבל הם בעצם Symbols שונים לחלוטין.
בלי להרחיב יותר מדי, אציין רק של Symbols יש global registry שדרכו אפשר לקבל את ה instance (הייחודי, singleton) של Symbol מסוים – מבלי ליצור תלויות של כל הקוד בקובץ יחיד. הפונקציה בעזרתה מקבלים מופע של Symbol נקראת (…)Symbol.for. לרוב מי שישתמש ב Symbols הם Frameworks. אם אתם רוצים להבין Symbols יותר לעומק – אני ממליץ על המאמר המקיף הזה.

שימוש חשוב נוסף של Symbols הם לספק שמות חדשים בשפה, שלא יתנגשו בשמות קיימים בקוד קיים. למשל: רצו להוסיף יכולת להגדיר אובייקטים כ Iterable ע”י מימוש של פונקציה עם שם מסוים – אך כל שם בעולם שיבחרו (כולל zz3pro) – עשוי להיות כבר בשימוש ע”י קוד ג’אווהסקריפט (זוכרים את חוק אטווד?).

הפתרון הוא בשימוש ב symbols – כך שלא תהיה התנגשויות. בכדי להגדיר אובייקט כ Iterable עלינו לממש פונקציה בשם ()[Symbol.iterator], כאשר Symbol.iterator הוא Symbol של השפה. פונקציות הרי רשומות כ property על האובייקט (או prototype) – וה property הזה יכול להיות גם Symbol.

Generators

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

Generators מייצרים פונקציות שלא מבוצעות במלואן בקריאה אליהן – אלה רק בסדרה של קריאות, בעצם סוג של iterator. הסימון שלהן בשפה: *function.

כלומר: Generator הוא פונקציה שמייצרת iterator.

מה השוני שלהן מ [Symbol.iterator] או מפונקציה שמחזירה פונקציה? יש לפונקציה שנוצרה ע״י Generator כמה תכונות מיוחדות ומתקדמות – המנוהלות ברמת המפרשן. השימוש העיקרי של Generators הוא לנהל בצורה קלה לקריאה סדרה של פעולות, בד”כ אסינכרוניות – בעלות קשר אחת לשנייה.

הנה תיאור ההתנהגות הבסיסית:

ה Generator מייצר את ה iterator מאחורי הקלעים. גוף הפונקציה שלו – היא בעצם ה template לקוד של כל iterator – כאשר המפרשן מנהל את ה state של ה iterator הזה: הוא זוכר היכן בדיוק עצרנו, ואת ערכי המשתנים ב scope הפונקציה באותה הנקודה (קרי: ה activation frame).

yield היא מילה שמורה חדשה בשפה – המשמשת את ה Generators. המשמעות שלה היא כמו return שגורם לשמירת ה state של ה iterator: כשנקרא ל ()next פעם נוספת – נמשיך בדיוק מהשורה הבאה אחרי זו שממנה יצאנו, ועם אותם המשתנים.

הנה דוגמה קצת יותר מציאותית:

  1. כשמגדירים generator מתוך מחלקה, אין צורך במילה function – נשארת רק הכוכבית (*).
  2. ה Generator בעצם מתאר רצף פעולות שנדרש כדי להשיג תוצאה כלשהי. במקרה שלנו, בכדי להגיע לפרטי התשלום של הלקוח:
    1. קודם צריך למצוא account token
    2. איתו לעשות login
    3. עם ה token הזה לשלוף את המידע על התשלומים.
כלומר ה generator מתאר את הקשר בין הפעולות והסדר שלהן – אבל הוא לא מבצע אותן. מי שיבצע אותן הוא מי שיקרא ל iterator, ויש פה הפרדה בין הגדרת סדר הפעולות (ה Generator) לביצוע שלהן (הפעלת ה iterator).
האם זה לא מעצבן לקרוא לפונקציה next, next, next בכדי שתסיים את הפעולה? למה לא לכתוב פונקציה רגילה שמבצעת ברצף את כל סדר הפעולות?
זו בדיוק הנקודה של generators. כאשר אנו עושים פעולות אסינכרוניות, יש טעם רב להתחיל פעולות במקביל. ה generator מאפשר לי להתחיל פעולה אסינכרונית ראשונה, לקבל חזרה שליטה, ולהיות מסוגלים להפעיל פעולות נוספות בין שלב לשלב בביצוע ה iterator. אפשרות נוספת שניתנת לנו: להתערב ברצף ההרצה.
כאשר אני מפעיל פונקציה – אין לי יכולת לשלוט בה, ולהתערב – עד שהיא מסתיימת.
תכונה חשובה של generators היא יכולת מובנה להתערב בשלבים. אם אנחנו שולחים ערך לפונקציה ()next – הערך הזה יחליף את הערך ב state של האיטרטור:
סה”כ generators נחשבים פעולה מעט low-level עבור רוב המפתחים, ורבים שכן עבדו עם generators מצאו את עצמם עוטפים את התשובות ב Promises. מנגנון בשפה בשם Async / Await מסתמך על מנגנון ה generators בכדי לתת לנו רמת הפשטה גבוהה ונוחה מאוד לשימוש – לביצוע רצף פעולות אסינכרוני.

Async – Await

בפשט אפשר לומר ש Async-Await הוא Syntactic Sugar שהופך את השימוש ב Promises לקל ואלגנטי יותר.
עוד נוסחה שמוזכרת הרבה היא ״Async-Await ≈ Generators + Promises״ – אל דאגה! השימוש ב Generators הוא פרט מימוש מאחורי הקלעים.

Promises הם בהחלט שיפור יחסית לעבודה עם callbacks – אבל כשהשימוש בהם נעשה רחב, התגלה שגם להם יש כמה נקודות לא-נוחות.
Async Await הוא שיפור נוסף מעל Promises. מתכנתי ג׳אווהסקריפט ותיקים בוודאי קצת מקנאים במתכנתים חדשים שנוחתים לכל הנוחות הזו…

תחילה ארצה להציג את 2 הבעיות העיקריות שב Promises. אציין ש Async-Await גם יותר יעיל ברמת הביצוע וצריכת הזיכרון – במידה ואתם מריצים המון קוד מקבילי.

טיפול בכישלונות – בשני אופנים שונים:

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

Control Flow נוטה להסתבך – החלוקה לפונקציות הנשלחות ל then

בעבודה עם Promises קל להגיע לקוד מהסוג הבא (דוגמה מפושטת):

אולי אני צריך לבצע קריאה אסינכרונית ואולי לא – קשה לי לשלב את המשך ה flow בצורה אלגנטית בלי להוציא חלק מהקוד לפונקציה שניה / אולי לעטוף קטע קוד סינכרוני ב Promise בשביל הסימטריה…

הנה אותו הקוד עם async-await:

בהחלט קוד יותר אלגנטי, ושיותר קל לעבוד איתו!

אם באמת זה נראה לכם קוד מוצלח יותר – אפשר לעבור ולהבין את התחביר. בואו נפרק אותו שלב אחר שלב:

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

את המילה השמורה await ניתן להציב רק בתוך פונקציה שהוגדרה כ Async.
הצבה שלה לפני Promise תגרום למפרשן להמתין עד של promise יש תשובה – ואז היא מחזירה אותה.

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

בעצם מאחורי הקלעים, המפרשן מייצר מפונקציית ה async מופע של Generator ומשתמש ב yield בכל נקודת await. ברגע שה Promise מוכן עם תשובה – הוא יקרא שוב ל Generator – שימשיך מהנקודה האחרונה.

זהו Syntactic Sugar לא קטן – ואין טעם לנסות לממש זאת לבד!

למרות ש await יוצר סדרתיות ברצת הפונקציה, עדיין אפשר להשתמש בכלים של promises על מנת להריץ קוד במקביל:

שימו לב שבדוגמה זו, לא עשינו כלום עם הערך שחזר מהביטוי “(…)await Promise.all”. אם קרתה שם תקלה – לא נדע ממנה. אם היינו משתמשים ב return לערך – תקלה הייתה מתרגמת ל Exception.

לסיום הנושא, ולחידוד כיצד מתנהגת “ההפסקה” של await – בואו נחשוב מה יהיה פלט התוכנית הבאה:

התשובה היא:

a start
b start
a end
b end

אנו נכנסים ל a ואז ל b – ונעצרים בביצוע הפונקציה b בגלל ה await.
אנו יכולים להמשיך, ואכן ממשיכים בביצוע הפונקציה a ומדפיסים “a end”. רק אז ה thread מתפנה לחכות ל await ולטפל בו – ואז לסיים את ביצוע הפונקציה b.

לו היינו מוסיפים ביטוי await גם על הפעלת הפונקציה b, התוצאה תהיה:

a start
b start
b end
a end

במקרה זה פונקציה a נערצת בקריאה “await b” והפונקציה b נעצרת קריאת ה await שלה.
רק לאחר שפונקציה b מסתיימת – ה await שבפונקציה a משתחרר – וממשיך לסיומה.

זה עשוי להיות מבלבל – אבל זו המציאות בעבודה עם Async-Await – שיש לשים לב אליה.

סיכום

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

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

שיהיה בהצלחה!

JavaScript ES6/7/8 – להשלים פערים, ומהר – חלק ב’

בהמשך לפוסט הקודם על ES6, אני רוצה להמשיך ולעזור לסגור פערים, למי שעדיין לא מכיר את העדכונים בשפה.נוספו הרבה Utilities לשפה ב ES6: מבני נתונים הם היום iterable, וניתן להפעיל עליהם פונקציות
()find(), sort(), filter(), foreach(), map וכו’

כל זה מאוד נחמד – אבל אלו אינם הנושאים עליהם אני רוצה לפרט. אני מניח שאתם מכירים את הנושאים הללו כבר – משפות אחרות. זה עובד ממש אותו הדבר.

יש עוד תוספות נחמדות לשפה: האובייקט String קיבל סדרה של מתודות שימושיות כמו ()startsWith או ()includes. מה שהייתם מצפים.

הספריות הסטנדרטיות  עברו מודרניזציה. נוספו יכולות internationalization כגון Intl.DateTimeFormat או Intl.NumberFormat. מי שזקוק בוודאי יבין מיד את היתרונות.

נוספו מבני נתונים כמו Map או Set.

כאן אולי עולה השאלה, למה צריך Map עם הקלות של ייצוג Dictionary על אובייקט {}?

בגדול Map הוא iterable וניתן לעשות על ה entries שלו פעולות כמו map/filter, ויש לו כמה פונקציות/תכונות שימושיות שלא זמינות לאובייקט. אבל הייתרון הגדול בשימוש ב Map לדעתי – היא בקריאות בקוד, כאשר שימוש ב Map מדגיש את הכוונה: פשוט לאחסן זוגות איברים, ותו לא.

מטרת הפוסט הזה, אם כן, היא לא להסביר יכולות בסיסיות של שפות תכנות מודרניות – את זה אתם כבר מכירים. ואם לא – אז ה IDE ינגיש לכם אותם ב autocomplete או שתמצאו בקלות תשובה בגוגל.

מטרת הפוסט היא להתמקד בתחביר חדש / ייחודי ב ES6 שקשה להבין לבד.
לספק לכם כלים להבין את התחביר המורכב, ומה שמתרחש מאחוריו.

נצא לדרך!

השלמות

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

האם קטע קוד הבא מובן לחלוטין? נגענו ברוב האלמנטים – אבל לא חיברנו אותם לגמרי:

בעצם אנחנו יכולים להשתמש ב object deconstruction כפרמטר של פונקציה בשביל: א. לתת שמות לפרמטרים של הפונקציה. ב. לקבוע ערכי ברירת מחדל.

ביצירת אובייקטים ב ES6, אם ה key וה value בעלי שמות זהים, במקום לכתוב height: height – אפשר פשוט לכתוב height.

הנה ההפעלה:

אני מקווה שזה פשוט הגיוני.

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

  1. ההגדרה:
    1. אנו מציבים את הערך h של ה destructed object בתוך המשתנה height.
      הייתי רוצה להמליץ ליוצרי השפה על תחביר חץ כגון: height <- h = 6  שהיה אולי יותר ברור – אבל כבר מאוחר מדי.
    2. סיפקנו ערכי ברירת-מחדל, ב-2 רמות: גם אובייקט, וגם ערכים לשדות.
  2. ההפעלה:
    1. כשלא סופק אובייקט, ברירת המחדל היא האובייקט שסופק.
    2. סופק אובייקט, אך properties החסרים בערך – יקבלו ערך ברירת-מחדל.
    3. השם הארגומנט הוא h ולא height, ולכן שליחת אובייקט עם property בשם height – הוא חסר משמעות (אם כי ניתן להתבלבל).
שתי דוגמאות אחרונות לסיום ההשלמות:
  1. יש לנו Arrow Function שמחזירה אובייקט. המפרשן של ג’אווהסקריפט עשוי לא להבין בצורה חד-ערכית למה התכוונו (?! אולי זה destruction של אובייקט). הפתרון התחבירי – לעטוף את גוף הפונקציה בסוגריים.
    אם היה מדובר ב object deconstruction – היינו עוטפים בסוגריים את כל הביטוי (אגף ימין + שמאל של ההשמה).
  2. מה זו שרשרת החצים הזו? מאוד הגיוני: פונקציה שמחזירה פונקציה. אתם כנראה תתקלו בכאלו.
    1. הנה ההפעלה: ההפעלה הראשונה (basePort = 80) מקבלת פונקציה, וההפעלה השנייה (distance = 100) מפעילה את הפונקציה שהתקבלה. אוי, יצא מספר מוכר!
זהו. סיימנו את ההשלמות ואפשר להמשיך הלאה.

Classes

ES6 הציגה Syntactic sugar להגדרת Classes. כלומר: נוספה מילה שמורה class שעוזרת להגדיר class, אבל זה אינו מבנה שמציג יכולות חדשות בשפה – אלא רק מקצר כתיבה, וחוסך התעסקות עם prototype. מאחורי הקלעים נוצר קוד שיכולנו לכתוב גם ב ES5:

באופן דומה, יש גם הורשה:

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

כלומר: לא קיבלנו Full-fledged classes מובנים בשפה – אך קיבלנו כלי שבהחלט כדאי להשתמש בו.

עוד 2 דברים שניתן להגדיר על מחלקות הם getters/setters, ו static members – הזמינים רק מתוך המחלקה, ולא מתוך המופע (כלומר: ב ES5 אלו properties שיושבים על המצביע ל constructor ולא על ה prototype):

זהו. עכשיו אתם מכירים classes ב ES6. מזל טוב!

Modules

ההפרדה ב JavaScript בין קטעי קוד מקבצים שונים (“מודולים”) צמחה מ 2 תקנים: CommonJs הסינכרוני (NodeJs) ו AMD האסינכרונית (בדפדפן, המימוש הנפוץ נקרא Require.js – כתבתי עליו פוסט בזמנו).

הגדרות המודולים הבשילו – והיום הם חלק מהתקן של השפה. הם נמצאים במלואם בתקן – אבל לא כל פרטי המימוש זמינים לרוחב כל המנועים השונים. הדפדפנים המודרניים תומכים היום בטעינה של מודולים בנוסח  וחלקם גם ב dynamic import. עדיין מדובר ב 75-85% מהמשתמשים בלבד (בעת כתיבת הפוסט, ע”פ caniuse) – משהו שקשה מאוד להסתמך עליו.

הפתרון הפשוט היום הוא להשתמש בכלי להרכבת קבצי ה source ל bundle – כמו WebPack או Parcel, ע”מ לקבל תמיכה במודולים בדפדפן – משהו שרבים מאיתנו כבר עושים היום, בכל מקרה.

בצד השרת (NodeJs) התמיכה הרשמית במודולים החלה בגרסה 12.
בגרסאות ישנות של Node, זהו פיצ’ר ניסיוני שאפשר להדליק עם feature flag – או שאפשר לקבל אותו מספריות צד-שלישי.

בכל מקרה, הכיוון הוא לאמץ מודולים, וגם אם אתם לא משתמשים בהם כרגע – סביר שתשתמשו בקרוב. את דוגמאות הקוד בפוסט אני מריץ על node 12.

מודולים נועדו לספק לנו Encapsulation לקוד, כך שנוכל לשלוט בקובץ הג’אווהסקריפט שלנו: אלו חלקים הם “ציבוריים” ואנו רוצים לאפשר לקבצים אחרים להשתמש בהם, ואלו הם פרטי מימוש פנימי. מודולים הם גם הבסיס ל Lazy Loading של התכנית – אבל זה היום שיקול משני.

ב ES6 כל קובץ הוא מודול. אנו יכולים להחצין (export) משתנים, פונקציות, מחלקות, וכו’ – ומי שמעוניין להשתמש בהם, יצטרך להצהיר על רצונו זה במשפט import. מה שלא הוחצן – איננו נגיש למודולים (קבצים) אחרים, ולא ניתן לעשות לו import. פשוט!

הנה האופנים בהם ניתן להחצין אלמנטים במודול, שימו לב שענייני ה import/export הם עניין שבו נוטים להתבלבל:

  1. אנו מוסיפים את המילה השמורה export לפני ההגדרה (משתנה, פונקציה, וכו’) כדי להחצין את ההגדרה. פעולה זו נקראת named export.
  2. אנו מחצינים שורה של אלמנטים בפקודה בודדת. הסוגריים המסולסלים עוטפים את האלמנטים. זהו גם named export.
  3. אנו משתמשים בפקודה מיוחדת בשם default export המחצינה אובייקט – ויכולה להיות מוגדרת לכל היותר פעם אחת בקובץ.
    1. מי שיבצע import למודול יוכל לתת איזה שם שירצה לאובייקט הזה.
    2. הסוגריים המסולסלים מגדירים אובייקט בו במקום. יכולנו גם לקרוא ל export default עם רפרנס לאובייקט שנוצר קודם לכן.
  4. אפשר לערבב את ה default export בתוך פעולת export של מספר איברים אחרים. גישה זו איננה מומלצת!
פרקטיקה מקובלת היא להשתמש ב default export בלבד, על מנת שיהיה מקום אחד ברור שמציין מה בדיוק מוחצן מהמודול – ולשים את פעולת ה default export בסוף הקובץ. זוהי פרקטיקה הלקוחה מ CommonJS.
דרך אחרת היא להחליט להשתמש ב export על כל אלמנט שאנו רוצים להחצין, כך שרמת הגישה תהיה מוגדרת “על הרכיב”. הערבוב בין הגישות – עשוי להיות מבלבל, ולכן אינו מומלץ.

באופן דומה, ניתן לבצע import:

  1. צורה זו מייבאת את כל הקובץ, והיא תריץ מחדש את כל הקוד בקובץ (כמו import בשפת C). צורה זו איננה מומלצת לשימוש, ואיננה חלק ממערכת המודולים!
  2. הצורה המומלצת היא לייבא את ה default export – ולתת לו שם מקומי. פשוט.
  3. צורה זו היא named import בו אנו מציינים את שמות האלמנטים בהם אנו רוצים לעשות שימוש.
    התחביר מזכיר תחביר של deconstructing assignment.
  4. אפשר להשתמש ב named import אך לתת שמות אחרים מקומיים לאלמנטים שלהם עשינו import.
  5. אפשר לייבא את אל האלמנטים שהוחצנו, מה שנקרא namespace import.
    1. אפשר לבצע deconstructing assignment בכדי להגיע לתוצאה דומה ל named import.
  6. אפשר לבצע import מעורב (אם היה גם export מעורב). foo  (מחוץ לסוגריים המסולסלים) הוא השם לאובייקט ברירת המחדל שהוחצן. הגישה הזו יוצרת מקום לבלבול בכמה רמות שונות – ולכן אני ממליץ להימנע ממנה.

Promises

אני מניח שאני לא צריך להכיר ולהסביר מה הם Promises – אבל אולי אני טועה. Promises הוא Pattern המאפשר לבצע ניתוק בין הרצה של לוגיקה לקבלת התוצאה שלה. סה”כ זהו כלי שימושי מאוד – שהפך לחלק מרכזי מאוד בשפה.
אני מניח שרובכם מכירים את הרעיון משפות תכנות אחרות, או מספריות כמו Q, Bluebird  או מ jQuery.deferred (אותו כיסיתי בפוסט עבר).

מחמת הספק, ומחמת החשיבות של היכולת – אסכם אותה בקצרה.

Promise הוא הבטחה לקבלת תוצאה עתידית של הפעלת לוגיקה כלשהי, לרוב הפעלה אסינכרונית. שם נרדף, ולא פחות מוצלח הוא “Deferred” – תשובה המגיעה מאוחר יותר. ה Promise הוא ה handler להפעלה, המאפשר לאסוף את התשובה:

  • במקום אחר (להעביר את ה promise/handler לאורך כמה פונקציות – ורק אז לקרוא את התשובה)
  • ובזמן אחר (להספיק לעשות פעולה נוספת בזמן שהפעולה האסינכרונית רצה).

יתרון נוסף וחשוב של Promises הוא כתיבת קוד למסודר יותר – יחסית ל callbacks.

“אז מה ההבדל? במקום callbacks מקוננים, אני כותבת את השורות בזו אחר זו?”

  1. כן – זה שיפור מורגש.
  2. callbacks עם error handling הוא קוד שנוטה להסתבך במיוחד. נראה בהמשך כמה יותר אלגנטי הוא הפתרון של Promises.

הנה דוגמה פשוטה:

  1. יצרתי Promise והעברתי להרצה פונקציה המבצעת קריאה אסינכרונית ברשת (request הוא מודול של NodeJs). כשתחזור התשובה – הפונקציה תמשיך לפעול ותקבע תשובה ב Promise, תשובה שיהיה אפשר לאסוף מרגע שנקבעה.
    1. אם הבקשה מצליחה – אני מאפשר החזרת ערך בעזרת ה Promise – בסימן הצלחה (להלן “resolve”).
    2. אם החלטתי שהבקשה נכשלה – אני מאפשר החזרת הסבר בסימן כישלון (להלן “reject”).
  2. במקום / שלב מאוחר יותר – אני שולף את הנתונים מה Promise
    1. then – אם הייתה הצלחה.
    2. ה Promise שלי, יכול היה להחזיר Promise בעצמו – וכאן הייתי יכול להמשיך ולשרשר את הטיפול בתשובה שלו. במקרה שלנו, לא הגדרנו תשובה בפעולת הסעיף הקודם, ולכן הערך הוא undefined.
    3. catch – יתבצע אם היה כישלון (במקרה שלנו – הייתה הצלחה). “ערוץ” ה catch משתשרשר כל ה promises שבדרך (“then”), כך שאם נכשל ה promise המקורי (למשל: נקלקל את ה url) – יופעל ה catch.
    4. finally – קוד שירוץ בכל מקרה.

השימוש העיקרי ל Promises הוא טיפול בפעולות אסינכרוניות (הרי בג’אווהסקריפט יש לנו Thread יחיד), וטיפול כזה לא יהיה שלם ללא פעולות “מיזוג” על הפעולות האסינכרוניות:

  1. Promise.all יוצר Promise חדש, שיהיה resolved (או rejected) כאשר כל ה promises ברשימה יחזירו תשובה. זהו כלי חשוב מאוד להרצה של מספר פעולות אסינכרוניות במקביל – ואז טיפול בתשובות.
    1. אפשר לשים לב שהתקן הוגדר לפני שהיה Rest Parameter לפונקציות…
  2. הפעולה ההופכית, race, שיכולה הייתה גם להיקרא any – מחזירה promise שיחזיר לנו תשובה כלשהי (שחזרה) מה promises שנשלחו כפרמטרים. פעולה פחות נפוצה – אך עדיין חשובה.
  3. ניתן להשתמש ב Promises גם לפעולות סינכרוניות. פשוט יוצרים promise כבר עם “התשובה בפנים” בעזרת הפונקציות reject או resolve.

סיכום

טוב, התקדמנו צעד חשוב, ואני מקווה שלאחר פוסט זה, יש לנו מושג לגבי עיקרי השפה ES6.

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

אחרי הפוסט הזה, אתם אמורים להכיר את רוב התחביר החדש בשפה – אבל למי שרוצה “להגיע עד הסוף”, חסר עוד קצת.

אני מלא כוונה טובה לסיים פוסט נוסף (ואחרון?) בסדרה – ולכסות גם את הנושאים הללו.

שיהיה בצלחה!

Javascript ES6/7/8 – להשלים פערים, ומהר

ג’אווהסקריפט היא אחת השפות הנפוצות בעולם: קל ללמוד אותה*, יש לה מונופול בסביבת הדפדפנים, וכמעט לכל מערכת חשובה היום בעולם – יש ייצוג וובי. ג׳אווהסקריפט גם פופולארית למדי בצד-השרת (node.js), היא נחשבת לשפה אוניברסלית שרצה בכל מקום – ויש לה מעט מאוד מתנגדים, כי היא לא “שייכת” לשום קבוצה.

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

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

חוסר עקביות מעורר השתאות של ג’אווהסקריפט, בפעולות החיבור בין אובייקטים. מקור

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

לאחר שנים של שיפורים קטנים יותר ופחות – שפת ג’אווהסקריפט ביצעה כמה שינויים משמעותיים מאוד. כל-כך משמעותיים שזו שפה כמעט אחרת: הרבה יותר מתוכננת ועקבית – אבל גם הרבה יותר עשירה. אי אפשר כבר ללמוד אותה על בוריה ביום-יומיים (כשפה).
לא אכנס לעומק שמות-הקוד, הכינויים, והגרסאות השונות, אבל אפשר לומר שהמהדורה השישית של השפה, שנקראת ECMAScript 6 (בקיצור ES6) או ECMAScript 2015 – היא כנראה הקפיצה המשמעותית ביותר. במהדורה זו נוספו “מחלקות” ו”מודולים” לשפה – והפכו אותה דומה הרבה יותר לשפות OO מוכרות כמו Java, TypeScript או #C. הרבה יותר – אבל עדיין, בפרטים יש הבדלים רבים.
פעם נאמר שההבדל בין JavaScript ל Java שקול להבדל בין Carpet ל Car.
היום כבר אפשר לומר שההבדל בין JavaScript ל Java שקול להבדלים בין Carrier ו Car – כבר באותו האזור.
עוד שתי מהדורות חשובות של JavaScript (או ECMAScript – בשם הפורמאלי) הן מהדורות 7 ו 8 – להלן ES7 ו ES8, כל אחת הוסיפה סדרה של כלים משניים (אך עדיין משמעותיים) לשפה.
אימוץ התקנים של ECMAScript ע”י מנועי ההרצה (כמו V8 או Chakra) לא נעשה כמקשה אחת בנוסח “מעכשיו אנחנו תומכים ב 100% ב ES5.1” אלא התמיכה נעשית feature by feature – כך התקן מאפשר.
לכן לא חשוב כ”כ איזה פיצ’ר שייך לאיזו מהדורה של התקן – אלא חשוב יותר להסתכל על התמונה הכוללת: כמה פיצ׳רים זמינים אל איזה אחוז מהמנועים.
רמת התמיכה ב ES6 ע”י מנועי-ההרצה השונים. מקור.

אם עוד לפני שנה-שנתיים התמיכה בדפדפנים עדיין לא הייתה טובה מספיק – והשימוש העיקרי ב ES6 היה בסביבות בהן אנו שולטים על הגרסה (כמו NodeJs), היום המצב כבר השתנה ובגרסאות הדפדפנים האחרונות התמיכה כבר טובה למדי!
התמונה למעלה מציגה את התמונה עבור ES6, אך המצב גם כבר דיי טוב עבור ES7 ו ES8.
לפני שאתם משתמשים בפיצ’ר מתקדם כדאי לבדוק את רמת התמיכה שלו באתר CanIUse.

לצורך הפוסט אשתמש בשם ES6 בכדי להתייחס ל ES6+ES7+ES8 – נראה לי שהם מספיק קרובים בכדי שיהיה אפשר להתייחס אליהם כמקשה אחת. לכל מה שהגיע לפני ES6 אקרא בפוסט ES5 (למרות שיש מגוון גרסאות שונות).

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

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

אם זה המצב, כנראה שאם תפתחו קוד חדש שנכתב על בסיס ES6 – תתקשו לקרוא אותו. חלקים חוזרים בתחביר – לא יהיו מובנים.

מטרת הפוסט היא לא לכסות את כל פינות ES6 וכל הפיצ׳רים – אלא בעיקר לכסות את התחביר החדש – ומשמעויותיו. לאפשר לכם לקרוא ולהבין קוד ES6. את התשובות לשאולת בנוסח ״איך עושים …. ב ES6״ – אני אשאיר לגוגל ו Stack Overflow.

נצא לדרך!

let ו const מחליפים את var

ל var (הגדרת משתנה) של ג׳אווהסקריפט יש כמה בעיות מהותיות:
  • אם שכחנו להשתמש במילה השמורה var בהגדרת משתנה – אין בעיה! המשתנה יוגדר על המרחב הגלובלי (או אובייקט שמייצג אותו, למשל window בדפדפן).
  • אם הגדרנו משתנה פעמיים – אין בעיה! הוא יוגדר מחדש (על חשבון הקודם). הגדרה כפולה של משתנה היא כנראה באג ולא כוונת המתכנת הסביר.
  • ה scope של הגדרת var הוא scope הפונקציה – ולאו דווקא הבלוק העוטף (כלומר: {}), זה גם מבלבל (שונה משפות תחביר-C האחרות) – וגם פחות שימושי: משתנים שאורך החיים שלהם מתאים יותר ל block ״זולגים״ החוצה ל scope של הפונקציה.
  • קוד שבא לפני הגדרה של משתנה שהוגדר כ var – עדיין יכול להשתמש במשתנה. זו מן התנהגות של מנגנון שנקרא hoisting בו כל הגדרות ה var (וגם function או class) מקודמות לתחילת ה scope שבהן הוגדרו לפני שהקוד מבוצע במפרשן.
    • עצה נפוצה ב ES5 היא לבצע את כל ההגדרות בתחילת ה scope – בכדי להימנע מהתנהגות לא-צפויה של הקוד. כלומר: לכתוב את הקוד כפי שאכן ירוץ.
מה ההבדל בין let ל const?
let הוא משתנה שיכול להשתנות, ו const הוא משתנה שערכו מוגדר רק פעם אחת (כמו const ב Kotlin, כמו final בג׳אווה או readonly ב #C).

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

כלי להגנה על פרמטרים באובייקט הוא הפונקציה (…)Object.freeze. חשוב להזכיר שהיא לא רקורסיבית, קרי לא תגן על שינוי תכונות של אובייקטים שנשמרים על האובייקט – מה שהופך אותה לפחות שימושית.

אז איך מתמודדים עם מגבלות עבר (כלומר: “ה var”)?

עם בעיות ה var של ג׳אווהסקריפט, החלו להתמודד עוד ב ES5 בעזרת מנגנון שנקרא “strict mode״: אם בתחילת ה scope (פונקציה או גלובאלי) כתבתם את השורה “use strict” – אזי המפרשן יהיה סלחן פחות לטעויות.
בהקשר של var: כאשר אנחנו נמצאים ב Strict mode – אנו מחויבים להגדיר משתנה בעזרת var / let / const.
ב ES6:
  • מוגדר תמיד strict mode בתוך מודולים – אלמנט חדש בשפה (פוסט הבא?), שהוא דיי נפוץ. בשל תאימות לאחור לא החילו strict mode על המרחב הגובאלי / פונקציות רגילות – וההמלצה היא להמשיך ולהגדיר בהם ״use strict״.
  • שימוש ב let / const לא מאפשר להגדיר מחדש משתנה שכבר הוגדר.
  • ה scope של let / const הוא הבלוק {} בו הם הוגדרו – ולא רק הפונקציה. זה כנראה השיפור המורגש ביותר.
  • לכאורה let / const לא עוברים תהליך של Hoisting ולא ניתן לגשת אליהם לפני שהוגדרו.
    • למען הדיוק, כן מתרחש Hoisting (מגבלות טכניות?) – אבל המפרשן מוסיף גם בדיקה בעת הגישה, ואם יש גישה למשתנה לפני שאותחל – הוא יזרוק Reference Error:
  1. מדפיסה ״global x״ מכיוון ש x לא  c ב scope הפונקציה, הולכים ל scope החיצוני – ומוצאים אותו שם. זו התנהגות ES5.
  2. השורה השנייה תזרוק ReferenceError בעת הפענוח.המשתנה y לא אותחל – זו הבדיקה שדיברנו עליה. היה hoisting ולכן המפרשן יודע על קיומו, אבל לא ניתן לגשת אליו.
הערה: בשל הבדיקה שנוספה לשפה שמשתנה לא יקרא לפני שהוגדר, ייתכן והחלפה גורפת של var ל const/let של ES6 יגרמו ל Errors חדשים שלא נזרקו בשימוש ב var. שווה לעשות את המעבר – אבל להיות גם מודעים לאפשרות לתקלות.

הבלוק שאתם רואים (סוגריים מסולסלים צהובים) הוא התחליף המקובל ב ES6 ל Immediately Invoked Function Expressions – הגדרה של פונקציה שמיד מפעילים אותה. זה בעצם היה תרגיל לצורך “סגירת” משתנים מסוימים ב scope מצומצם יוצר, מה שאנו מקבלים ב ES6 מבלוק רגיל – כאשר אנחנו משתמשים ב let/const.

Arrow Functions

על פניו, Arrow Functions (בקיצור: AF) הם דרך מינימלית יותר להעביר פונקציה כארגומנט.

  1. התחביר הקלאסי (הפונקציה אנונימית ומצביע אליה מושם למשתנה).
  2. תחביר AF כאשר יש פרמטרים.
  3. תחביר AF ללא פרמטרים.
אלמנט יותר חשוב הוא ש AF  נוצלו על מנת לעשות תיקון היסטורי בהגדרת ה this בג’אווהסקריפט. ב ES6 “הרכיבו” כמה תיקונים היסטוריים על שינויים חדשים – מה שמעודד אפילו יותר להשתמש בפיצ’רים הללו.

כפי שאתם בוודאי זוכרים, this בג’אווהסקריפט מתייחס ל context הקורא לפונקציה, ולא ל scope החיצוני – כפי שמקובל ברוב שפות התכנות. זה מבלבל (כתבתי על זה פוסט שלם, בזמנו) – ותרגיל מקובל הוא להוסיף ב scope החיצוני את השורה:

var that = this; // Store the context of this

על מנת להיות מסוגלים לגשת ל this של ה scope העוטף.

ב AF – זו ההתנהגות הטבעית. הפרקטיקה היום ב ES6 היא להשתדל ולהשתמש ב Arrow functions ככל האפשר.

כמה תכונות חדשות של פונקציות (ומסביב) – ששינו את תחביר השפה

Rest Parameter

Rest Parameter הוא המקביל של varargs של ג’אווה / params של #C:

כמו בשפות אחרות – על ה rest param להיות אחרון ברשימת הפרמטרים (הגיוני).

אתם בוודאי תתקלו גם template strings במוקדם או במאוחר. המירכאות הבודדות והכפולות כבר תפוסות בשפה – אז בחרו מירכאות נוטות-לאחור. כל ביטוי בתוך המחרוזת שסגור ב {}$ – יוערך (eval) ע”י המפרשן.

ברוכה הבאה, ג’אווהסקריפט, למשפחת השפות המודרניות!

Spread Operator

Spread Operator (בקיצור: SO) הוא כלי חדש וחשוב בשפה. התחביר שלו זהה ל Rest Parameter (שלוש נקודות) – מה שהזכיר לי אותו בהקשר לאייטם הקודם.

ה SO מקבל אובייקט iterable (כמו מערך או מחרוזת)  – ו”מפזר” את הערכים שלו. הקונספט הזה קיים גם בשפות אחרות.

חשוב לציין שאי אפשר להשתמש בו סתם כך, למשל: להציב את התוצאה שלו למשתנה. יש להשתמש בו בהקשר שמוכן לקבל iterable מהסוג הנכון.

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

  1. בצורה הזו x מקבל את הרשימה, בעוד y ו z – לא מקבלים ערכים, ולכן הם undefined.
  2. אם ״פיזרנו״ את הרשימה – כל הפרמטרים מקבלים ערכים מהרשימה (כי הרשימה ארוכה דיה).
  3. אי אפשר להשתמש ב SO להשמה פשוטה. זה לא הגיוני. אפשר להשתמש ב SO רק בהקשרים המוכנים לקבל iterable.
בואו אבל נראה דוגמה יותר שימושית. מי שלא כותב הרבה ג’אווהסקריפט נוטה ליפול הרבה בפח הבא:

פעולת sort בעצם משנה את האובייקט עליו היא מופעלת. עוולה ישנה של ג’אווהסקריפט.
אבל – יש פתרון:
  1. ה SO מאפשר לנו לייצר בקלות עותק חדש של המערך – ורק עליו נבצע מיון.
  2. אנחנו יכולים להשתמש ב SO גם בכדי להרכיב בקלות ובצורה דינאמית – מערכים חדשים.
  3. SO עובד גם על אובייקטים, ומאפשר מן תחביר חדש וקל “להרחיב” אותם.
  4. אפשר גם הפוך – אפשר לצמצם עותק של האובייקט. כמובן, אבל חשבתי שיהיה שימושי להזכיר.
בקיצור: כלי שימושי!

Deconstructing

אם אתם עובדים עם שפות מודרניות, סיכוי טוב שה Spread Operator הזכיר לכם פעולת Deconstruction, המפרקת איברים ממערך או אובייקט – בהשמה. גם פעולה זו נוספה לשפה:

  1. בדוגמה הפשוטה ביותר, אנו מציבים כמה ערכים, במקרה שלנו a ו b – בפעולה אחת, מתוך מערך.
  2. אפשר להשתמש ב deconstruction בכדי לבצע swap, למשל.
  3. אפשר לשלב Rest Operator בתוך Deconstruction ולהציב את כל הערכים הנותרים בתוך המשתנה שקראנו לו rest.
    1. שימו לב שאם אני לא זקוק לערך מסוים, אני יכול לדלג עליו עם פסיק ללא משתנה. נחמד.
Deconstruction עובד גם על אובייקטים:
  1. זהו התחביר. אנחנו שולפים לתוך משתנה בשם x את הערך למפתח x מתוך האובייקט.
  2. מה עושים כאשר המשתנה כבר מוגדר, אך אנו רוצים להציב בו שוב?
    1. תקלה: אסור להגדיר מחדש משתנה בעזרת let.
    2. תקלה: ג׳אווהסקריפט לא יודע לזהות שמדובר בפעולת deconstruction.
    3. הפתרון התחבירי: לעטוף את השמת ה deconstruction בסוגריים. אני מקווה שעכשיו זה נראה הגיוני.
  3. השימוש הנפוץ ל deconstruction, מן הסתם – הוא בהשמה לריבוי ערכים. מה קורה כאשר אין התאמה בין שם המשתנה למפתח באובייקט? – אנו מקבלים undefined.
    1. מה עושים אם לאובייקט יש מפתחות בשמות שלא מתאימים לנו? – אנחנו יכולים להשתמש בתחביר הזה בכדי לבחור באלו שמות משתנים להציב אותם. אנו רוצים שערך המפתח x יכנס למשתנה a, וערך המפתח y למשתנה b.
    2. האם אפשר לספק שמות שונים רק לחלק מהאיברים? אפשר.
      אני מסתכל על השורה ותוהה הזו מה הסיכוי לנחש מה היא עושה אותה מבלי להכיר את הכללים?!
  4. גם כאן אפשר להשתמש ב rest operator (כרגע פיצ׳ר בהרצה), rest הפעם הוא מטיפוס אובייקט (ולא מערך).

ערך ברירת מחדל

כן! אנחנו יכולים לקבוע ערכי ברירת מחדל לפרמטרים בפונקציות (ובעוד כמה מקרים).

  • ערך ברירת מחדל לפרמטר בפונקציה – שימושי להרחבת פונקציה בצורה תואמת-לאחור או צמצום החתימה שלה – עבור השימושים הנפוצים.
    • ערך ברירת המחדל הוא תחליף מרכזי תחביר ה x = x || 10 שמאוד היה מקובל בשפה. היום – כמעט ולא תראו אותו.
    • ערך ברירת המחדל הוא תחליף מסוים ל function overloading – יכולת שלא קיימת בשפה.
  1. שימוש פשוט בערכי ברירת מחדל.
  2. אם מעבירים undefined לפרמטר עם ערך ברירת-מחדל, אזי יתקבל ערך ברירת המחדל – ולא undefined. ערך null יעבור כרגיל.
  3. הנה, אפשר להשתמש בערך ברירת-מחדל גם בהשמת deconstruction.
  4. גם כאן, כללי ה null וה undefined – תקפים.

סיכום

שפת ג׳אווהסקריפט השתנתה מאוד בכמה השנים האחרונות. אם פעם זה היה ״חזון קדימה״ – היום זו המציאות. זו כמעט שפה חדשה.
בפוסט הזה ניסיתי להסביר את התחביר, וכיצד כמה וריאציות שלו עשויות ליצור קוד שייראה ״חייזרי״ למפתחי ES5. בשאיפה – הצלחנו להתיר הרבה מהקשיים, ופעם הבאה שתראו קוד ES6 – יהיה לכם הרבה יותר קל לקרוא ולהבין אותו.
שיהיה בהצלחה!
—רוצים להנסות בזריזות ב ES6, מסביבה מעט יותר נוחה מה console של הדפדפן? ה https://es6console.com – הוא אופציה ראויה. רק על תשכחו להדליק את ה flags של ES7/8 – פשוט אין ES8 console…

תבנית עיצוב: Null Object

לא כתבתי הרבה בזמן האחרון – ככה זה שמשפצים דירה…
בכל זאת, אנסה לכתוב פוסט קצר על תבנית עיצוב בשם Null Object.יש לנו מסורת ב SAP, להעביר פעם בשנה קורס “תבניות עיצוב” (Design Patterns) לעובדים החדשים. חלקם מכירים את הנושא – אבל תמיד יש מה לשפר. אני כותב פוסט זה תוך כדי הכנה של החלק שלי בקורס.
האמת? יש לי הסתייגות פנימית קלה מנושא תבניות העיצוב: תבניות עיצוב הוא תחום שעלול לגרום ליותר נזק מתועלת – כאשר מנסים “לדחוף כמה שיותר תבניות עיצוב במטר מרובע”, וזה יכול לקרות הרבה.
מצד שני, כשמתבגרים ונרגעים ניתן להשתמש בהן בצורה מאוזנת ומועילה. חוץ מזה: זה נושא שאנשים רוצים לדעת היטב – ומי אני שאמנע מהם? האם אני הייתי מוכן לוותר על להכיר את הנושא? בטח שלא!

בקורס, אנו מנסים להדגיש את הנקודות החשובות הבאות:

  • תבניות עיצוב כשפה רק עבור התקשורת הפשוטה, כדאי ללמוד תבניות עיצוב: “ה class הזה הוא בעצם כמו Strategy” יכול להגיד המון למי שמכיר את ה Pattern או לגרור דיון של כמה דקות – למי שלא.
  • תבניות עיצוב כתיקון למצב בעייתי, או מה שנקרא Refactoring To Patterns: כדאי ומומלץ להימנע משימוש בתבניות עצוב במחשבה על “מצב עתידי”, קרי “יהיה יותר קל לטפל גם במספר רב של אובייקטים אם נשתמש כאן ב Composite“. חכו שיהיה מצב בו יש מספר רב של אובייקטים – ורק אז עשו Refactoring ל Composite.
    ההמלצה היא לזהות צורך מיידי ש Pattern יכול לפתור, או לסירוגין smell בקוד שנובע מאי-שימוש ב Pattern ורק אז לעשות refactoring ולהכניס את ה Pattern.
אם “smell” נשמע עבורכם סינית – מדובר על code smells, שהם חלק מהשפה של טכניקת ה refactoring.

דיי מדהים איך חברת קוקה קולה לוקחת מושג כ”כ שלילי בעולם הגברי (“אפס”) ומנסה למכור אותו כמותג נחשק – לגברים. האם יכולים לשחק לנו בשכל עד כדי כך? מה עם קבעון גברי טיפוסי?!

קצת על null בג’אווהסקריפט

ג’אווהסקריפט הופכת לשפה משמעותית עבורנו – ולכן הדוגמאות שאספק יהיו בעיקר מעולם זה. לא מעניין אתכם javaScript או ה nulls שלה – דלגו ל section הבא.

למה “nulls” ולא “null“? כי יש כמה כאלו בג’אווהסקריפט. תלוי את מי שואלים, יש שניים או שלושה, אני אציג את נקודת המבט המחמירה:

  • יש ערך “null, שמשמעותו היא “נקבע למשתנה זה ערך ריק / חסר משמעות”. בניגוד לג’אווה זהו איננו ערך ברירת המחדל למשתנים.
  • יש ערך “undefined” ששמו דיי מבלבל: נשמע שהוא מתאר משתנים שלא הוגדרו – אך בעצם הוא נקבע כערך ברירת מחדל למשתנים שהוגדרו אך לא נקבע להם ערך. שם מוצלח יותר היה יכול להיות unassigned.
  • יש מצב בו לא הוגדר משתנה. אפשר לקרוא לו “בום!“: ניסיתם לגשת למשתנה, אפילו בזהירות? – ייזרק error. שם שהייתי מעדיף לכזה משתנה הוא undefined, אבל השם כבר תפוס. זה לא בדיוק null value, אבל הוא דורש התגוננות דומה (ונוספת).
בואו נראה דוגמת קוד:
אפשר לראות שהבדיקה שאלו 2 ערכים שונים בעליל ולא תת מקרה אחד של השני.
אני לא מתעסק בשוויון ללא בדיקת טיפוס (==), מכיוון שהיא לא הדבר הכי צפוי בג’אווהסקריפט (אבל גם לא הדבר הכי לא-צפוי):
כלומר: כדי לא לחטוף NullPointerException וגם / או UndefinedPointerException (בקריצה לגאווה) האם עלינו לבדוק כל משתנה שאנו מקבלים – פעמיים בפני אפשרות ל null?
לשמחתנו, אנו יכולים לנצל את “הגמישות” בבדיקת הטיפוסים בג’אווהסקריפט מדי פעם כדי לבדוק את שני ערכי ה null בפעם אחת. התחביר המועדף עלי הוא שימוש ב ! (או !!, כאשר השמת הערך כפי שהוא לא אפשרית / ברורה):
נראה עקבי.
אבל… מה קורה עם משתנה z שלא הגדרתי (כלומר הוא: truly undefined)?
בואו ננסה:
אולי סוג אחר של הגנה?
אאוץ! גם לא.
ה errors שאתם רואים הם לא “ערך החזרה רגיל של פונקציה”, הם פשוט יעיפו לכם את התוכנית – אם תנסו לבדוק האם ערך כלשהו, שלא הוגדר, הוא בעצם null. מה עושים? הנה הפתרון:
אוי ואבוי? האם צריך לעשות בדיקה כזו בכל מקום בקוד?
בכלל לא… ארגומנט של פונקציה תמיד יהיה מוגדר, לפחות undefined.
יש לעשות בדיקה מקיפה זו כל פעם שניגשים (פעם ראשונה?!) למשתנה במרחב הגלובלי.
הערה: גישה למשתנה גלובאלי בצורה  .window היא דווקא בטוחה בפני ReferenceError. תודה לקורא שתיקן אותי.
אז האם יש 2 או 3 ערכי null בג’אווהסקריפט?
  • אולי אלו 2: null ו undefined
  • אולי אלו 2: null/undefined ו truly undefined
  • אולי אלו 3.

תלוי את מי שואלים….

מה הבעיה עם null?

ראשית, ש”אבא” שלו – פשוט לא אוהב אותו.
טוני אוהר’ה, מי שהמציא את הקונספט בשפת ALGOL איפשהו בשנות השישים, אומר שזו פשוט הייתה עצלנות. “לא היה לי זמן למצוא פתרון יותר אלגנטי באותה התקופה”. הוא קורא להחלטה ליצור את null טעות, ולא סתם טעות: “טעות מיליארד הדולר שלי” (טריליון דולר – בתרגום למונחים ראליים של היום). איזה נזק לתעשייה כולה! ומאז ששפת C החליטה לאמץ את הרעיון – אבדה הדרך חזרה…
מה הבעיה עם null?
איזה Exception מעיף לכם את התוכנה, בשפה ה strongly types עם הקומפיילר ההדוק שלה (קרי: Java), הכי הרבה פעמים?
רגע… תנו לי לנחש (לתקשר) … האם זה אולי NullPointerException?
כשאנו מפעילים פונקציה (/מתודה) ורשום לנו שהפונקציה מחזירה אובייקט מסוג ListItem, בעצם זה נכון נכון: היא מחזירה אובייקט מסוג ListItem או null.
כשרשום שהפונקציה מחזירה אובייקט מסוג List, גם זה לא נכון! היא מחזירה אובייקט מסוג List או null.
ב List אנחנו זוכרים לטפל: זהו ה common scenario – מה שצפוי, רצוי וקל לחשוב עליו.
את ה null אנחנו מפספסים הרבה יותר. והיכן אנו מגלים את הבעיות הללו? בתוך ה IDE? – לא כ”כ. אולי בזמן בדיקות, אולי בשרת האינטגרציה… אולי ב production … ואולי אצל הלקוח.
אם כ”כ הרבה מפתחים שוכחים אותו דבר – כל כך הרבה פעמים, אולי הבעיה היא בשפה / כלים ולא במתכנתים?!
יתרה מכך, אפילו אם היה לנו קומפיילר שמתריע על כל מקרה שבו שכחנו לטפל ב null (ואין כזה – למיטב ידיעתי), עדיין הקוד היה הרבה יותר מסובך. לפחות עליה ברמה אתה של Cyclomatic complexity – ע”פ הגדרה: תמיד יש עוד מקרה אחד, עוד הסתעפות אחת של התרחשות שיש לטפל בה. בכל פונקציה.
הנה דוגמה מקוד של jQuery, ספרייה שעפו בה המון referenceErrors לאורך פיתוחה. באופן כמעט אקראי בחרתי בפונקציה add שרושמת אירוע על אלמנט בדום בעקבות קריאה ל (…)on.$. היא לא מקבלת את הפרמטרים כמו שהם – פונקציית on עושה המון בדיקות קלט, כולל nulls למיניהם. שימו לב עדיין מה יש בה:
סימנתי בצהוב כל מיני בדיקות null למיניהן שעדיין יש לעשות, למרות ההגנה של פונקציית on.
מה זה?? האם jQuery הוסיפה טיפוס null משלה בשם “strundefined“?
לא לדאוג. זהו פשוט קיצור של “typeof undefined” – כנראה בגלל שיש הרבה בדיקות כאלו.
בקיצור: null מחרבן לנו את הקוד והופך אותו מסובך יותר לבדיקה. הוא עוזר, כנראה, לכתוב גרסה ראשונה – אבל בדרך ייצוב המערכת לגרסה 5 אנחנו נאכל איתו עוד הרבה קש…
“מראה מראה שעל הקיר, מי השפה עם ה Null המזיק-פחות, שאותו יש להוקיר?” מקור: הבלוג הטכני של לוסידצ’ארטס https://www.lucidchart.com/techblog/

מה עושים?

וואהו! הצלחתי למלא את הפוסט דיי מהר, ובלי לשים לב. ככה זה ארכיטקטים: דברנים!
ישנן שפות מעטות (ואמיצות!) שהחליטו להיפטר מ null. אני שמעתי על Cyclone (דיי נדירה) ועל Haskell [א].
הייתי פעם בהרצאה של מישהו שהציע תחביר בו מצב ברירת המחדל הוא שאובייקט לא יכול להיות null. אם פונקציה יכולה להחזיר null, חובה עליה להכריז על ערך ההחזרה עם prefix של סימן-שאלה (כמו nullable object ב #C). למשל:
public ?ListItem getChild(….);
או
private ?List calcItems(…);
אני מוצא את הפתרון הזה מאוד אלגנטי – אך אינני מכיר שפה שאמצה אותו.
פתרון אחר הוא Design Pattern בשם Null Object שאומר: במקום להחזיר null, עם אופציה ברורה לשבור את הקוד במקומות אחרים שלא מוכנים לכך – החזר אובייקט (“Null Object”) שיתאר מצב null-י, מבלי לחרב את המערכת. על האובייקט הזה לדמות בצורה הטובה ביותר שניתן (אך סטנדרטית) מצב של “לא ידוע” / “אין”.
הנה דוגמה (שקצת יותר דומה לג’אווה דווקא, לא רוצה להתעסק עכשיו עם תיאור מחלקות בג’אווהסקריפט):
class Employee {
 
  public final static NULL_EMPLOYEE = new Person(”, [], 0, ”);
 
  Employee (String name, String[] expertise, int age, String desc) {
        …
  }
}
הרעיון הוא שבמקום להחזיר null, אני אחזיר את Employee.NULL_EMPLOYEE, שתאפשר לקוד להמשיך לרוץ – מבלי לשבור אותו.
אם יש לולאה שרצה על מערך מומחיויות העובד – ריצה על מערך ריק (שמגיע מה null object) לא תשבור אותו, אין בעיה. הקוד ימשיך בד”כ gracefully. פה ושם יהיו מסכים מוזרים שמדברים על עובד ששמו “” – אך זה הרבה יותר טוב מהודעת שגיאה לא ברורה, והמתנה ל patch הקרוב.
את תבנית העיצוב של Null Object מתארים באופן פורמאלי בצורה הבאה:
אפשר בהחלט לא להשתמש ב Null Object אחד בעל משמעות כללית, אלא לייצר כמה, עם משמעות סמנטית עשירה יותר, ועם התנהגויות שמתארות בצורה טובה יותר את ה “null behavior” (או “do nothing”) של אותו האובייקט. למשל במערכת העובדים:
פתרון אחר, אולי “פתרון חצי-דרך”, הוא בשלב מוקדם ככל האפשר – להחליף ערכי null בערכים סבירים אחרים – עבור שאר ה flow. זכרו שאם בדקתם בפני null נקודתית אז פתרתם את הבעיה של היום, אבל לא את התיקון / תוספת של “עוד-שעה”. פתרון זה (שאני קורא לו “inline null object”) הוא לא אידאלי, אבל מהיר וקצת יותר ארוך טווח מטיפול נקודתי ב null. כמו פלסטר עם דבק חזק במיוחד.
הוא נראה כך (בכתיבה המקוצרת שלו בג’אווהסקריפט):
 
init: function(options) {
    this.user = options.user | { name: “unkown”, items: []};
},

עדכון יוני 2014:

אפל הודיעה על שפת Swift, שתחליף עם הזמן את Objective-C לפיתוח OS X/iOS ומה שאומר שצפוי לה שימוש נרחב. מה הקשר? ב Swift משתנים לא יכולים להיות nil אלא אם הוגדרו כ Option Type (מה שנקרא גם “Maybe”) – שבתחביר השפה זהו סימן שאלה בסוף – למשל ?Int. פתרון מאוד אלגנטי לטעמי לעניין ה null!
התחביר דוגמה מאוד ל nullable types ב #C – תחביר המאפשר nulls ב primitives. כלומר – עשו שם רק את החצי הקל של העבודה…

סיכום

  • התרגלנו (בעצם: נולדנו) לעולם בו פונקציות עלולות להחזיר null בכל רגע, וללא התראה.
  • null יכול להפתיע בכל מקום בקוד – מה שהופך אותו לבעיית תחזוקה קשה.
  • NullObject יכול לעזור.
    • לצמצם הפתעות.
    • להוסיף סמנטיקה עשירה יותר למצב ה null.
  • NullObject הוא לא מושלם: הוא דורש מעט יותר עבודה. מומלץ לשימוש באופן מקיף במערכת שמתקרבת לשלב התחזוקה (לא MVP).
הפוסט נכתב בזמן שיא (לפוסט מסוגו) של שעתיים וחצי בערך. סליחה מראש במידה ויש תקלות הגהה הגעה הגאההה.
שיהיה בהצלחה!
—-
[א] יש לה משהו שנקרא maybe שאמור לטפל בצורה פשוטה במקרים לא צפויים. אני לא יודע איך הוא בדיוק עובד.

ניסוי בטכנולוגיות צד-לקוח (React.js)

פוסט זה נכתב בשיתוף פעולה של ניר בנימין, איתמר שגב ושלי.
הוא מסכם ניסיון של 3 ימים בכתיבה בראקט, שהתרחשה במסגרת “האקתון” במעבדות SAP.
הפוסט פורסם לראשונה ב geektime, אך ללא הפיסקה “כמה נושאים מעט יותר מתקדמים” (בעצתו של ניר – בכדי להקל מעט על הקוראים). אני רוצה להאמין שקוראי בלוג זה הם בעלי קיבולת גבוהה יותר מ 1600 מלים / 7 דקות קריאה – המומלצות לבלוגים.אמרו לי אם לא. אני לא מזלזל בטענה “זה קצת ארוך מדי בשבילי”. אנחנו בני-אדם.

—-

Angular.js, ספריית ה MVC מבית גוגל לצד-הלקוח, הופכת לבחירה יותר ויותר מקובלת בקרב מפתחים. כיום, היא אחד הנושאים המדוברים ביותר בטכנולוגיות צד-לקוח.

בצד ההגמוניה ההולכת ונבנית ע”י אנגולר בצד-הלקוח, נשמעים בד בדד, גם קולות הטוענים שאנגולר מורכבת מדי – שאפשר לעשות את הדברים בצורה פשוטה יותר. פשטות היא ערך משמעותי בעולם הקוד הפתוח ובצד הלקוח בפרט. אחד הקולות הללו מגיע מצד Facebook ו Instagram. הפתרון שלהם: ספרייה ששוחררה לפני כשנה בשם react.js.

React.js (להלן “ראקט”), לא תתחרה באנגולר ראש-בראש בעתיד הנראה לעין: אנגולר היא מקיפה הרבה יותר (הן פונקציונלית והן תהליכית), ומתאימה הרבה יותר ל mainstream.
ראקט פותרת רק חלק קטן ממה שאנגולר פותרת – ועושה זו בצורה לא שגרתית, מה שעלול להרחיק ממנה מפתחים רבים מה mainstream. היא אנדרדוג. כמה מהרעיונות שלה מקוממים במבט ראשון. אולי גם במבט שני :).

יש כבר כמה דוגמאות לשימושים ב React.js במוצרים מרכזיים[א]: פייסבוק (חלקים), אינסטגרם (עם ה router של backbone) ו Khan Academy (עם Backbone). ניתן למצוא עליה דיונים ב StackOverflow ואפילו יש 4 גרסאות שונות שלה לשימוש ב jsFiddle.

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

יצאנו לבדוק במה מדובר…

מה ראקט מציעה?

הדבר הראשון הניכר משימוש בראקט הוא מ Client Side Rendering מהיר ביותר! ראקט יכולה לשמש לכתיבת אפליקציות שמרגישות מאוד “חלקות” ולא נתקעות. היא מתאימה לאפליקציות בעלות רינדור אינטנסיבי.

עד כמה שזה מרשים – זה עדיין לא פיצ’ר שמצדיק שימוש בספרייה עבור רוב המשתמשים. היכולות הללו בעיקר קוסמות למי שנמצא ב extreme optimization של אפליקציות ווב (כמו חברי “מועדון ה sub-second”) ומחפש דרך לאופטימיזציה גבוהה מאוד, מבלי לחרב את הקוד.

האלמנט השני שמאפיין את ראקט הוא פישוט צד-הלקוח בכל הנוגע לעדכונים של ה DOM.
בד”כ באפליקציה נראה 2 סוגי עבודה עם ה DOM: בנייה ראשונית של האפליקציה (על בסיס state מסוים) ועדכון ה DOM הקיים בנתונים חדשים. באפליקציות SPA, עדכון ה DOM הוא חלק משמעותי מהאפליקציה.

ראקט מציעה לנו לבטל את עדכון ה DOM: כאשר יש צורך בעדכון כלשהו, פשוט נהרוס את ה DOM של האפליקציה כולו – ונבנה את כולו מחדש בעזרת לוגיקת “הבנייה הראשונית”.
פשוט, לא?

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

רעיון שלישי ואחרון של ראקט הוא חלוקה מחדש של ה concerns בצד הלקוח: במקום הפרדה בין UI, לוגיקה, נתונים ואולי שליטה (וריאציות שונות של MVC) – ראקט מציעה הפרדה לרכיבים המחולקים ע”פ “יכולות עסקיות”, כאשר כל רכיב אחראי על הנתונים, הלוגיקה וה UI שלו בעצמו.

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

רבים ממשתמשי ראקט לא מאמצים / מאמצים חלקית בלבד את הרעיון השלישי, ומשתמשים בראקט בתור שכבת UI (קרי View) לארכיטקטורת MVC סטנדרטית. MVC היא אולי לא מושלמת – אך היא מוכרת ומובנת (כלומר: כל קבוצה מכירה אותה בדרך משלה).
שימוש בראקט עם Angular או Ember אפשרי, אך הוא לא נפוץ. השימוש בראקט מייתר חלק מיכולות Angular/Ember ומחליש את הסדר המובנה שהן מציעות. שימוש בראקט עם Backbone.js, הוא דווקא טבעי בהרבה – כאשר ראקט מחליפה את ספריית ה Template ליצירת Views.

יש כאלו הממליצים להתחיל לעבוד עם ראקט כ View של Backbone, ורק לאחר שמשיגים בצורה זו שליטה – להמשיך הלאה.

סיכום ביניים:

  • ראקט לא מציעה MVC, או תחליף ישיר ל MVC.
  • ראקט מהווה סוג של תחליף ל Templates (וקצת יותר מכך). במקום שפת template “מנוונת” – ראקט מאפשרת להשתמש (פחות או יותר) בג’אווהסקריפט לבניית ה markup.
  • ראקט מערבבת בין ג’אווהסקריפט לבין “HTML) Markup)”, אך מפרידה בין רכיבים על הדף – כל אחד אחראי על הפונקציונליות שלו.
  • ראקט יעילה מאוד ברינדור, ומתאימה לאפליקציות אינטנסיביות ברינדור (בעיקר כאלו שעובדות על ה DOM).
    אציין שגם Ember יעילה למדי בתחום זה (בעזרת Handlebars) – אולם נראה שהקוד יהיה פחות קריא.

כיצד ראקט עובדת

React.js היא ספרייה פשוטה, יחסית, בעלת אלמנטים ספורים.

הגדרת רכיבים (Components) נעשית ע”י המבנה הבא:

var = React.createClass({
  method1: function(…){..};
  method2: function(…){..};
});
לא שונה בהרבה מ Backbone, Angular, או ספריות נפוצות אחרות.

בראקט ישנה הבחנה בין 2 סוגי רכיבים:
  • רכיבים שהם Immutable
  • רכיבים בעלי state
שני הרכיבים נוצרים ע”י createClass, אך הם שונים בשימוש שלהם ב State: האם כותבים משהו על ה state של הרכיב או לא. ראקט מעודדת לנסות ולכתוב כמה שיותר רכיבים Immutable – עבור יתרון בביצועים. כדי לחסוך ב Garbage Collections ראקט מבצעת pooling של אובייקטים (כגון Events) ועושה בהם שימוש-חוזר.הרכיבים מושפעים בעת הרינדור שלהם מ2 מקורות: ה properties – שנקבעים בעת יצירת המופע של הרכיב (ולא ניתן לשנותם מרגע זה) וה state – שנשמר על הרכיב, וניתן לשנותו בכל רגע. בדכ ה properties ישמשו גורם חיצוני לקבוע את התנהגות הרכיב, בעוד ה state הוא פנימי לשימושו של הרכיב.

אפליקציית ראקט טיפוסית מתחילה בקריאת:

 React.RenderComponent();
היוצרת את רכיב האב וקוראת ל ()render שלו. הוא מצידו יקרא ייצור מופעים של רכיבים בנים (משולבים ב HTML markup) – ויקרא ל render שלהם וכך חוזר חלילה לעומק עץ של רכיבים ו markup.

כאשר יש שינוי ב state של אחד הרכיבים – פונקציית ה ()render של אותו רכיב תקרא ותגרום לעדכון של תת העץ שלו.
הרכיבים בראקט מגדירים את המראה שלהם בעזרת Markup ורכיבים בנים, כאשר הם לא כותבים ישירות ל DOM של הדפדפן לא רק ל React.DOM – ה DOM הוירטואלי שראקט מנהלת.

כפי שציינו, כתיבה ל DOM היא פעולה יקרה. יתרה מכך: הדפדפן מחזיק Buffer קטן של כתיבות ל DOM אותן הוא מנסה לאגד לאצווה, אולם כל קריאה מה DOM עשויה להכריח אותו לכתוב ל DOM בנקודת זמן לא רצויה (מדוע? הסבר בפוסט זה, “ההשפעה של פעולות DOM מתוך קוד ג’אווהסקריפט”).
מכיוון שראקט מנהלת עותק שלה של ה DOM, היא יכולה לקרוא ולכתוב ל DOM הווירטואלי שלה מבלי לעדכן את ה DOM של הדפדפן. כמו כן היא יכולה לסדר פעולות ל DOM בצורה אופטימלית מבחינת ביצועים (סדרות של קריאות וסדרות של כתיבות).
כאשר היא רוצה לעדכן את ה DOM של הדפדפן, היא עושה diff יעיל (להלן הסבר האלגוריתם) של ה DOM הווירטואלי וה DOM האמיתי ומעדכנת ב DOM האמיתי רק אלמנטים שבאמת השתנו. כמובן, שכדי שזה יעבוד, אין לערבב על אותו אזור ב DOM את ראקט עם ספריה אחרת שכותבת ל DOM ישירות.

ראקט מחלקת את הטקסט לרכיבים אטומיים קטנים, כך שיהיה אפשר לעדכן רק את ה nodes של הערכים (בירוק), מבלי לעדכן את הערכים של ה Labels (בצהוב). שימו לב למספור שראקט מייצרת על ה DOM (תכונת data-reactid) – בעזרתו היא מבצעת השאוות יעילות של ה DOM.
הרעיון של Virtual DOM אינו חדש, אגב: Backbase הציגה מין Virtual DOM משלה לפני שנים (היא כנראה לא היחידה), וטכניקה דומה נמצאת בשימוש במנועים גרפיים של משחקי מחשב.
הניתוק מה DOM מאפשר לראקט גם לרוץ בצד השרת (עבור מכשירים חלשים / sub-second load time) – ממש כמו rendr עבור Backbone.js.
השוואה לא מדוייקת בין האתר Hacker News (שורה עליונה) ו clone שלו שכתבנו בראקט (שורה תחתונה). אדום – טעינה ראשונית. בכחול – פעולת דפדפוף בין הכתבות (האתר שלנו היה הרבה-הרבה יותר מהיר). פרופיל ה timeline שקבלנו מקוד הראקט מאוד לא אופייני לאתר / אפליקציה שלא עברה עדיין שום אופטימיזציית ביצועים.

דוגמת קוד

אין כמו לראות קצת קוד.
הנה אפליקציה פשוטה למדי עם 2 כפתורים (++X ו ++Y) המעלים את הערכים של ה counters המתאימים במרכז המסך.

התוצאה הסופית של האפליקציה

קוד ב react ניתן לכתוב באחד מ2 סגננונות: JSX ו”ג’אווהסקריפט נקי”.
JSX הוא הרחבה לג’אווהסקריפט עם XML (מבוסס E4X?), המאפשרת לכתוב snippets של XML בתוך הג’אווהסקריפט ע”פ חוקים מסויימים.

JSX אמור להיות פשוט יותר ונקי יותר לכתיבה – ועל כן מועדף ע”י החלק הגדול של מפתחי ראקט. ה JSX מקומפל לג’אווהסקריפט רגיל לפני שהוא מורץ על הדפדפן. ניתן לקמפל אותו בתוך הדפדפן ע”י הוספת קובץ ג’אווהסקריפט בשם JSXTransformer (לפיתוח מהיר יותר) או בצד השרת ע”י רכיב מתאים של Node (לביצועים טובים יותר).

אנו עבדנו ב IDE בשם WebStorm (מומלץ) שקיבל יפה מאוד את הרחבות ה XML בתוך קובצי ה javaScript, והמשיך לתפקד היטב עם קוד הג’אווהסקריפט (hinting, ניווט, וכו’).

עוד וריאציה נפוצה יחסית לשימוש בראקט היא כתיבה ב CoffeeScript על מנת לכתוב קוד מינימליסטי של ראקט ללא JSX.

בדוגמה זו נראה כתיבה בצורת JSX ולאחר מכן, את הגרסה המקומפלת שלה ל”ג’אווהסקריפט נקי”.

  1. כאשר משתמשים ב JSX Transformer בכדי לקמפל JSX “תוך כדי שימוש” (“on the fly”) – חייבים לכלות הערה זו בתחילת הקובץ.
  2. אנו מגדירים רכיב (component) פשוט של ראקט, בשם Simple.
  3. את הרצת התוכנה בראקט מבצעים בעזרת פקודת renderCompoent על הרכיב הראשי. שימו לב לתגים ה Simple – שאיננה סטנדרטית ב HTML, אלא מבטאת את הרכיב שלנו. אנו שולחים לרכיב ה Simple שלנו Property (בלתי ניתן לשינוי immutable) – בשם message ואומרים לראקט להוסיף את הרכיב בתוך document.body (קרי InnerHTML).
  4. אחד השלבים הראשונים במחזור החיים של רכיב react הוא קריאה למתודה ()getInitialState המאתחלת את ה state של הרכיב. בשלב זה, ניתן להבין שזהו stateful component.
  5. שלב מעט מאוחר יותר במחזור החיים של הרכיב, הוא הקריאה למתודה ()render המייצרת את ה markup של הרכיב – ומחזירה אותו לרכיב האב (במקרה זה – זהו רכיב האב).
  6. יצירת ה markup נעשית על ידי כתיבה inline של markup ב JSX. עטפנו הכל בסוגריים כדי שנוכל למקם את ה div הראשון בשורה חדשה (אחרת JavaScript יחשוב שרצינו לכתוב ;return, ואח”כ אולי עוד משהו לא רלוונטי).
  7. סוגריים מסולסלות הוא ה escaping לשילוב ביטויי ג’אווהסקריפט בתוך ה XML (חייב לחזור ערך). במקרה זה אנו קוראים לאובייקט ה properties של הרכיב (שהוא immutable  – בלתי ניתן לשינוי). את הערך של התכונה message קבענו בשלב 3.
  8. באופן דומה אנו רושמים מטפלים לאירועים (incX, incY).
  9. כאשר האירועים נקראים (בעקבות לחיצה של העכבר על אחד הכפתורים), מספיק שנשנה את ה state של הרכיב – כדי ש react ידאג לקרוא ל render, ובצורה יעילה (למשל: לא יותר מפעם אחת ב Animation Frame של הדפדפן).
  10. לתחביר ה XML יש כמה תכונות מיוחדות – למשל הטיפול ב inline style attribute.
ניתן למצוא online JSX compiler בקישור הבא
כפי שאפשר להבחין, ה markup שאנו כותבים ב JSX הוא איננו HTML. זהו XML שרק דומה ל HTML. הסיבה לשימוש ב XML הוא הצורך ב markup קל לפענוח – HTML הוא מאוד סלחן ובעייתי לפענוח.
הדרך הכי בולטת לשים לב להבדל בין ה JSX XML ו HTML היא לקחת דוגמת HTML מהאינטרנט (שעובדת!) ולהדביק אותה לתוך האפליקציה. כמה הבדלים מרכזיים הם:
  1. אלמנטים חייבים להסגר. לא עוד
    אלא רק
    .
  2. מכיוון שה markup מקומפל לג’אווהסקריפט, לא ניתן להשתמש במלים שמורות של שפת ג’אווהסקריפט. למשל, במקום תכונת class משתמשים ב className.
  3. inline style attribute לא נכתב כמו ב HTML, אלא כאובייקט – כמו בדוגמת הקוד למעלה. אפשר גם לכתוב אותה inline בעזרת סוגריים מסולסלים כפולים {{…}}=style.

בסה”כ, בניגוד לציפיותנו, התרגלנו לכתיבת ה XML דיי מהר – והיא לא היוותה מטרד מיוחד.
מעניין לציין ש react עושה escaping ל markup בכדי להגן בפני חולשות XSS.

הנה הקוד לאחר שקומפל ל JavaScript בעזרת ה JSX Transformer:

הודאה קטנה: זה לא בדיוק הקוד. הקוד שנוצר הוא מעט “עמוס” בגלל השימוש ב “React.DOM” בכל מקום – ולכן עשינו לביטוי זה extract למשתנה שפחות תופס את העין: קו תחתון (בירוק).
הכנסנו את הביטוי במקום שורת רווח כדי לא “לקלקל” את מספרי השורות. ה JSX Transformer דואג לשמר את מספרי השורות בין ה source JSX לתוצר ה javaScript שנוצר.

כמה נושאים מעט יותר מתקדמים

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

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

אפשר לנהל את מודל כזה בצורה הקלאסי (חיבור למודל backbone, למשל) או לתת לרכיב להביא את המידע לו הוא זקוק ישירות מהשרת בעזרת קריאת Ajax (גישה של micro-services – כל רכיב דואג לעצמו מא’ ועד ת’).

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

  1. List רכיב שמיצג את הרשימה כולה – רכיב זה ייצור ListItem עבור כל אחת מהרשומות במודל שלנו, וגם ינהל את הרשימה במידה ויש צורך.
  2. ListItem רכיב שמייצג כל רשומה – רכיב זה יצטרך להכיר את המאפיינים של כל רשומה וידע להציג אותם כראוי.

כזכור, בראקט כל אחד מהרכיבים מחזיר בפונקציות ה () renderאת מבנה ה DOM שעליו הוא אחראי.
בדוגמה שלפנינו, כל רשומה היא בעצם DIV שמכיל את מבנה ה DOM של הרשומה. רכיב ה ListItem יחזיר אך ורק את ה DOM שנדרש בשביל להציג את הרשומה הבודדת.
רכיב ה List שאמור להציג את כלל הרשומות, יחזיר רק את העטיפה הנדרשת לכלל הרשומות, במקרה שלנו רק DIV עוטף אחד.

  1. זוהי הגדרת רכיב האב, ה List. בעת יצירתו הרכיב מקבל startIndex – מאיזה מספר רשומה להציג את ה listItems ו data – הנתונים עצמם המגיעים מהשרת מבנה json.
  2. דרך יעילה לרנדר רשימה של אברים היא היא לעבור על נתוני המקור (this.props.data) בעזרת פונקציית map (של מערך בג’אווהסקריפט) וליצור מכל פריט נתונים – מופע של הרכיב (ListItem).
    לא הייתה מניעה, אגב ש ListItem יהיה מורכב גם הוא מתתי-רכיבים. במקרה שלנו הפרדנו רק אלמנט אחד לרכיב-בן: Comment (האלמנט האחרון).
  3. הרכיב ListItem הוא עצמאי ואינו מודע ל List. אחד הרעיונות בראקט הוא לייצר רכיבים הניתנים לשימוש חוזר.
  4. ניתן לראות כיצד ListItem משתמש בטבעיות כ properties שהושמו לו ביצירה (שלב 2) – אלו בעצם הנתונים כפי שקיבלנו אותם מהשרת.
  5. שראקט יודע לטפל ב markup (למשל: אלמנט div), רכיבים (למשל ) או במערך של רכיבים / אלמנטי markup – כמו במקרה זה.
בעקבות שלב 5 בקוד, ראקט יוציא לנו הערה ב console של הדפדפן בזמן ריצה:
לא כדאי להתעלם מהערות אלו (ואחרות שהוא מוציא): כאשר יש רשימה, ראקט מבקש מאתנו מפתח ייחודי שבעזרתו יזהה שינוי state באובייקטי הרשימה. אחרת, האופטימיזציות של ראקט עלולות לגרום לו לדלג על שינויים שנעשו באובייקט – מבלי לעדכן אותם על המסך.
הנה דוגמה ל key כזה שהוספנו לאובייקט הרשימה בפרויקט שלנו:



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

יש עוד כמה נושאים ששווים ציון, אך היריעה קצרה להכיל. הנה מספר נקודות התייחסות אליהם:
מחזור החיים של הרכיב
כדאי להכיר: Component Lifecycle Event
כשתכתבו רכיבים מורכבים יותר – כנראה שתוכלו להיעזר בשלבים שונים ב lifecycle.
ממשק חיצוני של רכיב
כפי שאמרנו, רכיבים נבנו לשימוש חוזר / תחוף. ראקט מספקת מספר כלים כדי לכתוב רכיבים עמידים (robust):
  • default props – ערכי default במידה והמשתמש לא הגדיר את Property נדרש.
  • propTypes – מנגנון אימות על properties: האם יש property שחייבים להגדיר (isRequired), האם יש Property שסט הערכים התקניים שלו הוא סגור ( ([…])isRequired, ) וכו’.
שימוש חוזר בקוד בין רכיבים
במקום להסתבך עם prototype chains, ראקט מציעה מנגנון נוח יותר של mixins, לשיתוף התנהגות בין כמה components שונים.
שינויים נקודתיים
על רכיב ניתן לשמור קישורים (תכונת refs) לאובייקטים מסוימים ב DOM בכדי לבדוק / לעדכן אותם בצורה נקודתית.

סיכום ורשמים אישיים

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

התיעוד שלה – לא טוב. את רוב התשובות מצאנו ב stackoverflow וכמעט לא בתיעוד הרשמי. נציין שכל נושא שלא עניין אותנו, דווקא היה מתועד בצורה מופתית! (“חוק מרפי של התיעוד”).נתקענו כמה פעמים על בעיות, לזמן לא קצר – אך בסוף הצלחנו לפתור את הבעיות ולהבין את מקורן. למשל גילינו, בדרך הקשה, שקביעת ערך לאובייקט ה state (כלומר (…)setState) לא מתרחשת מיידית, אלא היא מנוהלת בתור ומבוצעת רק לפני ציור ה Activation Frame (כלומר האירוע בו הדפדפן מעדכן את תצוגת המסך, שאמור להתרחש 60 פעמים בשנייה). קביעת ערך בפונציה א’ לא מבטיח שזה הערך שיקרא מאובייקט ה state בפונקציה ב’ שמתרחשת מיד לאחר מכן. עדיף להעביר את הערך כפרטמר לפונקציה ולא להסתמך על ה state שיהיה מעודכן.

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

היינו מנסים את react.js בעתיד באפליקציות לא-גדולות ועתירות רינדור (הייתה לנו אחת כזו לפני מספר חודשים).
מכיוון שרקאט היא לא כ”כ דומה ל frameworks אחרים, כדאי כנראה להשתמש בה רק בצוות שירגיש נוח איתה – לרוב מפתחי client-side hardcore הרגילים לכתוב קרוב ל DOM.
שאר המפתחים – יוכלו להמתין שראקט תתבגר עוד קצת ותגיע לגרסה…נאמר, 0.5.

כותבים שותפים:




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



איתמר שגב הוא מפתח Client-Side ב SAP מאוד אוהב טכנולוגיות חדשות ובעיקר את כל מה שגוגל עושה.

קישורים

[א] בהשוואה לאנגולר. ניתן להזכיר שגוגל לא משתמשת באנגולר במוצרים המרכזיים שלה.