להבין את ההקשר של THIS ב-JS
איך עובדת הגדרת משתנים ב-JS? למה פונקציות יורשות this?

אני מבולבל לגבי ‘this’. אוקיי? אמרתי את זה.
- איך הוא נקבע?
- למה הוא שונה עבור פונקציית חץ?
- ולמה פונקציות רגילות לא מקבלות אחד? אלה השאלות שאני רוצה לענות עליהן במאמר הזה.
THIS לא מתייחס להקשר (scope לקסיקלי)
אתם צריכים לדעת שכשאתם כותבים פונקציה כמו שאני עושה כאן:
var name = "alex"
function greet() { console.log("hi "+this.name);}
greet() // מה קורה כאן?מה זה מדפיס?<script> רגיל בדפדפן, לא מודול. אותו דבר לשאר.
הצגת הפלט
hi alexקריאה חשופה בסקריפט רגיל, אז this הוא window. var גלובלי נדבק ל-window, אז this.name הוא "alex". במודול זה נזרק: שם this הוא undefined.
אותו קוד לא ירוץ בג’אווה, C או PHP. למעשה גם ב-JS הוא לא היה מדפיס alex אם לא הייתי מגדיר את name באמצעות var. זו הערת אגב קטנה, אבל משתנים שמוגדרים עם var חיים ב-global scope ולא בהקשר הלקסיקלי.
| מוגדר | var | let ו-const |
|---|---|---|
| גלובלית | global scope | הקשר לקסיקלי גלובלי |
| בפונקציה | function scope | הקשר לקסיקלי בהיקף הפונקציה |
| בבלוק | ה-scope של הפונקציה העוטפת (או global scope) | הקשר לקסיקלי בהיקף הבלוק |
הנה הדגמה מהירה להבדל:
if (true) { var v_name = "alex" let l_name = "alex"}
console.log(v_name) // "alex"console.log(l_name) // ReferenceError: l_name is not definedאבל למה this.name מחזיר var שהוגדרה גלובלית? זה מפני שבסביבה לא strict כמו הדפדפן, ה-‘this’ שמוגדר גלובלית מתייחס לאובייקט window, ו-var שמוגדרת ב-global scope נכנסת בדיוק לשם. אבל למעשה זה פשוט כי כל קוד הדפדפן, מבחינה טכנית, רץ בתוך אובייקט ה-window, ואם חושבים על זה בצורה הזאת, זה נהיה די ברור למה המשתנים נדבקים אליו ולמה פונקציות יורשות אותו.
הנה כמה מקורות לקריאה נוספת https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Closures https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/let#temporal_dead_zone_tdz
THIS הוא סוכר תחבירי
אי פעם תהיתם איך אובייקטים קיבלו את מקומם ב-js? איך js מתייחס לאובייקטים? תסתכלו על זה: בואו נגדיר אובייקט User פשוט
class User { name = ""; constructor(name) { this.name = name; } greet() { console.log("hi "+this.name) }}כשאנחנו קוראים לפונקציה מהמופע של אותו אובייקט, מה שקורה בסוף הוא:
const alex = new User("alex")alex.greet()
// שקול ל:alex.greet.call(alex)// אוalex.greet.apply(alex, [])מהדוגמה הפשוטה הזאת כבר אפשר להבין ש-this הוא פשוט האובייקט שעליו קראו לפונקציה - מה שנמצא משמאל לנקודה. אין אובייקט, אין this. לא אכפת לו איפה הפונקציה נכתבה, רק איך קראו לה.
אז הקוד הבא לא יברך את alex
const name = "alex";
function greet() { console.log("hi "+this.name);}
greet()
// שקול לgreet.call(undefined);מה זה מדפיס?
הצגת הפלט
hi ␣this עדיין window, אבל const לא נדבק אליו. אז this.name קורא את window.name, מאפיין אמיתי של הדפדפן שהוא מחרוזת ריקה כברירת מחדל. לא undefined. דפדפנים.
וגם זה לא יעבוד
function setup(){ const name = "alex";
function greet() { console.log("hi "+this.name); }}
greet()
// היה שקול ל (אם greet הייתה קיימת כאן)greet.call(undefined);מה זה מדפיס?
הצגת הפלט
Uncaught ReferenceError: greet is not definedשאלה מכשילה. greet קיימת רק בתוך setup(), אז כאן בחוץ אין מה לקרוא. זה אף פעם לא מגיע לשלב שבו מתבלבלים עם this.
כדי להבין טוב יותר את מה שכתבתי עכשיו, עברו על הדוגמה הבאה תוך ניסיון לנחש מה הפלט של התוכנית
הנה ההגדרה שלנו, מחלקת user פשוטה עם property בשם this.name ושתי גרסאות של greet, אחת שהיא פונקציה רגילה והשנייה פונקציית חץ
class User { name = ""; constructor(name) { this.name = name; } normalGreet(){ console.log("hi "+this.name) } arrowGreet = () => { console.log("hi "+this.name) }}
let user = new User("alex");let miri = new User("miri")שאלה ראשונה
let alex = new User("alex")
// ראשוןalex.normalGreet();alex.arrowGreet();מקווה שלא אקלקל את זה שגם וגם עובדות מצוין ומדפיסות את הודעת הברכה שלנו עבור alex - hi alex
שאלה שנייה
let alex = new User("alex");
let normal = alex.normalGreet;let arrow = alex.arrowGreet;
normal();arrow();מה זה מדפיס?
הצגת הפלט
Uncaught TypeError: Cannot read properties of undefined (reading 'name')קוד בתוך class תמיד רץ ב-strict mode, אז normal() מנותקת מקבלת this === undefined במקום window, וקריאת .name ממנו זורקת שגיאה. arrow() לא מגיעה לתורה.
פה מתחילה הבעיה, אתם מצליחים לזהות אותה?
מה קורה כשאנחנו משייכים את הפונקציה למשתנה לפני שקוראים לה?
מסתבר שהפונקציה הרגילה מתפוצצת עם TypeError: Cannot read properties of undefined (reading 'name'), עכשיו תחשבו למה. אמרנו שלפונקציה רגילה יש this דינמי, שמחושב בזמן ריצה. תחשבו על זה, אנחנו קוראים לפונקציית greet כאילו על כלום, אין לה הקשר this, אז איך היא יכולה להתייחס ל-this.name? (גוף של מחלקה תמיד רץ ב-strict mode, אז this כאן הוא פשוט undefined במקום ליפול בשקט ל-window.) היא נזרקת עוד לפני ש-arrow() מגיעה לתורה.
במקרה של פונקציית החץ, שה-this שלה מוגדר בזמן ההגדרה, ההקשר נשאר שלם גם אחרי השיוך למשתנה אחר. וזה בדיוק ההבדל בין פונקציות רגילות לפונקציות חץ.
אתם אולי שואלים את עצמכם, האם אני באמת צריך לדעת את זה? באיזה הקשר אני אראה את זה? התשובה תלויה בשימוש שלכם ב-OOP ב-JS או בכל פריימוורק אחר. לדוגמה
<button onclick={alex.normalGreet}>Normal Greet</button><button onclick={alex.arrowGreet}>Arrow Greet</button>אפשר להסתכל על הקוד למעלה כשיוך פונקציה ל-property של אובייקט!
// זה מעתיק בדיוק את הדוגמה השנייה שלנוbuttonElement.onclick = alex.normalGreetאז כדי שה-tsx שלנו יעבוד, תצטרכו או להשתמש בפונקציית חץ באובייקט User, או להעביר אותה ככה
<button onclick={()=>alex.normalGreet()}>Normal Greet</button>ראו גם: Livewire ל-WP, שבו onclick מוטמע מעביר ל-handler את this (הכפתור עצמו).
החלק השלישי הוא סתם בשביל הכיף, למקרה שתרצו להבין טוב יותר איך bind, call ו-apply עובדים:
let miri = new User("miri")
let normal = alex.normalGreet;let arrow = alex.arrowGreet;
normal = normal.bind(miri);arrow = arrow.bind(miri);
normal();arrow();מה זה מדפיס?
הצגת הפלט
hi mirihi alexbind קובע את this לפונקציה רגילה, אז normal מברכת עכשיו את miri. פונקציית החץ כבר לקחה את ה-this שלה מהמקום שבו נכתבה, המופע alex, ו-bind לא יכול לקחת אותו בחזרה.
הפונקציה הרגילה מדפיסה hi miri. bind מחזיר פונקציה חדשה שה-this שלה ממוסמר למה שהעברתם לו, ופונקציה רגילה מקבלת את זה בשמחה.
פונקציית החץ עדיין מדפיסה hi alex. לפונקציות חץ אין this משלהן בכלל - הן לוקחות אותו מהמקום שבו הן הוגדרו, ובמקרה של שדה במחלקה זה המופע שנבנה. bind, call ו-apply יכולים לנסות לתת לה this אחר, והיא פשוט מתעלמת מהם. היא נולדה שייכת ל-alex, והיא תמות שייכת ל-alex.
אז, בחזרה לשאלות שלי מההתחלה. this נקבע בזמן הקריאה, לפי מה שנמצא משמאל לנקודה (או לפי call, apply ו-bind). פונקציות חץ שונות כי אף פעם אין להן this משלהן - הן שואלות את זה של המקום שבו הן נכתבו. וכשקוראים לפונקציה על כלום, אין מה לקבוע, אז מקבלים undefined (או window, אם אתם ב-sloppy mode וחסרי מזל).
עדיין קצת מבולבלים? גם אני. רק פחות מאשר כשהתחלתי.
5 קטעי קוד, 5 ניחושים. הציון יופיע כאן.
תגובות
התגובות נשמרות ב-GitHub Discussions (דרך giscus), אז כדי להצטרף לשיחה צריך חשבון GitHub.