JS: איך עובד this binding?
-
דבר מעניין שלמדתי אתמול:
const myLogger = { messageNumber: 0, log (message) { console.log(`${this.messageNumber++}: ${message}`); } }; function log (message, logger) { (logger.log || console.log)(message); } log('Hello, world!', myLogger); log('Another message...', myLogger);
מה יהיה בפלט של הקוד, (ובעיקר למה?)
-
אוקי הבנתי, הthis מתייחס למיקום של המצביע ולא למיקום של הפונקציה עצמה שלו אין משמעות.
זה הגיוני. הנה המחשה יותר בסגנון שלי:var test = { a: 0, log: function log () { console.log(this.a++); } }; test.log() var refFun = test.log; refFun();
כיון שאופרטור || מחזיר מצביע חדש אז התופעת לוואי הזו קורית גם שמה, יפה.
-
@dovid תמיד הסתכלתי על הלוגיקה של איגוד ה-
this
גם כן כמוך שזה ענין של "על ידי איזה מצביע הגעת לפונקציה?". אבל מתוך הנ"ל למדתי שהכיוון שגוי. זו הנקודה שאותה רציתי ללבן.במנוע JS יש 2 רכיבים: ה"מפרסר" וה"מריץ".
המפרסר מפרסר את הקוד ומתרגם אותה לפעולות קטנטנות פרימטיביות, שאותם הרכיב המריץ מריץ.
המשימה של איגוד ה-this
לאובייקט הנכון היא משימתה של המפרסר ולא של המריץ. מבחינת המריץ, הפונקציה נראית אותו דבר לגמרי בין אם נגשת לזה כמאפיין של אובייקט אחר או לא. זה תמיד אותו "מצביע".
כדי לאגד את ה-this
, המפרסר בודק את ההקשר הלשוני של אפרטור הקריאה -()
.
הכלל הוא שאם אופרטור הקריאה()
מתייחס לביטוי של "גישה למאפיין של אובייקט" (MemberExpression
), אז המפרסר מאגד את האובייקט ההוא ל-this
של הפונקציה בעת הקריאה (כלומר, במקרה שלobj.property(...args)
המפרסר מתרגם את הקוד לפעולה שמתנהגת כ-property.call(obj, ...args)
)
בדוגמה שלי, הביטוי שאליו מתייחסת אופרטור הקריאה הוא ביטוי לוגי (LogicalExpression
), לא ביטוי של גישה למאפיין של אובייקט.עיין כאן בניתוח ה-AST של
(logger.log || console.log)(message)
ותבין. -
@yossiz ראשית אני מעריך את העומק. הבנתי מה שאתה אומר שהמריץ כבר לא מתעסק עם this כי המפרסר מחליט בשבילו, וזה בהחלט יפה לדעת.
שנית, בראש שלי אינני מבין למה צריך להגיע לזה, זה ברור שתוצאת ה || מחזירה מצביע חדש. כי ה || מחזירות ערך שהוא הנכון מבין השניים. ודאי שזה לא מחזיר את המצביע המקורי (שאז היית עושה ++ למשל אחרי הסוגריים). -
@dovid אמר בJS: איך עובד this binding?:
שנית, בראש שלי אינני מבין למה צריך להגיע לזה, זה ברור שתוצאת ה || מחזירה מצביע חדש. כי ה || מחזירות ערך שהוא הנכון מבין השניים. ודאי שזה לא מחזיר את המצביע המקורי (שאז אם היית עושה ++ למשל אחרי הסוגריים).
לא הבנתי כוונתך. נגיד לשם דוגמה שהביטוי
object.property
מחזיר מצביע במיקום0x000a
שערכו הוא0x000b
שזה הכתובת בזכרון של הפונקציהproperty
.
אז בשלמא אם אתה כותב:let a = object.property
מובן שזה ייתן לך מצביע חדש במיקום0x000c
שערכו גם כן0x000b
.
אבל למה נראה לך שהביטוי(null || object.property)
לא יחזיר בדיוק אותו מצביע שobject.property
יחזיר? -
@yossiz שוב פעם אתה תמיד מגיע מהבנת הדברים לעומקם, תסביר לי שאני די רדוד ותמים בעניין למה הקוד הזה לא חוקי:
var a = 0, b = 1; var c = (a || b)++;
אם ה || היה מחזיר מצביע זה היה אמור להיות קוד חוקי.
ההבנה הפשוטה היא מייד שה || מחזיר ערך ולא מצביע. ולערך לא שייך לעשות ++. -
@dovid אחרי העיון, (ואם תרשה לי לתרגם את הדברים שלך מהזווית שלי), אני מסכים לגמרי למה שאתה טוען, ועוד יותר, נראה לי שזה פחות או יותר אותו דבר כמו שאני אמרתי
ובהקדם,
צריך להבחין בין ערך, מצביע, ו-identifier.- ערך הוא הערך הגולמי. לדוגמה המספר 5, או התו 'a'. זה אובייקט שקיים בזכרון בזמן ריצה.
- מצביע (pointer) ג"כ הוא אובייקט פיזי שקיים בזמן ריצה. כלומר, מדובר על ערך שקיים בזכרון שערכו הוא כתובת זכרון של אובייקט אחר.
- בשונה מזה, identifier הוא מושג שקיים רק ברמת השפה/מפרסר. מדובר על "ידית" לכתובת זכרון, כלומר, דרך להזכיר את הכתובת של ערך בלי לייצר "מצביע" רגיל.
(למי שמכיר, ההבדל בין pointer ל-identifier הוא ההבדל ב-++C בין pointer ל-reference variable, נוסח אחר לאותו מושג זה lvalue ו-rvalue)
ברמת השפה/מפרסר הפעולה של
++
חלה רק על identifier ולא על ערך.
אמנם ברמת ההרצה, תמיד הפעולה יתבצע על ערך גולמי ולא דרך identifier. המושג identifier לא קיים עבור סביבת הריצה. הכל מתבצע ישירות על ערכים.אם כן, אפשר לתרגם מה ש@dovid אמר בצורה כזאת:
התוצאה של ביטוי לוגי עם אופרטור||
היא "ערך" ולא identifier. והמפרסר חייב identifier עבור פעולת++
.אותו דבר, ב-this binding.
המפרסר עושה את איגוד ה-this בכפוף לצורה שבה הזכרת את הפונקציה. אם הזכרת אותו על ידי identifier שהוא חלק מביטוי שהוא בתבנית MemberExpression אז יתבצע איגוד לאובייקט הבסיס של ה-MemberExpression. אחרת זה לא יתבצע.
הביטויlogger.log || console.log
הוא לא MemberExpression
ויותר מזה הוא לא identifier בכלל אלא ערך גולמי של פונקציה. וזה מה שדוד מוסיף.הכל ברור?
עריכה: ושכחתי להגיד תודה ל @dovid על העזרה שלו בליבון הנושא. תודה רבה.