JavaScript: מה המשמעות של זה?

לא קל להבין את הערך של this ב-JavaScript...

Jake Archibald
Jake Archibald

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

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

  1. אם הפונקציה מוגדרת כפונקציית חץ
  2. אחרת, אם קוראים לפונקציה/לכיתה באמצעות new
  3. אחרת, אם לפונקציה יש ערך this 'מוגדר'
  4. אחרת, אם this מוגדר בזמן הקריאה
  5. אחרת, אם הפונקציה נקראת דרך אובייקט הורה (parent.func())
  6. לחלופין, אם ההיקף של הפונקציה או של ההורה נמצא במצב מחמיר
  7. אחרת

אם הפונקציה מוגדרת כפונקציית חץ:

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. יש לכם שאלות? יש משהו שפספסתי? אפשר לשלוח לי ציוץ.

תודה על הבדיקה של מתיאס בינס, אינגוואר סטפניאן ותומס סטיינר.