משמעות מילת המפתח this ב-javascript

שייך לקטגוריות JavaScript

הגדרה – this הוא האובייקט שקרא לפונקציה

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

מה זה בעצם scope? כל פונקציה מקבלת מרחב משלה להגדיר בו משתנים ומאפיינים. כשאנחנו רוצים לגשת למשתנה כלשהו אנחנו צריכים לעשות זאת מהמרחב בו הוא הוגדר. ב-javascript, כאשר איננו מגדירים פונקציה, הכל קורה ב-scope הגלובאלי, כלומר ה-window.

קוד

var o={}; // declaring new object
o.name="moon";
o.method=function () {
	alert(this.name);
};
o.method();

הגדרנו אובייקט חדש בשם o והגדרנו לו מאפיין מסוג פונקציה שמבצע alert ל-this.name.
אך מהיכן this מגיע?
בהפעלת הפונקציה, o.method(), נקבל את הערך "moon". התהליך שקורה כאן הוא כזה:

  1. יצרנו משתנה מסוג מחרוזת בעל הערך "moon" ושמנו אותו במאפיין name של האובייקט o.
  2. יצרנו פונקציה עם תוכן מסויים ושמנו אותה כמאפיין ל-o.
  3. הפעלנו את הפונקציה כאשר o הוא זה שקורא לה – הוא זה שמופיע לפני הנקודה.
  4. הפונקציה מופעלת כאשר this הוא בעצם רפרנס ל-o. נקבל o.name שערכו הוא "moon".

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

מכך, אפשר להתחכם קצת:

var o={}; // declaring new object
o.name="moon";
o.method=function () {
	alert(this.name);
};

var x={};
x.name="sun";
x.method=o.method;
x.method();

מה עשינו כאן? יצרנו אובייקט חדש בשם x עם מאפיין name בעל הערך sun.

את הרפרנס לפונקציה שהגדרנו ושמנו ב-o שמנו גם ב-x.method. בהפעלה של x.method לא נקבל, חס וחלילה, את moon, כי אין שום שיוך ממשי ל-o. נקבל את sun כי x הוא זה שקרא לפונקציה, וערכו של this נקבע לפי מי שקרא לפונקציה.

אפשר לומר שלכל פונקציה ב-javascript, נוסף ארגומנט בלתי נראה ששמו this. ובנוסף על כך, בכל קריאה לפונקציה, נשלח הארגומנט הזה מאחורי הקלעים עם הערך של אותו אובייקט שקרא לפונקציה.

function f() { alert(this.name); }

הופכת ל

function f(this) { alert(this.name); }

כלומר:

var o={}; // declaring new object
o.name="moon";
o.method=function (this) {
	alert(this.name);
};
o.method(o);
 
var x={};
x.name="sun";
x.method=o.method;
x.method(x);

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

class Foo {
	string name="foo";
	string GetName() { return this.name; }
}

Foo o=new o();
string s=o.GetName();

בעצם הופך להיות מעין:

class Foo {
	string name="foo";
	string GetName(Foo this) { return this.name; }
}

Foo o=new Foo();
string s=o.GetName(o);

שינוי ערכו של this

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

