מה עוד רע ב-JavaScript

מאמר זה הוא המשכו של המאמר הקודם בו דיברנו על כמה חסרונות של שפת Javascript. במאמר הזה נעבור על עוד כמה דברים ש-JavaScript פחות טובה בהם כמו this, error handling ועל הספריה הסטנדרטית שלה.

this

נסתכל על הקוד הבא:

class A {
f() { console.log(1); }
g(){ setTimeout(function() { this.f(); }, 1000)}
}
function f() { console.log(2); }
var a = new A()
a.g()
> 2
view raw this_problem.js hosted with ❤ by GitHub

בניגוד ל-Java ושפות אחרות, המשתנה this לא מצביע על האובייקט אלא על ה-scope של הקריאה לפונקציה. במקרים רבים ה-scope הוא אכן האובייקט שאליו שייכת הפונקציה. אך במקרים אחרים, כמו בעת קריאה לפונקציה setTimeout, ה-scope של הקריאה לפונקציה איננו האובייקט אליו שייכת הפונקציה אלא ה-scope הגלובלי. בדוגמא הזאת, הפונקציה f שמוגדרת ב-scope הראשי ושמדפיסה 2 היא זו שנקראת, ולא פונקציית המחלקה שמדפיסה 1. אם לא היינו מגדירים את הפונקציה f ב-scope הגלובלי היינו מקבלים שגיאה שהאובייקט f אינו מוגדר.
יוצא שמקריאת הקוד של מחלקה A אי אפשר להבין מה יבצע הקוד, משום שהביצוע תלוי ב-scope שנקבע בזמן הריצה של הקוד. כתוצאה מכך אנו מקבלים קוד לא אינטואיטיבי שלא מסביר את עצמו. בנוסף, המוסכמה ברוב שפות התכנות היא ש-this מצביע על המופע של המחלקה שעליו רצים. תסכול רב היה מנת חלקם של מי שנאלצו ללמוד לאורך השנים בדרך הקשה של-this ב-JavaScript יש משמעות שונה.

worried-man

כיום אפשר להימנע מהבעיה. בשנת 2015 נוסף לשפה הפיצ'ר פונקציות חץ (Arrow Function). הפונקציות הללו שומרות על ה-scope בהן הן נוצרות וכך נשמרת המשמעות האינטואיטיבית של this. הקוד הבא ידפיס 1 כמצופה.

class A {
f = () => { console.log(1); }
g = () => setTimeout(() => this.f(), 1000)
}
function f() { console.log(2); }
var a = new A()
a.g()
> 1
view raw this_arrow.js hosted with ❤ by GitHub

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

שגיאות חסרות טיפוסים

בכל שפת תכנות מודרנית יש מנגנון לזריקת ותפיסת שגיאות. למשל בג׳אווה:

try {
System.out.println(obj.charAt(0));
} catch(NullPointerException e) {
System.out.println("NullPointerException..");
} catch(MyAwesomeCustomException e) {
System.out.println("Caught an exception: " + e.myCustomInformation);
}

או בפייתון:

try:
ratio = a / b
except ZeroDivisionError:
ratio = float(‘inf’)
except TypeError as e:
logger.critical(‘Got TypeError: %s’, e)

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

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

try {
const y = x
} catch(err) {
console.log(err.name + ': ' + err.message);
}
> ReferenceError: x is not defined

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

class MyError extends Error {}
try {
doStuff();
} catch(err) {
if (err.name == 'MyError') {
console.log("This is my error");
} else if (err.name == 'ReferenceError') {
console.log("Not my error. Ask the ReferenceError guys");
} else {
console.log("I have no idea");
throw err;
}
}

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

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

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

try {
throw “this is an error”;
} catch(err) {
console.log(err.name + ': ' + err.message);
}
> undefined: undefined

בשפות אחרות מקובל שכדי לזרוק אובייקט כשגיאה עליו לרשת מ-Exeption או להיות throwable בצורה כלשהי. ב-JavaScript אפשר לזרוק ממש כל אובייקט.

try {
throw new Date();
} catch(err) {
console.log(err);
}
Tue Jun 25 2019 19:12:20 GMT+0300 (Israel Daylight Time)

אם אנחנו כותבים גם את הקוד שמטפל בשגיאה שנתפסה זה לא נורא, פרט לעובדה שמדובר בקונבנציה רעה. אך הדבר יכול להוביל לשגיאה של ממש במערכות שמניחות מבנה של שגיאה כדי להדפיס stack trace וכדומה, כמו ב-node.
עד ES6 היה אפשר לזרוק רק שישה סוגים של שגיאות חוץ מ-Error הבסיסי.

חוסר בספרייה סטנדרטית

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

>>> x = {'a': 1, 'b': 2}
>>> y = {'b': 2, 'a': 1}
>>> x == y
True

או בג׳אווה:

HashMap<String, Object> map1 = new HashMap<String, Object>();
map1.put("a", 1);
map1.put("b", 2);
TreeMap<String, Object> map2 = new TreeMap<String, Object>();
map2.put("b", 2)
map2.put("a", 1);
map1.equals(map2); // true

לעומת זאת ב-JavaScript השוואה כזו לא נותנת תוצאה דומה.

x = {'a': 1, 'b': 2}
y = {'b': 2, 'a': 1}
x === y // false
x == y // false

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

x = {'a': 1, 'b': 2}
y = {'b': 2, 'a': 1}
_.isEqual(x, y) // true

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

>>> a = [3, 2, 1]
>>> sorted(a)
[1, 2, 3]
>>> a
[3, 2, 1]
>>> a.sort()
>>> a
[1, 2, 3]

