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 – ואולי עוד כלי או שניים.

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

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

שיהיה בצלחה!

4 תגובות בנושא “JavaScript ES6/7/8 – להשלים פערים, ומהר – חלק ב'

להגיב על אנונימי לבטל