javascript נותנת לנו כלים לקרוא לפונקציה עם this אחר מבלי לשבור את הראש יותר מדי, בעזרת המתודות call/apply. שתי המתודות דומות: הן מתודות של פונקציה (יושבות ב-Function.prototype) שמקבלות אובייקט וארגומנטים, וקוראות לפונקציה עצמה עם הארגומנטים. השונה הוא דרך שליחת הארגומנטים – call מקבל אותם כארגומנטים רגילים (כמו params של C#) ואילו apply מקבל אותם כמערך.

את הקוד העליון, נשנה כדי שיפעל עם call:

var o={}; // declaring new object
o.name="moon";
o.method=function () {
	alert(this.name);
};

var x={};
x.name="sun";
o.method.call(x); 
מה שיקרה פה, מאחורי הקלעים:
x.temporaryMethod=o.method;
x.temporaryMethod();

האופרטור new ו-this

פונקציה ב-javascript היא לא רק מספר statements להפעלה מאוחר יותר, היא גם מתפקדת כמחלקה. כל פונקציה היא גם מחלקה. ממחלקה אפשר ליצור מופעים.

function Foo(name) {
	this.name=name;
}

הפונקציה הזו, כמו כל הפונקציות, משמשת גם כמחלקה, ומופע שלה אנו יוצרים כך:

var instance=new Foo("bar");
alert(instance.name); // "bar"

מה בעצם קרה כאן מאחורי הקלעים?בעת הפעלת new Foo("bar")

var instance={}; // declaring new object
Foo.call(instance,name); // calling the constructor with the new object as this, and the argument "name"

הפעלת הבנאי תפעיל את הביטוי:

this.name=name; // this is the "instance" object. assign name as the sent argument

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

this ו-prototype

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

ב-javascript מספר מחלקות מוגדרות מראש:
Object
Function
String
Number
Array
Date
Error
RegExp

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

ב-C#3.0 יש פיצ'ר חדש שנקרא extension method, שמרחיב בעצם פונקציונליות של מחלקות מוגדרות כמו string, int וכו'

ב-ruby גם כן הפיצ'ר קיים.
String.prototype.trim=function () {
	return this.replace(/^\s+|\s+$/g,"");
};
var s="    x        ";
alert("<"+s+">");
alert("<"+s.trim()+">");

לאחר הרחבת ה-prototype של המחלקה String במאפיין בשם trim שמכיל פונקציה שמקצצת רווחים מתחילת וסוף המחרוזת, נוכל לגשת מתוך כל מחרוזת למתודה הזו. this בתוך הפונקציה בעצם יכיל את הערך של המחרוזת, כי משתנה המחרוזת הוא זה שקרא לפונקציה.

DOM ו-this

<div id="div" onclick="alert(this.innerHTML);">div</div>

ה-DOM ידע לפרש את קוד ה-HTML לקוד הבא:

div.onclick=function () {
	alert(this.innerHTML);
};

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

הדפדפן יודע לשים "מאזין" ללחיצת עכבר, ובלחיצה הוא בודק האם המיקום של העכבר נמצא על אותו div. אם כן, הוא בודק האם ל-div יש מאפיין בשם onclick, ואם כן הוא פשוט מפעיל את הפונקציה בצורה כזו:

div.onclick();

כך, this יקבל רפרנס ל-div.

לסיכום

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



קטגוריות

חיפוש

עיקבו אחרי (אקספרימנטלי!)

9 תגובות

כתיבת תגובה

09.05.07 בשעה 18:26
ניר טייב
(אתר)

יש לציין שבאמצעות המתודה call שתיארת כאן אפשר לבצע קריאה למתודת האב כאשר עושים ירושה.

var Derived = function(){
 this.y = function(){
 Base.prototype.y.call(this);
 }
}

כמו שעושים super בJava.

ויש גם את המתודה apply שבעזרתה אפשר להעביר פרמטרים גם למתודה (גם ב-call אפשר אבל apply דורשת את הפרמטר הזה).

1
09.05.07 בשעה 19:37
רן פולק

מעניין.. תודה :-)

2
10.05.07 בשעה 11:50
אלעד
ניר טייב אמר/ה:
יש לציין שבאמצעות המתודה call שתיארת כאן אפשר לבצע קריאה למתודת האב כאשר עושים ירושה.

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

3
12.05.07 בשעה 22:04
אוריאל כץ
(אתר)

שכחת להגיד שהPROTOTYPE בעצם משמש ליצרת האובייקט כאשר new קורה.
זאת אומרת כאשר עושים new Foo() מה שקורה זה שנוצר העתק של הPROTOTYPE ואז מופעל הCONSTRUCTOR,ואם רק הייתה מוריד את הסוגריים הייתה מקבל העתק של הPROTOTYPE מה שמאפשר ליצור ירושה פשוטה על ידי השמה של הPROTOTYPE למחלקה אחרת,לדוגמא:
Derived.prototype = new Foo

4
13.05.07 בשעה 12:18
אלעד
אוריאל כץ אמר/ה:
שכחת להגיד שהPROTOTYPE בעצם משמש ליצרת האובייקט כאשר new קורה.

זאת אומרת כאשר עושים new Foo() מה שקורה זה שנוצר העתק של הPROTOTYPE ואז מופעל הCONSTRUCTOR,ואם רק הייתה מוריד

את הסוגריים הייתה מקבל העתק של הPROTOTYPE מה שמאפשר ליצור ירושה פשוטה על ידי השמה של הPROTOTYPE למחלקה

אחרת,לדוגמא:

Derived.prototype = new Foo

ה-prototype לא מועתק רק ביצירה של מופע (new) אלא בכל פנייה למאפיין של אובייקט, המנוע בודק האם המאפיין הזה קיים

לאובייקט עצמו ואם הוא לא מוצא אותו – הוא בודק ב-prototype של ה-constructor שלו (המחלקה, כלומר הפונקציה ממנה הוא

נוצר).

דוגמה:

function Base() { }
Base.prototype.a=function () { alert("a"); };

var b=new Base;
b.a();

Base.prototype.b=function () { alert("b"); };
b.b();

אחרי יצירת המופע הוספתי עוד מתודה בשם b ובפנייה אליה המנוע מוצא אותה ב-prototype ומפעיל אותה.

דבר נוסף, אין הבדל בין new Foo לבין ()new Foo, שניהם מפעילים את הבנאי.

לגבי ירושה:
new Foo יוצר אובייקט עם מאפיינים. ברגע שאתה שם אותו ב-prototype של מחלקה אחרת, כל הרפרנסים של המתודות של אותו

אובייקט יהיו כפרופרטיז למחלקה החדשה.

