Dev-Time

this keyword

הקדמה

בכתבה זו אני אתייחס לשימוש של this רק בjavascript בדגש על הclient-side.
this זה משהו שעלול לבלבל מפתחים מנוסים ועל אחת כמה וכמה מפתחים מתחילים.
לי באופן אישי לקח הרבה זמן להבין בדיוק כיצד זה עובד אך זה משהו בסיסי שנדרש לדעת מכל מפתח.
בשביל להבין טוב יותר את הדברים אנסה לתת הסבר קצר, שנוכל לקבל הבנה בסיסית כיצד הדברים עובדים בדפדפן שלנו.
הדפדפן באופן כללי מריץ 3 שפות HTML, CSS וJavaScript. בשביל שנוכל להבין לעומק כיצד this מתנהג חשוב שנבין שJavaScript היא למעשה אוסף של פונקציות ואובייקטים. ישנם אובייקטים ופונקציות שמגיעות עם השפה עצמה כגון Number או Object. ישנם אובייקטים שמגיעים הישר מהדפדפן שלנו והמרכזי שבהם הוא ה-window object שנקרא גם global object שלמעשה אוגר בתוכו את כל הפונקציות האובייקטים שמוגדרים בדף.

להרחבה בנושא – ממליץ לקרוא את הכתבה JavaScript Core של לידור.


אחרי הקדמה, נחזור לסיבה שאתם כאן – this

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

אקח לדוגמה את המשפט -> ‘יוסי נכנס למיטה ומיד לאחר מכן הוא נרדם’
היה ניתן לכתוב את המשפט הזה גם בצורה הבאה -> ‘יוסי נכנס למיטה ומיד לאחר מכאן יוסי נרדם’, כמו שאתם מבינים החזרה על השם יוסי בפעם השנייה אינה חיונית כיוון שיש לנו את הקישור (הוא) שמפנה אותנו ליוסי בתחילת המשפט.
זה קישור או רמז שיוצרת לנו המילה הוא – זה בדיוק השימוש שלנו לthis.


פחות עברית ויותר קוד

נכנס לקוד שתראו כיצד זה נראה ב- javascript.
קודם כל, המילה this הינה מילה שמורה בשפה – זאת אומרת שלא ניתן לכתוב משתנים בשם this או לדרוס אותה בדרך כלשהי.
ברוב המקרים הthis שלנו יקבע על ידי איך הפעלנו את הפונקציה. ברירת מחדל הינו הglobal context שהוא בדפדפן window object.
this מתייחסת לcontext שהיא נמצאת בו.
כל דבר שאני ארשום בתוך הדפדפן למעשה ייכנס ויהיה תחת האובייקט הגלובלי שנקרא window, כשאני כותב את הפקודה console, חשוב להבין שאני למעשה כותב window.console.
זאת אומרת שאם אני אכתוב בדפדפן את הדבר הבא:

אני אקבל כפלט את האובייקט window שהוא הcontext במקרה הזה. כיוון שהאובייקט console יושב בתוך האובייקט window.
אם ננסה לדמות את זה, זה יראה כך פחות או יותר:

ולכן הפלט שקיבלתי היה window כיוון שהcontext הוא האובייקט window.


5 התנהגויות של הthis

  1. Function invocation
  2. call, apply, bind
  3. Event handler
  4. Constructor function
  5. Arrow function

התנהגות ראשונה: Function/Method Invocation

חשוב שנבין כי מי שפעיל את הפונקציה הוא הופך להיות ה-context (תמיד מצד שמאל לנקודה), זאת אומרת שבמידה והפונקציה נמצאת בתוך אובייקט (פונקציה בתוך אובייקט נקראת מתודה).
בדיוק כמו בדוגמה הקודמת:

לא להתבלבל – למרות שבדוגמה הנקודה שמפעילה את המתודה log היא console (משמאל לנקודה), עכשיו אני מצפה שתעלה לכם השאלה? WTF? למה אנחנו מקבלים כפלט את הwindow ולא את האובייקט console.
תשובה: הthis לא הופעל בתוך המתודה עצמה אלא אנחנו מעבירים את ה-this כפרמטר לפונקציה.
במידה והיינו יכולים לרשום בתוך המתודה log אז כנראה זה היה נראה כך, ובאמת ה-context היה console כמו בדוגמה.

