מה רע ב-JavaScript

בשנת 1995, בעיצומה של מלחמת הדפדפנים בין נטסקייפ למיקרוסופט, היה ברור שלדפדפן אמורה להיות יכולת להריץ קוד בנוסף ליכולת להציג טקסט גרידא. באפריל גייסה נטסקייפ את ברנדן אייק (Brendan Eich) כדי להטמיע בדפדפן מנוע שיריץ שפת תכנות מבוססת Scheme. למרות שאייק רצה שהסינטקס יהיה דומה לסינטקס של Scheme, הוחלט בנטסקייפ שכדי ששפת התכנות תוכל להתחרות במה שיציגו מיקרוסופט בדפדפן שלהם, היא צריכה להיות בעלת סינטקס שדומה לג׳אווה. כדי שהשפה תתקבל כסטנדרט היה צריך להוציא פרוטוטייפ שלה במהירות, וכך אחרי עשרה ימי עבודה בחודש מאי שחרר אייק את הפרוטוטייפ, לו הוא קרא Mocha. הוא המשיך לעבוד על השפה ובספטמבר שחרר גירסה בשם LiveScript ששוחררה לראשונה בגירסת בטא כחלק מהדפדפן של נטסקייפ. בדצמבר שוחררה הגירסה בשמה המוכר לנו, JavaScript.

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

בהינתן נקודת הפתיחה החפוזה הזאת, אין פלא שהתוצאה היא שפה שאפשר למצוא בה החלטות תכנון גרועות, חסרים בה פיצ׳רים בסיסיים, יש בה תמיכה חלקית בלבד בתכנות מונחה עצמים ויש בה כמה וכמה נקודות מוזרות וכואבות. בדיחת מתכנתים מוכרת משווה בין שני ספרים של אוריילי, JavaScript: The Definitive Guide המכיל 1096 עמודים, ו-JavaScript: The Good Parts המכיל 172 עמודים בלבד.

JavaScript The Good Parts

אם לא די בכך שנקודת ההתחלה של השפה הייתה בעייתית, במשך זמן רב השפה לא התפתחה ולא יצאו לה עדכונים. בשנת 2000 יצאה הגירסה ES5 שהיוותה את העדכון האחרון של השפה עד 2015, אז הוציא ארגון התקינה העולמי לאינטרנט את הגירסה ES6 שמהווה שינוי משמעותי ומאז יוצאים עדכונים באופן קבוע. הגירסאות האחרונות נתמכות על ידי כל הדפדפנים העיקריים, וניתן לתמוך בהן גם בדפדפנים ישנים על ידי כלים כמו babel. עדיין קיימות בשפה כמה וכמה בעיות, חלקן חמורות יותר וחלקן פחות, חלקן ניתנות לפתרון וחלקן לא. במאמר הזה ובמעמד הבא נדבר על הבעיות העיקריות האלו. חשוב להבהיר שאנחנו נתמקד בבעיות שקיימות בגירסה הנוכחית של השפה, נכון למאי 2019, ולא נזכיר בעיות שנפתרו על ידי אחד מהעדכונים.
אנחנו נשתדל להימנע מדיון בסגנון ״אין על Python״ או ״JavaScript היא שפה גרועה״, וננסה להסביר בהיגיון את החסרונות שיש לשפה. את המסקנה האם JavaScript היא שפה טובה או לא והאם להשתמש בה נשאיר לכם.

המרת טיפוסים

מצגות WAT הן מצגות המראות התנהגויות מוזרות שבתגובה אליהם התגובה של רוב האנשים היא WAT, הראשונה שבהן ניתנה ב-2012.

WAT

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

ב-JavaScript בדיקת שוויון המוכרת משפות התכנות האחרות נעשית על ידי אופרטור השוויון ===. אם אנו משווים באמצעות האופרטור הזה את הערך של המספר 1 והמחרוזת "1" אנו מקבלים כצפוי false, משום שהם אכן ערכים שונים. עם זאת, בשפה קיים אופרטור שוויון נוסף, חלש יותר, בו כדי ששני משתנים יהיו שווים לא צריך שיהיה ביניהם שוויון מוחלט אלא מספיק שוויון עד כדי המרה אוטומטית שנעשית בין הערכים. בשימוש באופרטור הזה ההשוואה לעיל מחזירה true. הדוגמה הזאת עדיין סבירה ואינטואיטיבית, אבל ההמרה האוטומטית בין הטיפוסים מסתבכת מהר מאוד.

