איך עובד this ב-JavaScript ומתי הוא עלול לשבור את הקוד שלך

סער טויטו

סער טויטו

Software & Web Development


TL;DR
  • this מצביע על האובייקט שקורא לפונקציה — ולא על הפונקציה עצמה. ההקשר משתנה בהתאם לאופן הקריאה.
  • האובייקט הגלובלי בדפדפן הוא window, ב-Node.js הוא global. this מחוץ לכל פונקציה יצביע אליו.
  • בעיית ההקשר בפונקציה פנימית (כמו בתוך forEach) this עלול לאבד את הקשר לאובייקט המקורי.
  • פתרון: פונקציות חץ אינן יוצרות הקשר חדש של this, ולכן שומרות על ההקשר של הפונקציה החיצונית.

this keyword במבט כללי

המילה this ב-JavaScript היא מילה מיוחדת שמצביעה על האובייקט שקורא לפונקציה. ערכה נקבע בזמן הריצה — לא בזמן הגדרת הפונקציה — ולכן הוא יכול להשתנות בהתאם להקשר. מחוץ לכל פונקציה, this מצביע על האובייקט הגלובלי (window בדפדפן, global ב-Node.js). בתוך מתודה של אובייקט, this מצביע על אותו אובייקט.

console.log(this);

מהו האובייקט הגלובלי?

כדי להבין לעומק את this, חשוב להבין מהו האובייקט הגלובלי.

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

האובייקט הגלובלי בדפדפן

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

לדוגמה, window מכיל פונקציות מוכרות כמו alert להצגת הודעות קופצות, confirm להצגת חלון אישור עם כפתורי "אישור" ו-"ביטול", ו-addEventListener שמאזינה לפעולות המשתמש כמו לחיצות עכבר או טעינת עמוד.

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

בנוסף, window מכיל אובייקטים גלובליים חשובים לניהול הדף, כמו document, שמאפשר גישה לעץ ה-DOM, ו-location, שמייצג את כתובת ה-URL של העמוד.

console.log(window); // window object in browser
window.addEventListener('click', () => console.log(window));
console.log(window.document.querySelector('h2'));

האובייקט הגלובלי ב-Node.js

ב-Node.js, האובייקט הגלובלי נקרא global, והוא מקביל ל-window בדפדפן, אך מותאם להרצה בצד השרת. בשונה מהדפדפן, האובייקט global אינו כולל פונקציות ואירועים הקשורים לממשק המשתמש, אלא רק פונקציות שקשורות להרצת תוכניות ולתהליכי מערכת בצד השרת.

לדוגמה, global כולל פונקציות כמו setTimeout ו-setInterval לניהול זמן, אך אינו כולל פונקציות כמו alert או אירועים כמו onClick, מכיוון שאלה שייכים לסביבה של הדפדפן בלבד. במקום זאת, global מכיל אובייקטים כמו process, שמספק מידע על התהליך שמריץ את הקוד, ופונקציות כמו require לייבוא מודולים חיצוניים.

ב-Node.js ניתן להדפיס לטרמינל הודעות באמצעות console.log, ממש כפי שעושים בקונסול של הדפדפן, אך לא ניתן להשתמש באובייקט document או בגישה ל-DOM, מכיוון שאין ממשק משתמש גרפי בצד השרת.

console.log(global); // global object in Node.js
console.log(global.process); // process object in Node.js (env vars, memory, etc.)
console.log(global.require); // require function in Node.js

דוגמאות לשימוש ב-this

אוקיי, אחרי שהבנו מהו ה-Global Object, הגיע הזמן לחזור למילה this ולראות איך היא "מצביעה" בהתאם להקשר שבו היא מופעלת. אז הנה דוגמה לשימוש במתודה עם this.

const object = {
  name: "Saar",
  age: 23,
  printThis() {
    console.log(this); // { name: 'Saar', age: 23, printThis: [Function: printThis] }
  }
};

object.printThis();

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

במקרה הזה, הקריאה object.printThis() גורמת לכך ש-this יפנה לאובייקט עצמו ויציג את כל המאפיינים שלו – name, age, והפונקציה printThis עצמה כחלק מהאובייקט.

הבעיה של this keyword

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

const example2 = {
  eventName: "Birthday Party",
  guestList: ['Novin', 'Saar', 'Ofri'],
  printGuestList() {
    console.log(`Guest list for ${this.eventName}`); // מציג "Birthday Party"

    this.guestList.forEach(function (guest) {
      console.log(`${guest} is attending ${this.eventName}`); // undefined
    });
  }
};

example2.printGuestList();

כאשר הפונקציה printGuestList נקראת מתוך האובייקט example2, המשתנה this מתייחס לאובייקט example2, ולכן this.eventName מציג את שם האירוע "Birthday Party". עם זאת, בתוך הקריאה ל-forEach, הפונקציה הפנימית נכתבת כ"פונקציה רגילה", מה שמוביל להגדרת הקשר חדש עבור this.

בעקבות זאת, this.eventName מחפש את המשתנה eventName באובייקט הגלובלי (window בדפדפן או global ב-Node.js). כיוון שבאובייקט הגלובלי לא קיים משתנה בשם eventName, התוצאה המתקבלת היא undefined.

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

הפתרונות לבעיית this keyword

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