דגש חשוב למחמירים: אחד הדברים ש- “use strict” מנסה שנקפיד עליו הוא אופן הפעלה מלא של מתודות/פונקציות.

אציג כאן דוגמה קלסית של “use strict” שתוכלו לראות עד כמה חשוב שנקפיד על syntax מלא של הדברים שאנו במצב הזה.

הבדל היחידי בין הדוגמאות כאן הוא אופן ההפעלה של הפונקציה foo, חשוב תמיד לזכור ש”use strict” ימנע מאתנו לעשות דרכי קיצור שעלולות להוביל לטעויות בקוד( או לundefined במקרה הזה).

שאלת ראיון עבודה:
מה הפלט של הקוד הבא?

תשובה:
זוהי שאלה קצת מכשילה שדורשת קצת יותר ניסיון והבנה, מפתחים פחות מנוסים היו אומרים – “matan” אך זו טעות.
התשובה במקרה הזה תהיה undefined.
הסבר – הסיבה לכך שתמיד חשוב לשים לב למי הפעיל את הפונקציה, כיוון שהwindow הוא זה שהריץ את הפונקציה naming ולא person.
כיוון שהפעלנו את naming באופן הזה – window.naming וכך היא נראת:

 למעשה יש רק רפרנס לפונקציה בלעדי ה-context שממנו היא באה, מה שאומר שה-context הוא הwindow (ברירת מחדל). הדפדפן מחפש בתוך אובייקט ה-window את ה-firstName וחוזר עם undefined, בגלל ש-firstName לא קיים בתוך האובייקט של ה-window.


התנהגות שניה:  call, apply and bind

call וapply אלו הם שני מתודות אשר מגיעות לנו עם כל פונקציה בגלל שהן יושבות על הprototype של Function object שjavascript נותנת לנו.
אז למה המתודות האלו נעדו? יש להן מטרה אחת ברורה, הפעלה של פונקציה ושינוי ה-context לאובייקט כלשהו.
call וapply, מבצעות את אותה הפעולה בדיוק וההבדל היחידי הוא שב apply מעבירים מערך של פרמטרים בעוד ש call מקבלת רצף סדור של פרמטרים.

דוגמה הכי בסיסית לעבודה עם call

כמו שאתם רואים, פרמטר ראשון הוא האובייקט teacher שהוא ה-context שעליו נרצה שהפונקציה תרוץ, הפרמטר השני הוא הפרמטר role של הפונקציה greet.

דוגמה של השניים:

בדוגמה יש שני אובייקטים של אנשים שונים ומשתנה שנמצא על ה-window. ה-firstName הוא יחודי לכל אדם (סוג של ID) במקום לכתוב בכל אחד מהאובייקטים מתודה של declaring שסתם תיצור לנו קוד כפול (DRY).
נכתוב פונקציה אחת חיצונית ונקרא לה כל פעם מחדש בעזרת call או apply ושכל פעם תשנה את ה-context לפונקציה.
null תמיד יתייחס ל-context ברירת המחדל שזה ה-window בדפדפן.

תשאלו אותי מה עדיף? נורא תלוי בפרמטרים שהפונקציה מקבלת, אבל בגדול הייתי בוחר דווקא בapply כיוון שמבחינת performance היא מהירה יותר מcall.
מבחן שיצרתי בjsPerf שתוכלו לראות.

bind

