נספח

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

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

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

"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

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

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

כאשר ערך פרימיטיבי מוקצה ממשתנה אחד למשתנה אחר, קוד ה-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, עם מגבלות מסוימות:

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

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

מקבץ השיחות

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

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

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

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

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

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