> [2,3].toString()
'2,3'
> [].toString()
''
> Boolean([].toString())
false
> Boolean([])
true

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

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

המרה אוטומטית של טיפוסים נעשית גם כאשר משתמשים באופרטורים כמו אופרטור החיבור:

> [] + []
''
> [] + {}
'[object Object]'
> {} + []
0
view raw plus-casting.js hosted with ❤ by GitHub

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

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

ערך לא מוגדר

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

> console.log(x);
Uncaught ReferenceError: x is not defined

נתבונן כעת בקוד הבא:

> function f(x) { console.log(x); }
> f()
undefined

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

> var x = undefined;
> console.log(x);
undefined

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

null vs. undefined

בסעיף הקודם ראינו שהטיפוס undefined מסמל משתנה שאין בו ערך. בשפות תכנות רבות קיים ערך המסמן משתנה שאין לו ערך. בשפות C, Java ודומותיהן קוראים לערך null, ברובי קוראים לו nil ובפייתון None. יש כמה הבדלים קטנים בין השפות (למשל, בפייתון הערך None הוא טיפוס בפני עצמו ואילו ב-Java הערך null נשמר במשתנה מהטיפוס המקורי, וזאת משום שפייתון היא שפה דינמית וג׳אווה היא סטטית), אך הרעיון והמימוש דומים.

ב-JavaScript יש לנו שני ערכים כאלו, והם undefined שראינו לעיל ו-null. הערכים האלו שווים בערכם אך הם מטיפוסים שונים.

> null == undefined
true
> null === undefined
false
> !!null === !!undefined
true
> typeof null
"object"
> typeof undefined
"undefined"

על פי ברנדן אייק המשמעות של null היא שאין אובייקט ואילו undefined משמעו שאין ערך, ולכן ה-type של null הוא object. דהיינו, null הוא משתנה המיועד לאובייקט שכרגע לא מכיל אובייקט ואילו undefined הוא משתנה שלא היינו אמורים להגיע אליו משום שהוא לא הוגדר כלל. עם זאת, החלוקה הזו לא תמיד מדויקת: ראינו שאפשר להפוך משתנה ל-undefined על ידי השמה, כך שאין זה נכון שהוא לא הוגדר כלל. כמו כן, בכל הנוגע להשוואות עם ערכים אחרים או להמרה לטיפוסים אחרים, שני הטיפוסים הללו זהים לחלוטין, כך שקשה להצדיק את קיומם של שניהם זה לצד זה.

התעלמות משגיאות

נתבונן בקוד הבא:

> function f(x) { console.log(x); }
> f()
undefined

או בקוד הזה:

> var o = {};
> console.log(o.x);
undefined

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

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

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

סיכום ביניים

ראינו כמה תכונות של JavaScript שיש לגביהן קונצנזוס כי הן תוצאה של תכנון לקוי. במאמר הבא נראה עוד כמה תכונות כמו מילת הקסם this, ה-scoping המסורבל של השפה ועוד.

חלק ב: מה עוד רע ב-JavaScript.

 

מקור תמונה:

JavaScript The Good Parts – https://www.flickr.com/photos/nathansmith/ – Nathan Smith

10 thoughts on “מה רע ב-JavaScript

Add yours

      1. לצערי הרב, אין מספיק דברים טובים ב-JS. 🙂
        כמובן שיש אבל אני אישית לא מת על השפה הזו. אני משתמש ביום יום בה ובפייתון ולטעמי פייתון הרבה יותר טובה.

נשמח לשמוע מה אתם חושבים על המאמר

Up ↑

לגלות עוד מהאתר camelCase

כדי להמשיך לקרוא ולקבל גישה לארכיון המלא יש להירשם עכשיו.

להמשיך לקרוא

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

נרשמת בהצלח. בתודה, הגמל.

שגיאה בלתי צפויה, אנא נסה שוב.

camelCase will use the information you provide on this form to be in touch with you and to provide updates and marketing.