בדיוק כמו לעשות:

Derived.prototype={ a:Base.prototype.a, b:Base.prototype.b };
// or
Derived.prototype.a=Base.prototype.a;
Derived.prototype.b=Base.prototype.b;

5
13.05.07 בשעה 12:20
אלעד
אוריאל כץ אמר/ה:
שכחת להגיד שהPROTOTYPE בעצם משמש ליצרת האובייקט כאשר new קורה.

זאת אומרת כאשר עושים new Foo() מה שקורה זה שנוצר העתק של הPROTOTYPE ואז מופעל הCONSTRUCTOR,ואם רק הייתה מוריד את הסוגריים הייתה מקבל העתק של הPROTOTYPE מה שמאפשר ליצור ירושה פשוטה על ידי השמה של הPROTOTYPE למחלקה אחרת,לדוגמא:

Derived.prototype = new Foo

ה-prototype לא מועתק רק ביצירה של מופע (new) אלא בכל פנייה למאפיין של אובייקט, המנוע בודק האם המאפיין הזה קיים לאובייקט עצמו ואם הוא לא מוצא אותו – הוא בודק ב-prototype של ה-constructor שלו (המחלקה, כלומר הפונקציה ממנה הוא נוצר).

דוגמה:

function Base() { }
Base.prototype.a=function () { alert("a"); };

var b=new Base;
b.a();

Base.prototype.b=function () { alert("b"); };
b.b();

אחרי יצירת המופע הוספתי עוד מתודה בשם b ובפנייה אליה המנוע מוצא אותה ב-prototype ומפעיל אותה.

דבר נוסף, אין הבדל בין new Foo לבין ()new Foo, שניהם מפעילים את הבנאי.

לגבי ירושה:
new Foo יוצר אובייקט עם מאפיינים. ברגע שאתה שם אותו ב-prototype של מחלקה אחרת, כל הרפרנסים של המתודות של אותו אובייקט יהיו כפרופרטיז למחלקה החדשה.

בדיוק כמו לעשות:

Derived.prototype={ a:Base.prototype.a, b:Base.prototype.b };
// or
Derived.prototype.a=Base.prototype.a;
Derived.prototype.b=Base.prototype.b;

6
13.05.07 בשעה 12:20
אלעד
אוריאל כץ אמר/ה:
שכחת להגיד שהPROTOTYPE בעצם משמש ליצרת האובייקט כאשר new קורה.

זאת אומרת כאשר עושים new Foo() מה שקורה זה שנוצר העתק של הPROTOTYPE ואז מופעל הCONSTRUCTOR,ואם רק הייתה מוריד את הסוגריים הייתה מקבל העתק של הPROTOTYPE מה שמאפשר ליצור ירושה פשוטה על ידי השמה של הPROTOTYPE למחלקה אחרת,לדוגמא:

Derived.prototype = new Foo

ה-prototype לא מועתק רק ביצירה של מופע (new) אלא בכל פנייה למאפיין של אובייקט, המנוע בודק האם המאפיין הזה קיים לאובייקט עצמו ואם הוא לא מוצא אותו – הוא בודק ב-prototype של ה-constructor שלו (המחלקה, כלומר הפונקציה ממנה הוא נוצר).

דוגמה:

function Base() { }
Base.prototype.a=function () { alert("a"); };

var b=new Base();
b.a();

Base.prototype.b=function () { alert("b"); };
b.b();

אחרי יצירת המופע הוספתי עוד מתודה בשם b ובפנייה אליה המנוע מוצא אותה ב-prototype ומפעיל אותה.

דבר נוסף, אין הבדל בין new Foo לבין ()new Foo, שניהם מפעילים את הבנאי.

לגבי ירושה:
new Foo יוצר אובייקט עם מאפיינים. ברגע שאתה שם אותו ב-prototype של מחלקה אחרת, כל הרפרנסים של המתודות של אותו אובייקט יהיו כפרופרטיז למחלקה החדשה.

בדיוק כמו לעשות:

Derived.prototype={ a:Base.prototype.a, b:Base.prototype.b };
// or
Derived.prototype.a=Base.prototype.a;
Derived.prototype.b=Base.prototype.b;

7
16.05.07 בשעה 16:59
אוריאל כץ
(אתר)

צודק בקשר ל סוגריים עם NEW.

8
25.02.09 בשעה 10:23
BomBas
(אתר)

יפה מאוד! אני מאוד אוהב את הבלוג הזה, אתה נשמע לי איש חכם מאוד (:
אני אישית משתמש לא מעט ב PROTOTYPE וב JQUERY (לא כתבת על זה, אבל זו ספרייה דומה ל PROTOTYPE) וזה באמת מקל את העבודה.

כל הכבוד.

9

כתיבת תגובה

תגיות מותרות לשימוש בתוכן
XHTML: אפשר להשתמש בתגים הללו: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong> <pre>