לא קל להבין את הערך של 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();
שיטות 'מוגבלות' למכונות
ב-methods של מכונות, אם רוצים לוודא ש-this
תמיד יפנה למופע של המחלקה, הדרך הטובה ביותר היא להשתמש בפונקציות חץ ובשדות class:
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
באופן פנימי. methods של מסוף כמו 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
. יש לכם שאלות? יש משהו שפספסתי? אפשר לשלוח לי ציוץ.
תודה על הבדיקה של מתיאס בינס, אינגוואר סטפניאן ותומס סטיינר.