בשנת 2009 ארגון תקינה עולמי שמגדיר ומאפיין תקנים בעיקר בתחום
הטכנולוגי ECMA הוציאה את גרסת  ES5) ECMAScript 5) שבין כל השדרוגים שהביאה איתה הגרסה, נוספה bind כמתודה על Function.prototype.
אז מה זה bind ומה היא עושה?
bind היא פונקציה וליתר דיוק מתודה, אשר מייצרת לנו העתק מדויק של אותה פונקציה שעליה אנחנו מפעילים את bind אך משנה ומקבעת באופן חד פעמי את הcontext של הפונקציה החדשה. בשונה מcall וapply שפועלות בזמן ריצה שאנו יכולים כל פעם לבצע הרצה עם context שונה. bind מקבעת באופן מוחלט וחד פעמי את ה-context כך שלא משנה כיצד תריצו את הפונקציה החדשה לא תוכלו לשנות את ה-context.

בדוגמה הבא תוכלו לראות כיצד הthis מקובע ולא ניתן לשינוי בפונקציה שbind יוצרת לנו.

אז חוץ מלהעתיק פונקציות עם שינוי context מתי bind באמת יכולה לעזור לנו?
דוגמה אמיתי מחיי היום יום.

בתוך האובייקט me נמצאת מתודה שמטרתה להדפיס לconsole את המדינה שלי אחרי שניה. אך מה שקורה בפועל שאני מקבל כפלט undefined.
הסיבה לכך היא ש-setTimeout דורשת callback אבל תשאלו את עצמכם מי בעצם מפעיל את הcallback הזה?
למעשה ()me.Country  רק מפעילה מתודה שבתוכה יושב ה-setTimeout. אז אנחנו עוברים ישר לברירת המחדל שלנו שבה ה-this הוא ה-window.
אז איך פתורים את זה? אציג 2 פתרונות קלאסיים שניתן לעשות.

פתרון ראשון:

 הbind פה בא לעזרתנו ובעצם משנה לנו את ה-context של הthis לאובייקט me.

פתרון שני:
בפתרון הראשון עשיתי שימוש בbind, במקרה הזה היה ניתן גם לפתור את בעיית ה-context בצורה של שמירה של הthis כמשתנה (קונבנציה היא לקרוא למשתנה that או self).
דוגמה שתיתן את אותה תוצאה בדיוק ללא שימוש באף אחת מהמתודות שהצגתי:

 


התנהגות שלישית: DOM event handler

 כשאנו מצמידים אירוע (event) לאלמנט בDOM, הthis הוא האלמנט שעליו מתרחש האירוע.

בדוגמה כאן הצמדתי אירוע לכפתור בDOM, בגלל שהאירוע מופעל על ידי pageButton (משמאל לנקודה) הוא זה שקובע את הthis.

שאלה:
מהו הthis בנתונים הבאים?

במידה וזה הDOM:

וזה הקוד JS:

חשוב לדעת: הכפתור שנמצא בתוך <div> גם מקבל גם את אותו אירוע (קליק) בעקבות event propagation.

תשובה:
צריכים לשים לב שהthis שלנו תמיד יקבע לפי האלמנט שאליו הצמדנו את האירוע (evet.currentTarget) שזה ה-<div> בדגומה כאן, ולא (event.target) שזה הכפתור שעליו המשתמש לחץ.

in–line event handler
כמו באנגולר, ריקאט וכו.. אנחנו כותבים את האירועים שלנו כinline event ולכן אציג כמה דוגמאות שתוכלו לראות את ההבדלים.

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

בדוגמה כאן שה-callback הוא זה שזה מבצע את הפעולה, גם היא כתבו כ-IIFE או כפונקציה חיצונית, הthis לא יהיה האלמנט עצמו אלא ברירת המחדל של הדפדפן (window).

אחד הפתרונות לזה הוא bind, וזה יראה כך:

מי מכם שיצא לו לעבוד עם React, ב-React נעזרים המון ב-bind כשרוצים להפנות את ה-context של אירוע מסוים ל-class.


התנהגות רביעית: constructor function

בנוסף לכוחה של המילה new בjavascript  שעליה אפרסם פוסט נוסף שיסביר את הנקודה.
אחד הדברים שהיא יודעת לעשות הוא להפנות את ה-context של הconstructor ולהעביר אותו הלאה אל האובייקט החדש שיצרנו.

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


התנהגות חמישית ואחרונה: Arrow function

