הבנת הערך של this
יכולה להיות מסובכת ב-JavaScript. כך עושים זאת...
ה-this
של JavaScript כולל בדיחות רבות, והסיבה לכך היא שהוא די מסובך.
אבל ראיתי שמפתחים עושים דברים הרבה יותר מורכבים וספציפיים לדומיין כדי להימנע מטיפול ב-this
הזה. אם יש לך ספק לגבי this
, אני מקווה שהמידע הזה יעזור לך. זה המדריך שלי ב-this
.
אתחיל עם המצב הספציפי ביותר, ונסיים עם המצב הכי פחות ספציפי. המאמר הזה דומה לif (…) … else if () … else if (…) …
גדול, אז אתם יכולים לעבור ישירות לקטע הראשון שתואם לקוד שאתם מעיינים בו.
- אם הפונקציה מוגדרת כפונקציית חץ
- לחלופין, אם הפונקציה/מחלקה נקראים עם
new
- אחרת, אם לפונקציה יש ערך
this
'bound'. - לחלופין, אם המדיניות
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
על ידי קריאה לפונקציה כבונה:
// TypeError: arrowFunction is not a constructor
new arrowFunction();
שיטות מכונות 'Bound'
בעזרת שיטות של מכונות, אם רוצים לוודא שהפרמטר this
תמיד יתייחס למכונה של המחלקה, הדרך הטובה ביותר היא להשתמש בפונקציות של חיצים ובשדות מחלקה:
class Whatever {
someMethod = () => {
// Always the instance of Whatever:
console.log(this);
};
}
הדפוס הזה מאוד שימושי כשמשתמשים בשיטות של מכונות כמו פונקציות event listener ברכיבים (כמו רכיבי React או רכיבי אינטרנט).
התוכן שלמעלה עלול להיראות כאילו הוא מפר את הכלל "this
יהיה זהה לערך של this
בהיקף ההורה", אבל זה יהיה הגיוני אם חושבים על שדות כיתה כסוכר תחבירי להגדרת דברים
במבנה:
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);
};
}
}
פטנטים חלופיים כוללים קישור של פונקציה קיימת במבנה, או הקצאת הפונקציה בבונה. אם מסיבה כלשהי לא ניתן להשתמש בשדות מחלקה, הקצאת פונקציות ב-builder היא חלופה סבירה:
class Whatever {
constructor() {
this.someMethod = () => {
// …
};
}
}
אחרת, אם הפונקציה/מחלקה קריאה באמצעות new
:
new Whatever();
הקוד שלמעלה יקרא ל-Whatever
(או לפונקציה הבונה שלו אם היא מחלקה) עם 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
מוגדר לערך אחר על ידי כלים כמו פונקציות event listener 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
מוגדר כ-False כי לא מתבצעת קריאה ל-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'
בפונקציה אם היקף ההורה נמצא במצב מחמיר (וכל המודולים נמצאים במצב strict).
אחרת:
function someFunction() {
return this;
}
// Logs `true`:
console.log(someFunction() === globalThis);
במקרה הזה, הערך של this
זהה לערך globalThis
.
סוף סוף!
זהו, סיימתם. זה כל מה שאני יודע על this
. יש לכם שאלות? יש משהו שפספסתי? אל תהססו לשלוח לי ציוץ.
תודה על הביקורת: Mathias Bynens, Ingvar Stepanyan ו-תומאס סטיינר.