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

הבנת הערך של this יכולה להיות מסובכת ב-JavaScript. כך עושים זאת...

ג'ייק ארצ'יבלד
ג'ייק ארצ'יבלד

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

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

  1. אם הפונקציה מוגדרת כפונקציית חץ
  2. לחלופין, אם הפונקציה/מחלקה נקראים עם new
  3. אחרת, אם לפונקציה יש ערך this 'bound'.
  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 על ידי קריאה לפונקציה כבונה:

// 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 ו-תומאס סטיינר.