שנת 2015 או ליתר דיוק בחודש יוני שנת 2015, חברת EMCA הכריזה על ECMAScript 2015 שהיא המהדורה השישית שלהם ולכן היא נקראת גם ES6. הגרסה החדשה הביאה איתה בין כל השדרוגים והתוספות את ה-arrow function שהיא גרסת כתיבה מקוצרת מבחינת syntax לפונקציה אנונימית עם שינוי התנהגות של הthis.
מאוחרי הקלעים arrow function בסוף הרי ממורת לפונקציה של ES5. המימוש של זה נעשה ב2 דרכים, עם self = this שהראתי לכם או על ידי שימוש ב-(bind(this לאותה פונקציה.
ניתן לראות את הדוגמה של ES6 בלינק.
בנוסף אני רוצה להמליץ למי שלא מכיר היטב את ES6, לקרוא את המדריכים של רן בר-זיק מאינטרנט ישראל.

עכשיו אתן כמה דוגמאות שנראה את ההבדלים בין השניהם.

חשוב שתראו כי במתודה arrowFunc הפלט שלי הוא ה-window בשונה מתודה רגילה שהיא לא arrow function.
הוספתי את המתודה demoFunc שבסופה יש bind כדי שתוכלו לראות שזו בדיוק אותה תוצאה כמו הarrow function.
חשוב להבין שה-arrow function מתנהגת שונה מפונקציה רגילה שהכרנו עד היום, ולכן צריך להיות זהירים איתה במיוחד בהקשר של הthis.
נסו לחשוב על זה שבכל מקום הייתם רוצים להכניס פונקציה שבסופה הייתה נאלצים לשים (bind(this אז אפשר פשוט לשים arrow function.
דוגמה קלאסית:

על המתודה log אני מפעיל את setTimeout שהcallback שלה מקבל bind בשביל שה-context ישאר באובייקט obj.
אם אנחנו יודעים שאנחנו עושים שימוש בbind אז למה לא לקצר ולכתוב את זה כך:


סיכום

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

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

כמו שאתם רואים אני מבקש להדפיס לconsole בדיוק את אותו דבר בשני הפונקציות.
אז מה קורה פה? למה פונקציה אחת מדפיסה true והשנייה false?

תשובה:
אומנם הפונקציות באמת נראות זהות, אך יש דבר קטן אחד שעושה את כל ההבדל (כמו בהרבה דברים בחיים). בהרצה במצב רגיל של הפונקציה אנו מקבלים כברירת מחדל את הglobal object ובמצב של 'use strict' אנו מקבלים undefined.
בדוגמה כאן מתרחש תהליך מיוחד שנקרא boxing. כשאנו מעבירים ערך פרימיטיבי בעזרת call או apply ללא ה'use strict' נוצר תהליך ה-boxing שזהו למעשה אובייקט שדואג לעטוף את הערך, לכן תמיד נקבל אובייקט לא משנה איזה ערך נעביר. במצב 'use strict' תהליך ה-boxing אינו מתרחש ובעקבות כך ערך פרימיטיבי נשאר כפי שהוא.
בדוגמה ניתן לראות שבמצב 'use strict' אנו מקבלים את סוג ערך (במקרה הזה מספר) ולא אובייקט כמו בדוגמה השניה.
דוגמה לכך:

בנוסף נוצרו דיונים שלמים לגבי השיטה שבה בbabel מקמפל ES6 לES5, כיוון babel מקמפל את הקוד שלנו ב'use strict' אז במצבים מסוים ייתכן שיהיה באג עם this.


מקורות קריאה:

davidshariff.com/blog/what-is-the-execution-context-in-javascript/
www.digital-web.com/articles/scope_in_javascript/
developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/this
rainsoft.io/gentle-explanation-of-this-in-javascript/

מתן יוסף

תמיד מתעניין ורוצה לדעת עוד..
יש לכם משהו לשאול או להעיר?
קישור ללינקדאין שלי ↓

Add comment

Follow

Get every new post delivered to your Inbox

Join other followers