נספח

ירושה של אב טיפוס

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

לדוגמה, לליטרל מחרוזת אין שיטות משל עצמו, אבל אפשר לקרוא ל-method .toUpperCase() לגביו הודות ל-wrapper של האובייקט String המתאים:

"this is a string literal".toUpperCase();
> THIS IS A STRING LITERAL

השיטה הזו נקראת ירושה של אב טיפוס – ירושה של מאפיינים ושיטות מהבנאי המתאים של ערך.

Number.prototype
> Number { 0 }
>  constructor: function Number()
>  toExponential: function toExponential()
>  toFixed: function toFixed()
>  toLocaleString: function toLocaleString()
>  toPrecision: function toPrecision()
>  toString: function toString()
>  valueOf: function valueOf()
>  <prototype>: Object { … }

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

const myString = new String( "I'm a string." );

myString;
> String { "I'm a string." }

typeof myString;
> "object"

myString.valueOf();
> "I'm a string."

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

const numberOne = new Number(1);
const numberTwo = new Number(2);

numberOne;
> Number { 1 }

typeof numberOne;
> "object"

numberTwo;
> Number { 2 }

typeof numberTwo;
> "object"

numberOne + numberTwo;
> 3

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

let stringLiteral = "String literal."

typeof stringLiteral;
> "string"

let stringObject = new String( "String object." );

stringObject
> "object"

הדבר עלול לסבך את השימוש באופרטורים של השוואות מחמירים:

const myStringLiteral = "My string";
const myStringObject = new String( "My string" );

myStringLiteral === "My string";
> true

myStringObject === "My string";
> false

הוספה אוטומטית של נקודה-פסיק (ASI)

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

  • האסימון מופרד מהאסימון הקודם באמצעות מעבר שורה.
  • האסימון הוא }.
  • האסימון הקודם הוא ), והנקודה-פסיק שהוכנסה תהיה הסיומת של המשפט do...while.

מידע נוסף זמין בכללים של ASI.

לדוגמה, השמטת נקודה-פסיק אחרי ההצהרות הבאות לא תגרום לשגיאת תחביר בגלל ASI:

const myVariable = 2
myVariable + 3
> 5

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

const myVariable = 2 myVariable + 3
> Uncaught SyntaxError: unexpected token: identifier

const myVariable = 2; myVariable + 3;
> 5

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

מצב קפדני

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

ES5 מטפל בכמה בעיות מתמשכות בסמנטיקה של JavaScript בלי לשבור הטמעות קיימות, על ידי הוספת 'מצב מחמיר', שמאפשר לבחור קבוצה מגבילה יותר של כללי שפה לסקריפט שלם או לפונקציה בודדת. כדי להפעיל מצב קפדני, משתמשים בליטרל המחרוזת "use strict" ואחריו נקודה ופסיק, בשורה הראשונה של הסקריפט או הפונקציה:

"use strict";
function myFunction() {
  "use strict";
}

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

(function() {
  mySloppyGlobal = true;
}());

mySloppyGlobal;
> true

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

(function() {
    "use strict";
    mySloppyGlobal = true;
}());
> Uncaught ReferenceError: assignment to undeclared variable mySloppyGlobal

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

(function() {
    "use strict";
    let myVariable = "String.";
    console.log( myVariable );
    sloppyGlobal = true;
}());
> "String."
> Uncaught ReferenceError: assignment to undeclared variable sloppyGlobal

(function() {
    let myVariable = "String.";
    "use strict";
    console.log( myVariable );
    sloppyGlobal = true;
}());
> "String." // Because there was code prior to "use strict", this variable still pollutes the global scope

לפי הפניה, לפי ערך

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

כשערך ראשוני מוקצה ממשתנה אחד למשתנה, מנוע ה-JavaScript יוצר עותק של הערך ומקצה אותו למשתנה.

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

const myObject = {};
const myObjectReference = myObject;

myObjectReference.myProperty = true;

myObject;
> Object { myProperty: true }

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

const myObject = {};
const myReferencedObject = myObject;
const myNewObject = {};

myObject === myNewObject;
> false

myObject === myReferencedObject;
> true

הקצאת זיכרון

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

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

ה-thread הראשי

JavaScript היא שפה עם שרשור יחיד עם מודל הפעלה 'סינכרוני', כלומר הוא יכול לבצע רק משימה אחת בכל פעם. ההקשר של הביצוע הרציף נקרא ה-thread הראשי.

ה-thread הראשי משותף למשימות אחרות של הדפדפן, כמו ניתוח HTML, רינדור ועיבוד מחדש של חלקים בדף, הפעלת אנימציות CSS וטיפול באינטראקציות של משתמשים, מהפשוטה (כמו הדגשת טקסט) ועד למורכבות (כמו אינטראקציה עם רכיבי טופס). ספקי דפדפנים מצאו דרכים לבצע אופטימיזציה למשימות שמבצעות ה-thread הראשי, אבל סקריפטים מורכבים יותר עדיין יכולים לצרוך יותר מדי משאבים ב-thread הראשי ולהשפיע על הביצועים הכוללים של הדף.

אפשר לבצע חלק מהמשימות בשרשורי רקע שנקראים Web Workers, בכפוף למגבלות מסוימות:

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

בזכות המגבלות האלה הן אידיאליות למשימות ממוקדות ודורשות משאבים, שאחרת היו יכולות לתפוס את ה-thread הראשי.

מקבץ השיחות

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

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

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

לולאת האירוע ותור הקריאה החוזרת (callback)

המשמעות של הביצוע הרציף הזה הוא שמשימות אסינכרוניות שכוללות פונקציות קריאה חוזרת, כמו אחזור נתונים משרת, תגובה לאינטראקציה של משתמש או המתנה לטיימרים שהוגדרו עם setTimeout או setInterval, יחסמו את ה-thread הראשי עד שהמשימה תושלם, או יקטעו באופן בלתי צפוי את הקשר הביצוע הנוכחי ברגע שבו הוספת הקשר הביצוע של פונקציית הקריאה החוזרת למקבץ. כדי לפתור את הבעיה, JavaScript מנהל משימות אסינכרוניות באמצעות 'מודל בו-זמניות' מבוסס-אירועים שמורכב מ'לולאת אירועים' ומ'תור קריאה חוזרת' (שלפעמים נקרא 'תור ההודעות').

כשמשימה אסינכרונית מופעלת ב-thread הראשי, הקשר הביצוע של פונקציית הקריאה החוזרת נמצא בתור של הקריאה החוזרת (callback) ולא מעל סטאק הקריאות. לולאת האירועים היא דפוס שנקרא לפעמים ריאקטור, שבודק באופן קבוע את הסטטוס של סטאק הקריאות ותור הקריאה החוזרת. אם יש משימות בתור להתקשרות חזרה וללולאת האירועים קובעת שערימת השיחות ריקה, המשימות מהתור לקריאה חוזרת יועברו למקבץ בזו אחר זו.