ירושה אב טיפוסית
מלבד null
ו-undefined
, לכל סוג נתונים פרימיטיבי יש אב טיפוס, מעטפת אובייקט תואמת שמספקת שיטות לעבודה עם ערכים. כשמפעילים חיפוש של method או של מאפיין בפרימיטיב, JavaScript עוטפת את הפרימיטיב מאחורי הקלעים ומפעילה את ה-method או מבצעת את חיפוש המאפיין באובייקט העטיפה במקום זאת.
לדוגמה, למחרוזת ליסטרית אין שיטות משלה, אבל אפשר להפעיל עליה את השיטה .toUpperCase()
באמצעות מעטפת האובייקט המתאימה String
:
"this is a string literal".toUpperCase();
> THIS IS A STRING LITERAL
התהליך הזה נקרא ירושה פרוטוטיפית – ירושה של מאפיינים ושיטות מה-constructor התואם של הערך.
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 { … }
אפשר ליצור פרימיטיבים באמצעות המשתנים האלה, במקום רק להגדיר אותם לפי הערך שלהם. לדוגמה, שימוש ב-constructor String
יוצר אובייקט מחרוזת, ולא מחרוזת לטיטרלית: אובייקט שמכיל לא רק את ערך המחרוזת שלנו, אלא גם את כל המאפיינים והשיטות שעברו בירושה מה-constructor.
const myString = new String( "I'm a string." );
myString;
> String { "I'm a string." }
typeof myString;
> "object"
myString.valueOf();
> "I'm a string."
ברוב המקרים, האובייקטים שמתקבלים מתנהגים כמו הערכים שבהם השתמשתם כדי להגדיר אותם. לדוגמה, למרות שהגדרת ערך מספרי באמצעות ה-constructor 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 הראשי.
משימות אחרות בדפדפן משתפות את הליבה, כמו ניתוח HTML, עיבוד ועיבוד מחדש של חלקים מהדף, הפעלת אנימציות CSS וטיפול באינטראקציות של משתמשים, החל מפעולות פשוטות (כמו הדגשת טקסט) ועד לפעולות מורכבות (כמו אינטראקציה עם רכיבי טפסים). ספקי הדפדפנים מצאו דרכים לבצע אופטימיזציה של המשימות שמבוצעות על ידי הליבה הראשית, אבל סקריפטים מורכבים יותר עדיין יכולים להשתמש בחלק גדול מדי מהמשאבים של הליבה הראשית ולהשפיע על הביצועים הכוללים של הדף.
אפשר להריץ משימות מסוימות בשרשור ברקע שנקרא Web Workers, עם מגבלות מסוימות:
- חוטי עבודה יכולים לפעול רק בקבצי JavaScript עצמאיים.
- אין להם גישה לחלון הדפדפן ולממשק המשתמש, או שהגישה שלהם מוגבלת מאוד.
- הם מוגבלים ביכולת לתקשר עם השרשור הראשי.
המגבלות האלה הופכות אותם למתאימים למשימות ממוקדות שצורכות הרבה משאבים, שעשויות לתפוס את ה-thread הראשי אחרת.
סטאק הקריאות
מבנה הנתונים שמשמש לניהול 'הקשרי ביצוע' – הקוד שמתבצע באופן פעיל – הוא רשימה שנקראת מקבץ הקריאות (לרוב פשוט 'מקבץ'). כשסקריפט מופעל בפעם הראשונה, מתרגם ה-JavaScript יוצר 'הקשר ביצוע גלובלי' ומעביר אותו ל-call stack, כאשר ההצהרות בהקשר הגלובלי הזה מבוצעות אחת אחרי השנייה, מלמעלה למטה. כשהמתורגם נתקל בקריאה לפונקציה במהלך ביצוע ההקשר הגלובלי, הוא דוחף "הקשר של ביצוע פונקציה" לקריאה הזו לחלק העליון של הסטאק, משהה את הקשר של הביצוע הגלובלי ומבצע את הקשר של ביצוע הפונקציה.
בכל פעם שמפעילים פונקציה, הקשר הביצוע של הפונקציה לאותה קריאה נדחף לחלק העליון של הסטאק, ממש מעל הקשר הביצוע הנוכחי. סטאק הקריאות פועל לפי העיקרון 'האחרון נכנס, הראשון יוצא', כלומר קריאת הפונקציה האחרונה, שנמצאת בחלק העליון של הסטאק, מבוצעת וממשיכה עד שהיא מסתיימת. כשהפונקציה הזו מסתיימת, המפרש מסיר אותה ממחולל הקריאות, וההקשר של הביצוע שמכיל את קריאת הפונקציה הזו הופך שוב לפריט העליון בערימה וממשיך את הביצוע.
הקשרי הביצוע האלה מתעדים את כל הערכים הנחוצים לביצוע שלהם. הם גם קובעים את המשתנים והפונקציות שזמינים בהיקף הפונקציה על סמך ההקשר של ההורה שלה, ומגדירים את הערך של מילת המפתח this
בהקשר של הפונקציה.
לולאת האירועים ואוסף הקריאות החוזרות (callbacks)
ההרצה הסדרתית הזו מובילה לכך שמשימות אסינכררוניות שכוללות פונקציות קריאה חוזרת (callback), כמו אחזור נתונים משרת, תגובה לאינטראקציה של משתמש או המתנה למכשירי זמן שהוגדרו באמצעות setTimeout
או setInterval
, יוצרות חסימה של ה-thread הראשי עד שהמשימה תושלם, או משבשות באופן בלתי צפוי את הקשר ההפעלה הנוכחי ברגע שקשר ההפעלה של פונקציית הקריאה החוזרת מתווסף ל-stack. כדי לטפל בבעיה הזו, JavaScript מנהלת משימות אסינכרוניות באמצעות 'מודל בו-זמניות' מבוסס-אירועים, שמורכב מ'מחזור אירועים' ומ'תור של פונקציות קריאה חוזרת' (לפעמים נקרא 'תור הודעות').
כשמשימה אסינכררונית מתבצעת בשרשור הראשי, הקשר הביצוע של פונקציית ה-callback מועבר לתור ה-callback, ולא לראש סטאק הקריאות. לולאת האירועים היא דפוס שנקרא לפעמים תגובה, שמבצע סקרים רציפים של סטטוס סטאק הקריאות ואת התור של פונקציות החזרה (callbacks). אם יש משימות בתור הקריאה החוזרת, ו-event loop קובע ש-call stack ריק, המשימות בתור הקריאה החוזרת מועברות ל-stack אחת אחרי השנייה כדי שיתבצעו.