למה לקשור פונקציה לעצמה?

02/08/2019

בפוסט של אתמול הופיעה שורה קצת מוזרה:

  constructor() {
    this.inc = this.inc.bind(this);
  }

למה לקשור את הפונקציה לעצמה? איך זה משפיע על הקוד? והאם דברים יעבדו גם בלי זה? בואו ניקח 5 דקות לדבר קצת על bind ו this ואני מקווה שבסופן נבין את התעלומה.

1. משפט וחצי על this ב JavaScript

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

class Foo {
  constructor(name) {
    this.count = name.length;
  }

  show() {
    console.log(`your name has ${this.count} letters`);
  }
}

const f = new Foo('ynon');
f.show();

הקוד עובד ומדפיס שבשם שלי יש 4 אותיות כמו שחשבנו שיקרה, בגלל שבכניסה לפונקציה show המשתנה this מתיחס לאותו דבר שהמשתנה f מתיחס אליו בקוד החיצוני.

אבל החיבור הזה בין הפונקציה show לבין המשתנה f בקוד החיצוני הוא שברירי וזמני. למעשה השם האמיתי של הפונקציה הוא Foo.prototype.show והשורה f.show היא בסך הכל קיצור דרך לפקודה הקצת יותר ארוכה:

Foo.prototype.show.call(f);

מה שעוד יותר מוזר הוא שקוד JavaScript הרבה פעמים "בטעות" שובר את החיבור הזה. הנה דוגמא אחת בה זה יכול לקרות:

class Foo {
  constructor(name) {
    this.count = name.length;
  }

  show() {
    console.log(`your name has ${this.count} letters`);
  }

  showLater() {
    setTimeout(this.show, 500);
  }
}

const f = new Foo('ynon');
f.showLater();

הפעם אנחנו מקבלים את התוצאה:

your name has undefined letters

הסיבה לבעיה היא שהחיבור בין הפונקציה Foo.prototype.show לבין המשתנה f החיצוני הוא זמני: הוא קיים רק כי f עבר כפרמטר נסתר לפונקציה, ואנחנו באותה שרשרת קריאות. ברגע שהוספתי setTimeout, כשהדפדפן יגיע להפעיל את show הוא כבר לא יעשה את זה עם אותו this.

2. קשירת הפונקציה לעצמה

עכשיו אפשר לדבר על השורה שלמעלה:

    this.inc = this.inc.bind(this);

ולשים לב שהמטרה שלה היא לא לקשור את הפונקציה לעצמה, אלא לקשור את הפונקציה ל this. ננסה את אותו קוד עם ה setTimeout אחרי הקשירה:

class Foo {
  constructor(name) {
    this.count = name.length;
    this.show = this.show.bind(this);
  }

  show() {
    console.log(`your name has ${this.count} letters`);
  }

  showLater() {
    setTimeout(this.show, 500);
  }
}

const f = new Foo('ynon');
f.showLater();

והתוצאה הפעם תקינה:

your name has 4 letters

החיבור של הפונקציה ל this גורם לזה שלא משנה מתי ואיך נפעיל אותה, המשתנה this בתוך גוף הפונקציה תמיד יכיל את האוביקט ש new Foo יצר. זאת ההתנהגות שאנחנו רגילים אליה מ Java או Python אבל היא לא ברירת המחדל ב JavaScript.

3. האם צריך תמיד לקשור פונקציות?

אז האם צריך להתחיל כל קלאס בשורת bind-ים? התשובה הקצרה היא לא. זה אולי לא מזיק אבל זה כן יכול לבלבל קצת (כמו שקרה בפוסט של אתמול) ולקחת קצת יותר זיכרון. אנחנו נפעיל bind רק כשאנחנו חושבים שמישהו עלול להפעיל את הפונקציה שלנו מחוץ לקונטקסט וש this עלול להתקלקל.

4. ויש גם קיצור דרך

ולפני שניפרד שווה להזכיר שיש גם קיצור דרך. הקוד הזה מפעיל אוטומטית את bind על הפונקציה show איפה שנתמך:

class Foo {
  constructor(name) {
    this.count = name.length;
  }

  show = () => {
    console.log(`your name has ${this.count} letters`);
  }

  showLater() {
    setTimeout(this.show, 500);
  }
}

אבל היו זהירים כי הוא לא נתמך ברוב הדפדפנים ולא ב Node.JS.