2019-05-14

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...

8 תגובות:

  1. אחלה פוסט, כרגיל תקציר מעולה של החידושים האחרונים. שווה להזכיר את Babel ואת הדחיפה שלו לקפיצה של JavaScript בשנים האחרונות. הכלי מאפשר לתרגם (to transpile) קוד שנכתב ב-ES6 לקוד שנכתב ב-ES5, ובכך לאפשר למפתחים להשתמש בתכונות מתקדמות של השפה בלי חשש לבעיות תאימות בדפדפנים. הכלי (או כלים אחרים דומים כמו המפרשן של TypeScript) הינו כלי מרכזי ובסיסי בכל פיתוח מודרני שמתבצע היום בשפה.
    כמו כן לגבי מודולים (ES Modules), שווה להזכיר שהם עדיין לא חלק מהתקן באופן רשמי, ולכן לא ניתן להשתמש בהם ללא כלים כמו Babel.

    השבמחק
    תשובות
    1. תודה אלון על התוספות 👍👍

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

      מחק
  2. תגובה זו הוסרה על ידי המחבר.

    השבמחק
  3. אנונימי15/5/19 11:54

    פוסט מעולה וקל לצריכה
    תודה!

    השבמחק
    תשובות
    1. תודה רבה! שמח לשמוע! 👍

      מחק
  4. אנונימי4/8/19 08:56

    לא deconstructing אלא destructuring

    השבמחק
  5. אנונימי14/3/20 14:28

    shalom :)

    השבמחק