ב-JavaScript ניתן לבצע רק מיון במקום שמשנה את המערך המקורי.

>>> var a = [3, 2, 1];
>>> var b = a.sort();
>>> b
[1, 2, 3]
>>> a
[1, 2, 3]

במקרה שנרצה להציב במשתנה b את המערך a הממויין תוך כדי שמירה על הסדר המקורי של a נצטרך להשתמש בספרייה כדוגמת immutable-sort:

>>> const sort = require('immutable-sort')
>>> var a = [3, 2, 1];
>>> var b = sort(a);
>>> b
[1, 2, 3]
>>> a
[3, 2, 1]

למה זה לא טוב?

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

>>> f'{7:03}'
'007'

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

>>> (7).toString().padStart(3, '0');
"007"

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

יש הצעה להוסיף ל-JavaScript ספרייה סטנדרטית, אבל לא ברור האם ומתי זה יקרה.

סיכום

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

Business photo created by yanalya – www.freepik.com

15 תגובות בנושא “מה עוד רע ב-JavaScript

הוסיפו את שלכם

  1. אני ראשון!!!1 היה ליגה, נראה לי שזאת רק ההתחלה של מה רע בג'וואה סקיפט, כנראה עדיף לרשום מה טוב ולסיים מהר 😉

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

      סוף הפוסט.

  2. JS זה הילד הרע שכולם סולחים לו בסוף… הוא פה, הוא איתנו, עכשיו בES6 הוא נראה הרבה יותר בוגר, והוא כאן בשביל להשאר. אני מאמין שעוד כמה שנים חלק מהדברים הרעים (מאוד אפילו, כמו scoping) יהיו אך נחלת העבר, ואנחנו נמשיך לרוות מהשפה הזו נחת.

  3. תודה. בנוגע לפסקה האחרונה, את\ם שולל שימוש ב Node לצורך פיתוח בצד שרת? כלומר, אם הפופולריות וההכרח להשתמש ב js נובעים מהמונופול שלה בדפדפן, היית מעדיף שלא להשתמש בה בצד שרת כל עוד ישנן אופציות אחרות?

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

  4. מעניין מאוד. המונופול של js  יכול להישבר אם גוגל תחליט למשל להפוך את Flutter לספריה המובילה לכתיבת Web Apps ולתת לאפליקציות שנכתבו בה עדיפות מסוימת בCrome

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

  5. מבלי להסביר *למה* JS הגיעה לאיפה שהיא הגיעה, מאיפה היא ירשה את הסמנטיקה, ומה המגבלות שנובעות מהDESIGN GOALS שלה, ושהיא תמיד צריכה להיות EMBDED במשהו,
    המאמר הזה הוא יותר כמו "דברים שאני לא אוהב בJS למרות שאני לא כ"כ מכיר אותה"

    JS היא לא "מונופול", אלא הSPEC היחידי שהוסכם ע"י מפתחי הדפדפנים במשותף,
    שאגב זה כבר לא נכון ב100%, כי יש את WEB ASSEMBLY

    1. אני משתמש ב-js ביום יום.

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

      את הפתיחה לא הבנתי. אשמח אם תסביר.
      אתה מוזמן גם לקרוא את החלק הראשון שם הרחבנו יותר על הסיפור של JS.
      https://camelcase.blog/what-bad-in-javascript/

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

  7. חשוב לציין שיש חלקים יפים יותר.
    אישית אוהב את השפה מאוד.
    מציע שוב, אולי תפרסם מאמר מה טוב בjs. אשמח לעזור בזה.

  8. אני חושב שרע וטוב זה סובייקטיבי.. לעומת אמת ושקר שזה דבר מוחלט. ולכן אני חולק על רוב תוכן המאמר.
    JS היא שפה דינמית נוחה וקלה שבאה לעשות את החיים למפתחים קלים!
    נכון, שמי שלא *באמת* יודע JS יכול לחטוף ממנה כאפות על ימין ועל שמאל, ולשבור את הראש כדי להבין מאיפה צץ הבאג, אבל מי שיודע ומכיר את ההתנהגות של השפה לא נופל מהדברים האלה, ואדרבה הרבה פעמים שמח, כי זה עושה לו את החיים ומהירות הפיתוח קלים ומהירים.
    אז נכון, לפרוייקטים בסדר גודל של פיתוח חללית – לא מומלץ להשתמש בה, אבל לפרוייקטים קטנים ובינונים – בהחלט כן.
    וחסרונות אכן יש בJS, אבל חסרונות יש בכל אחד..

    אני אישית באתי משפת C# והיה לי קצת קשה בהתחלה להתרגל לראש הפתוח של JS – אבל אחרי שאתה מתרגל ויודע איך לעבוד איתה באמת (ולא! לא רק כותב איתה קוד.. אלא באמת יודע אותה) אז אין על JS!!!

    1. גם בפה הכי טובה יש כנראה דברים רעים. זה שבפה יש דבר רע לא אומר שאתה לא תהנה ממנה. אנחנו יכולים (ומתכננים) לכתוב כזה מאמר על שפות נוספות וזה לא אומר שאנחנו לא אוהבים אותם.

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

  9. למה לא מוציאים js 2.0?

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

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

    אני מניח שבאנשהו יש דיונים מפורטים על זה, ולמה כן ולמה לא.

    אז אני בינתיים לא מבין למה לא.

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

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

ערכת עיצוב: Baskerville 2 של Anders Noren.

למעלה ↑

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

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

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

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