יכול להיות שיהיה לכם קשה למצוא את הערך של this
ב-JavaScript. כך עושים את זה…
this
של JavaScript הוא מושא של בדיחות רבות, כי הוא פשוט מורכב מאוד.
עם זאת, ראיתי מפתחים שמבצעים פעולות הרבה יותר מורכבות וייחודיות לדומיין כדי להימנע מהטיפול ב-this
הזה. אם לא ברור לך מהו this
, אולי המידע הזה יעזור לך. זה המדריך שלי בנושא this
.
אתחיל מהמצב הספציפי ביותר ואסיים במצב הפחות ספציפי. המאמר הזה הוא כמו if (…) … else if () … else if (…) …
גדול, כך שאפשר לעבור ישירות לקטע הראשון שמתאים לקוד שבודקים.
- אם הפונקציה מוגדרת כפונקציית חץ
- אחרת, אם קוראים לפונקציה/לכיתה באמצעות
new
- אחרת, אם לפונקציה יש ערך
this
'קשור' - אחרת, אם
this
מוגדר בזמן הקריאה - אחרת, אם הפונקציה נקראת דרך אובייקט הורה (
parent.func()
) - אחרת, אם הפונקציה או היקף ההורה נמצאים במצב קפדני
- אחרת
אם הפונקציה מוגדרת כפונקציית חץ:
const arrowFunction = () => {
console.log(this);
};
במקרה כזה, הערך של this
תמיד זהה לערך של this
בהיקף ההורה:
const outerThis = this;
const arrowFunction = () => {
// Always logs `true`:
console.log(this === outerThis);
};
פונקציות החץ נהדרות כי אי אפשר לשנות את הערך הפנימי של this
, הוא תמיד זהה ל-this
החיצוני.
דוגמאות נוספות
בפונקציות החץ, אי אפשר לשנות את הערך של this
באמצעות bind
:
// Logs `true` - bound `this` value is ignored:
arrowFunction.bind({foo: 'bar'})();
בפונקציות החץ, אי אפשר לשנות את הערך של this
באמצעות call
או apply
:
// Logs `true` - called `this` value is ignored:
arrowFunction.call({foo: 'bar'});
// Logs `true` - applied `this` value is ignored:
arrowFunction.apply({foo: 'bar'});
בפונקציות החץ, אי אפשר לשנות את הערך של this
על ידי קריאה לפונקציה כחבר של אובייקט אחר:
const obj = {arrowFunction};
// Logs `true` - parent object is ignored:
obj.arrowFunction();
בפונקציות החץ, אי אפשר לשנות את הערך של this
על ידי קריאה לפונקציה כ-constructor:
// TypeError: arrowFunction is not a constructor
new arrowFunction();
שיטות 'מוגבלות' למכונות
כשמשתמשים בשיטות של מכונות, כדי לוודא ש-this
תמיד מתייחס למכונה של הכיתה, הדרך הטובה ביותר היא להשתמש בפונקציות חץ ובשדות של הכיתה:
class Whatever {
someMethod = () => {
// Always the instance of Whatever:
console.log(this);
};
}
התבנית הזו שימושית מאוד כשמשתמשים בשיטות של מכונות כמפעילי אירועים ברכיבים (כמו רכיבי React או רכיבי אינטרנט).
יכול להיות שהקוד שלמעלה נראה כאילו הוא מפר את הכלל "this
יהיה זהה ל-this
בהיקף ההורה", אבל הוא מתחיל להיראות הגיוני אם חושבים על שדות הכיתה כסוכר סמנטי להגדרת דברים ב-constructor:
class Whatever {
someMethod = (() => {
const outerThis = this;
return () => {
// Always logs `true`:
console.log(this === outerThis);
};
})();
}
// …is roughly equivalent to:
class Whatever {
constructor() {
const outerThis = this;
this.someMethod = () => {
// Always logs `true`:
console.log(this === outerThis);
};
}
}
דפוסים חלופיים כוללים קישור של פונקציה קיימת ב-constructor, או הקצאה של הפונקציה ב-constructor. אם מסיבה כלשהי אי אפשר להשתמש בשדות של הכיתה, אפשר להקצות פונקציות ב-constructor:
class Whatever {
constructor() {
this.someMethod = () => {
// …
};
}
}
אחרת, אם קוראים לפונקציה או לכיתה באמצעות new
:
new Whatever();
הקוד שלמעלה יפעיל את Whatever
(או את פונקציית ה-constructor שלו אם מדובר בכיתה), כאשר הערך של this
מוגדר בתוצאה של Object.create(Whatever.prototype)
.
class MyClass {
constructor() {
console.log(
this.constructor === Object.create(MyClass.prototype).constructor,
);
}
}
// Logs `true`:
new MyClass();
אותו הדבר נכון למבנים ישנים יותר:
function MyClass() {
console.log(
this.constructor === Object.create(MyClass.prototype).constructor,
);
}
// Logs `true`:
new MyClass();
דוגמאות נוספות
כשמפעילים את הפונקציה עם new
, אי אפשר לשנות את הערך של this
באמצעות bind
:
const BoundMyClass = MyClass.bind({foo: 'bar'});
// Logs `true` - bound `this` value is ignored:
new BoundMyClass();
כשמפעילים את הפונקציה עם new
, אי אפשר לשנות את הערך של this
על ידי קריאה לפונקציה כחבר של אובייקט אחר:
const obj = {MyClass};
// Logs `true` - parent object is ignored:
new obj.MyClass();
אחרת, אם לפונקציה יש ערך this
של 'bound':
function someFunction() {
return this;
}
const boundObject = {hello: 'world'};
const boundFunction = someFunction.bind(boundObject);
בכל פעם שמפעילים את boundFunction
, הערך של this
יהיה האובייקט שמעבירים ל-bind
(boundObject
).
// Logs `false`:
console.log(someFunction() === boundObject);
// Logs `true`:
console.log(boundFunction() === boundObject);
דוגמאות נוספות
כשקוראים לפונקציה מוגדרת, אי אפשר לשנות את הערך של this
באמצעות call
או apply
:
// Logs `true` - called `this` value is ignored:
console.log(boundFunction.call({foo: 'bar'}) === boundObject);
// Logs `true` - applied `this` value is ignored:
console.log(boundFunction.apply({foo: 'bar'}) === boundObject);
כשקוראים לפונקציה מקושרת, אי אפשר לשנות את הערך של this
על ידי קריאה לפונקציה כחבר של אובייקט אחר:
const obj = {boundFunction};
// Logs `true` - parent object is ignored:
console.log(obj.boundFunction() === boundObject);
אחרת, אם השדה this
מוגדר בזמן השיחה:
function someFunction() {
return this;
}
const someObject = {hello: 'world'};
// Logs `true`:
console.log(someFunction.call(someObject) === someObject);
// Logs `true`:
console.log(someFunction.apply(someObject) === someObject);
הערך של this
הוא האובייקט שמועובר אל call
/apply
.
לצערנו, this
מוגדר לערך אחר על ידי דברים כמו מאזינים לאירועי DOM, והשימוש בו עלול לגרום לקוד שקשה להבין:
element.addEventListener('click', function (event) {
// Logs `element`, since the DOM spec sets `this` to
// the element the handler is attached to.
console.log(this);
});
אני נמנע משימוש ב-this
במקרים כמו זה שלמעלה, ובמקום זאת:
element.addEventListener('click', (event) => {
// Ideally, grab it from a parent scope:
console.log(element);
// But if you can't do that, get it from the event object:
console.log(event.currentTarget);
});
אחרת, אם הפונקציה נקראת דרך אובייקט הורה (parent.func()
):
const obj = {
someMethod() {
return this;
},
};
// Logs `true`:
console.log(obj.someMethod() === obj);
במקרה כזה, הפונקציה נקראת כחברה ב-obj
, כך ש-this
יהיה obj
. זה קורה בזמן הקריאה, כך שהקישור נשבר אם קוראים לפונקציה בלי אובייקט ההורה שלה, או עם אובייקט הורה אחר:
const {someMethod} = obj;
// Logs `false`:
console.log(someMethod() === obj);
const anotherObj = {someMethod};
// Logs `false`:
console.log(anotherObj.someMethod() === obj);
// Logs `true`:
console.log(anotherObj.someMethod() === anotherObj);
הערך של someMethod() === obj
הוא שקר כי someMethod
לא נקרא כחבר בקבוצה obj
. יכול להיות נתקלתם בבעיה הזו כשניסיתם לבצע פעולה כמו:
const $ = document.querySelector;
// TypeError: Illegal invocation
const el = $('.some-element');
הבעיה נובעת מהעובדה שההטמעה של querySelector
בוחנת את הערך this
שלה ומצפה שהוא יהיה צומת DOM כלשהו, והקוד שלמעלה מפר את החיבור הזה. כדי להשיג את התוצאה הזו בצורה נכונה:
const $ = document.querySelector.bind(document);
// Or:
const $ = (...args) => document.querySelector(...args);
עובדה מעניינת: לא כל ממשקי ה-API משתמשים ב-this
באופן פנימי. שיטות מסוימות במסוף, כמו console.log
, שונו כדי להימנע מהפניות ל-this
, כך שלא צריך לשייך את log
ל-console
.
אחרת, אם הפונקציה או היקף ההורה נמצאים במצב קפדני:
function someFunction() {
'use strict';
return this;
}
// Logs `true`:
console.log(someFunction() === undefined);
במקרה כזה, הערך של this
לא מוגדר. לא צריך להוסיף את 'use strict'
לפונקציה אם ההיקף של ההורה נמצא במצב קפדני (וכל המודולים נמצאים במצב קפדני).
אחרת:
function someFunction() {
return this;
}
// Logs `true`:
console.log(someFunction() === globalThis);
במקרה כזה, הערך של this
זהה לערך של globalThis
.
סוף סוף!
זה הכול! זה כל מה שאני יודע על this
. יש לכם שאלות? יש משהו שפספסתי? אפשר לשלוח לי ציוץ.
תודה לMathias Bynens, ל-Ingvar Stepanyan ול-Thomas Steiner על הבדיקה.