הפתרון הראשון: שימוש במשתנה עזר (that)

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

const example2 = {
  eventName: "Birthday Party",
  guestList: ['Novin', 'Saar', 'Ofri'],
  printGuestList() {
    const that = this;
    console.log(`Guest list for ${that.eventName}`); // מציג "Birthday Party"

    this.guestList.forEach(function (guest) {
      console.log(`${guest} is attending ${that.eventName}`); // משתמש ב-'that' במקום ב-'this'
    });
  }
};

example2.printGuestList();

המשתנה that שומר את הערך של this, אשר מתייחס לאובייקט example2. מאחר שהפונקציה הפנימית forEach אינה מגדירה הקשר חדש עבור that, היא יכולה לגשת לשם האירוע באמצעות that.eventName. כתוצאה מכך, ההדפסה מציגה את השם "Birthday Party" במקום undefined.

הפלט שיוצג בקונסול בעת קריאה לפונקציה:

Guest list for Birthday Party
Novin is attending Birthday Party
Saar is attending Birthday Party
Ofri is attending Birthday Party

הפתרון השני: שימוש בפונקציות חץ (Arrow Functions)

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

const example2 = {
  eventName: "Birthday Party",
  guestList: ['Novin', 'Saar', 'Ofri'],
  printGuestList() {
    console.log(`Guest list for ${this.eventName}`); // מציג "Birthday Party"

    this.guestList.forEach((guest) => {
      console.log(`${guest} is attending ${this.eventName}`);
    });
  }
};

example2.printGuestList();

הפונקציה הפנימית forEach משתמשת בפונקציית חץ, שאינה מגדירה הקשר חדש של this. פונקציית החץ משתמשת בהקשר של הפונקציה החיצונית, ולכן this.eventName מצביע לשם האירוע "Birthday Party". כתוצאה מכך, ההדפסה מציגה את השם "Birthday Party" במקום undefined.

הפלט שיוצג בקונסול בעת קריאה לפונקציה:

Guest list for Birthday Party
Novin is attending Birthday Party
Saar is attending Birthday Party
Ofri is attending Birthday Party

סיכום

מילת המפתח this ב-JavaScript היא אחד הנושאים המבלבלים בשפה, אך האמת היא שלא בכל סביבה חייבים להשתמש בה. לדוגמה, בעבר ב-React עשו בה שימוש נרחב, אבל מאז המעבר ל-hooks השימוש ב-this הפך לכמעט מיותר.

לעומת זאת, ב-Angular, שבו מבנה הקוד מבוסס על מחלקות, this עדיין נמצא בשימוש יומיומי כחלק מהעבודה עם מחלקות ושירותים. רוצים ללמוד עוד על מחלקות ובכלל על תכנות מונחה עצמים (OOP)? קראו את המאמר המלא שלנו.

שאלות נפוצות על this ב-JavaScript

מה זה this ב-JavaScript?

this היא מילת מפתח מיוחדת שמצביעה על האובייקט שקורא לפונקציה. ערכה נקבע בזמן הריצה ולא בזמן הגדרת הפונקציה. מחוץ לכל פונקציה, this מצביע על האובייקט הגלובלי — window בדפדפן או global ב-Node.js. בתוך מתודה של אובייקט, this מצביע על אותו אובייקט.

למה this מחזיר undefined בתוך forEach?

כאשר משתמשים בפונקציה רגילה (לא פונקציית חץ) כ-callback בתוך forEach, JavaScript יוצר הקשר חדש של this עבור אותה פונקציה. כיוון שהפונקציה לא נקראת ישירות מתוך אובייקט, this מצביע על האובייקט הגלובלי ולא על האובייקט המקורי. הפתרון הפשוט הוא להחליף את הפונקציה הרגילה בפונקציית חץ.

מה ההבדל בין פונקציה רגילה לפונקציית חץ מבחינת this?

פונקציה רגילה יוצרת הקשר משלה עבור this — הערך שלה נקבע לפי מי קרא לה בזמן הריצה. פונקציית חץ לעומת זאת לא יוצרת הקשר חדש; היא לוקחת את this מהפונקציה החיצונית שבתוכה היא מוגדרת (מה שנקרא "lexical this"). לכן פונקציות חץ מתאימות מאוד לשימוש בתוך callbacks ומתודות אובייקט.

מה ההבדל בין window ל-global?

window הוא האובייקט הגלובלי בדפדפן ומכיל פונקציות כמו alert, document ו-addEventListener. global הוא האובייקט הגלובלי ב-Node.js ומכיל פונקציות כמו process ו-require. שניהם משמשים כ"מרכז" של סביבת הריצה ומחזיקים את כל המשתנים והפונקציות שהוגדרו ברמה הגלובלית, אך כל אחד מותאם לפלטפורמה שלו.

האם this בשימוש ב-React?

בעבר, כאשר React השתמשה ב-class components, this היה חיוני לגישה ל-state ו-props. מאז המעבר ל-hooks (כמו useState ו-useEffect) ב-React 16.8, השימוש ב-this ב-React כמעט נעלם לחלוטין. ב-Angular לעומת זאת, שמבוסס על מחלקות TypeScript, this עדיין בשימוש יומיומי בשירותים ובקומפוננטות.