במאמר הזה אני אצלול קצת לעומק ואנתח את this בג'אווהסקריפט ואיך הוא מתנהג ואיך אפשר לשלוט עליו.
כל מי שעבד עם תכנות מונחה עצמים, ולא משנה באיזו שפה, יודע מה משמעות ה-this. מדובר במילה שמורה בכמעט כל שפות העלית והמשמעות שלה היא הקונטקסט של המחלקה. בג'אווהסקריפט יש לפעמים ל-this משמעות מעט מבלבלת. אבל בגדול זה נראה ככה:
var person = {
firstName: 'Moshe',
lastName: 'Cohen',
giveMeName: function () {
console.log(person.firstName + " " + person.lastName); //Or person
console.log(this.firstName + " " + this.lastName); //We can use this
}
}
אני מגדיר אובייקט person שיש לו שתי תכונות, שם ושם משפחה. מתודה משתמשת בתכונות האלו. אנחנו יכולים לקחת את שם האובייקט נקודה ואז התכונה או להשתמש ב-this, זה אותו הדבר. ה-this הוא הקונטקסט שבו הבלוק של הקוד, במקרה הזה אובייקט חי בו.
כדי להמחיש את הקונטקסט, אני אציג גם פונקציה וגם אובייקט:
this.firstName = 'Sarah';
this.lastName = 'Levi';
function giveMeName() {
console.log(this.firstName + " " + this.lastName);
}
var person = {
firstName: 'Moshe',
lastName: 'Cohen',
giveMeName: function () {
console.log(this.firstName + " " + this.lastName);
}
}
person.giveMeName();//Moshe Cohen
giveMeName(); //Sarah Levi
שימו לב שבמקרה של הפונקציה, הקונטקסט הוא גלובלי (או יותר נכון של אובייקט window שהוא האובייקט הגלובלי בג'אווהסקריפט). ה-this נקבל את הערכים של 'שרה' ו'לוי'. במקרה של האובייקט, הקונטקסט הוא של האובייקט בלבד.
מפה העניינים מתחילים להסתבך, רוב האנשים משתמשים בג'אווהסקריפט עם קולבקים (או עם promises, אבל אני לא נכנס לזה). מה זה קולבק? פונקציה שאני מעביר לפונקציה אחרת כדי שהיא תריץ אותה. מתי משתמשים בזה? להמון הזדמנויות. למשל, אם יש לי דף שבו אני שולח בקשה לשרת (למשל שמירה של מידע), אני אכתוב פונקציה ששולחת מידע לשרת ושאחד הפרמטרים שלה הוא פונקציה אחרת שהפונקציה ששולחת מידע צריכה להפעיל ברגע שכל התהליך נגמר. גם אם לא חשבתם על זה עד הסוף, סביר מאוד להניח שהשתמשתם בזה. משתמשים בזה כמעט בכל דבר, בין אם מדובר בפונקצית קליק או כל דבר אחר. אתה מעביר לפונקציה פונקציה אחרת שתופעל ואני אדגים:
var person = {
firstName: 'Moshe',
lastName: 'Cohen',
giveMeName: function () {
console.log(this.firstName + " " + this.lastName);
}
}
var otherObject = {
myMethod: function(callback) {
callback();
}
}
otherObject.myMethod(person.giveMeName);
כאן אפשר לראות שהפונקציה myMethod שנמצאת בתוך otherObject מקבלת callback והיא מפעילה אותו. זה משהו מאוד טריוויאלי ב-JavaScript ואנחנו עושים אותו כל הזמן. במקרה הזה myMethod לא עושה דבר, אבל היא יכולה לעשות המון דברים: קריאה ל-API בשרת אחר, פעולות על קבצים, קריאה למסד נתונים – יו ניים איט. אבל מה שחשוב הוא שהקולבק ירוץ בסוף הפעולה.
מה הבעיה? מה לפי דעתכם יודפס בקונסולה של הדפדפן? הייתם מצפים ש'משה כהן', נכון? אז זהו! שלא! מה שנקבל זה undefined undefined. למה? אמרתי שה-this קשור לקונטקסט. בג'אווהסקריפט הקונטקסט משתנה בהתאם למי שהפעיל את המתודה. כיוון ש-otherObject הופעל מהקונטקסט הגלובלי, אז ה-this יכוון לקונטקסט הגלובלי – שם אין לנו lastName ו-firstName ואז אנחנו נקבל undefined. אם היו שם – היינו מקבלים את הערכים שלהם! שימו לב לקוד הזה:
var person = {
firstName: 'Moshe',
lastName: 'Cohen',
giveMeName: function () {
console.log(this.firstName + " " + this.lastName);
}
}
var firstName = 'Zeev';
var lastName = 'Revah';
var otherObject = {
myMethod: function(callback) {
callback();
}
}
otherObject.myMethod(person.giveMeName); //Zeev Revah
מה שיודפס זה זאב רווח ולא משהו אחר. זה נראה פשוט כי מדובר פה ב-hello world ובקוד מאוד פשוט. אבל ההתבלבלות בקונטקסט יכולה להטריף מתכנתי ג'אווהסקריפט לא מנוסים (גם מנוסים). נשאלת השאלה איך שולטים בקונטקסט? איך אני יכול למשל לדאוג שאם אני מעביר מתודה מסוימת, היא תקבל את הקונטקסט של האובייקט המקורי ולא של האובייקט שהפעיל אותה?
הכירו את bind שמאפשרת לי לקבוע את הקונטקסט של כל פונקציה/מתודה שאני מפעיל מחוץ לפונקציה. איך אני עושה את זה? בקלות:
var person = {
firstName: 'Moshe',
lastName: 'Cohen',
giveMeName: function () {
console.log(this.firstName + " " + this.lastName);
}
}
var firstName = 'Zeev';
var lastName = 'Revah';
var otherObject = {
myMethod: function(callback) {
callback();
}
}
otherObject.myMethod(person.giveMeName.bind(person)); //Moshe Cohen
מה שחשוב פה הוא ה-bind שמופיע לקראת הסוף, אני מעביר דרכו את הקונטקסט. נחמד וברור, לא? bind מאפשר לי לקבוע את ה-this לכל פונקציה.
ה-bind יפעיל את ה-this ברגע שהפונקציה תיקרא. אז הוא מאוד שימושי אם אני הולך לקרוא למתודה יותר מאוחר, כמו למשל בקולבקים או אירועים. יש עוד דרך לקבוע את הקונטקסט וזה באמצעות call או apply שמופעלות ברגע הקריאה למתודה. נשמע מסובך? ממש לא!
var person = {
firstName: 'Moshe',
lastName: 'Cohen',
giveMeName: function () {
console.log(this.firstName + " " + this.lastName);
}
}
var firstName = 'Zeev';
var lastName = 'Revah';
var otherObject = {
myMethod: function(callback) {
callback.apply(person);
}
}
otherObject.myMethod(person.giveMeName); //Moshe Cohen
מה ההבדל בין call ל-apply? בדרך שבה אנחנו מעבירים ארגומנטים נוספים. במקרה שלנו ל-giveMeName אין ארגומנטים, אבל אם היו הייתי יכול להעביר אותם ככה:
function add(a, b){
return a + b;
}
assert( add.call(this, 1, 2) == 3, ".call() takes individual arguments" );
assert( add.apply(this, [1, 2]) == 3, ".apply() takes an array of arguments" );
עד כאן זה פשוט, נכון? בכל פעם שקוראים למתודה, צריך לזכור את הקונטקסט שבה היא עובדת. יש הרבה פעמים שזה לא רלוונטי ויש פעמים שלא. הבעיה היא שלא תמיד אנחנו מודעים לזה שאנחנו מפעילים מתודה או פונקציות. זה קורה בעיקר בפונקציות אנונימיות. הנה דוגמה קטנה:
var person = {
firstName: 'Moshe',
lastName: 'Cohen',
moods: ['happy', 'angry', 'sad'],
giveMeName: function () {
this.moods.forEach(function(mood) {
console.log(this.firstName + " " + this.lastName + ' Can be ' + mood);
});
}
}
person.giveMeName();
//"undefined undefined Can be happy"
//"undefined undefined Can be angry"
//"undefined undefined Can be sad"
אם נריץ את זה, אנחנו נקבל undefined undefined Can be happy, undefined undefined Can be angry… למה? למה מה שיש בקוד ה-foreach המסכן הזה לא יודע מה הוא ה-this? הקונטקסט הוא אותו קונטקסט, לא? התשובה היא לא. בתוך ה-foreach, אם תשימו לב, יש פונקציה אנונימית שמקבלת this משלה.
מה הפתרון? להשתמש ב-closure כדי להעביר לפונקציה הפנימית את משתנה ה-this שאנחנו רוצים להעביר. למי שלא יודע, closure זה המנגנון ב-JavaScript שמאפשר לנו שיתוף משתנים בין פונקצית האב לפונקצית הבן. במקרה הזה, אני אכניס את ה-this למשתנה אחר, שהוא this_ אבל אפשר לקרוא לו בכל שם שרוצים (הרבה קוראים לזה that). הנה הדוגמה המיוחלת:
var person = {
firstName: 'Moshe',
lastName: 'Cohen',
moods: ['happy', 'angry', 'sad'],
giveMeName: function () {
var _this = this;
this.moods.forEach(function(mood) {
console.log(_this.firstName + " " + _this.lastName + ' Can be ' + mood);
});
}
}
person.giveMeName();
//"Moshe Cohen Can be happy"
//"Moshe Cohen Can be angry"
//"Moshe Cohen Can be sad"
רואים כמה זה פשוט ונחמד? בגדול, אם הפונקציה/מתודה שאנחנו קוראים לה לא מתנהגת כמו שאנחנו חושבים שהיא מתנהגת, הסבירות שמדובר בבעית קונטקסט הוא גבוה. רוב המתכנתים המנוסים יודעים להתגבר על הבעיות האלו גם בלי לדעת את הידע התיאורטי מאחוריהן. אני למשל ידעתי להשתמש ב-this_ הרבה לפני שידעתי להסביר על קונטקסט. אבל יותר כיף לדעת משהו לעומק, לא?
2 תגובות
Hey Ran ,
Thanks for the post.
It is worth mentioning that ES6 has figured that out already.
using newly arrow functions will bind _this to `this` automatically.
Arrow functions have two additional minor differences from regular functions: a. they can’t be used as object constructors b. the special arguments variable isn’t available in arrow functions.
בדוגמה אחרונה אפשר אפשר להמנה מלהישתמש ב-_this בשתי דרכים: בפונקציית חץ או בארגומנט נוסף של forEach: https://gist.github.com/michacom/25c238a05ae5e08df11cbeeb7ac59a66