Howtojs

Как получить корректный this в callback?

7 февраля 2021 г. • ☕️ 5 мин.

Проблема

На равне с пониманием возврата из асинхронной функции, проблема с доступом к this в callback возникает достаточно часто.

Представим, у нас есть машина, которая посылает свои координаты каждые 3 секунды.

class Car {
  lat = 45.517441
  lon = -73.728358

  sendLocation() {
    console.log(`lat: ${this.lat}`)
    console.log(`lon: ${this.lon}`)
    // ...
  }
}

const car = new Car()

car.sendLocation()// lat: 45.517441
// lon: -73.728358

setInterval(car.sendLocation, 3000)// lat: undefined
// lon: undefined

Почему во втором случае, мы получаем не корректное поведение (теряем this)?

Что нужно знать про this?

this (или текущий контекст) - это специальное ключевое слово внутри функции и он не является фиксированным. Значение this зависит от того, как функция была вызвана и не зависит от того, как, где и когда функция была объявлена.

Внутри функции, this ссылается на объект “перед точкой”, который использовался для вызова метода.

В первом примере, мы вызываем функцию sendLocation на объекте car.

Во втором примере, мы берем функцию sendLocation на объекте car и передаем ее, как callback в функцию setInterval. В дальнейшем, ее вызывает кто-то другой, в нашем случае среда исполнения javascript при наступление заданного интервала времени.

Как получить правильный this?

Обернуть вызов в другую функцию

setInterval(function() {
  car.sendGPSLocation()
}, 3000)

setInterval(() => car.sendGPSLocation(), 3000)

В обоих случаях среда исполнения javascript вызовет анонимную функцию “обертку”, внутри который уже мы вызовем нужную нам функцию, указав на контекст this явно.

Использовать метод .bind

setInterval(car.sendGPSLocation.bind(car), 3000)

Метод .bind позволяем нам явно “привязать” контекст к функции. По сути он возвращает новую функцию, создавая обертку, как в первом случае.

Указать на this явно, в функциях, которые это поддерживают

array.map(callback[, thisArg])

Для примера метод .map позволяет опционально передать this.

Использовать стрелочные функции

В этом примере, код упадет с ошибкой, по той же причине - “потеря” this.

class Car {
  lastCoordinates = [
    { lat: 45.517441, lon: -73.728358 },
    { lat: 45.517441, lon: -73.728358 },
  ]

  sendGPSLocation(lat, lon) {
    console.log(`lat: ${lat}`)
    console.log(`lon: ${lon}`)
    // ...
  }

  sendLastCoorditanes() {
    this.lastCoordinates.forEach(function({ lat, lon }) {
      this.sendGPSLocation(lat, lon)      // TypeError: Cannot read property 'sendGPSLocation' of undefined
    })
  }
}

const car = new Car()

car.sendLastCoorditanes()

У стрелочных функций нет своего this. При обращении к this внутри стрелочной функции, его значение берётся из внешней “обычной” функции.

class Car {
  lastCoordinates = [
    { lat: 45.517441, lon: -73.728358 },
    { lat: 45.517441, lon: -73.728358 },
  ]

  sendGPSLocation(lat, lon) {
    console.log(`lat: ${lat}`)
    console.log(`lon: ${lon}`)
    // ...
  }

  sendLastCoorditanes() {
    this.lastCoordinates.forEach(({ lat, lon }) => {
      this.sendGPSLocation(lat, lon)    })
  }
}

const car = new Car()

car.sendLastCoorditanes()

Итоги

  • значение this у функций определяется в момент вызова
  • при вызове функции “через точку” car.sendLastCoorditanes(), значением this является объект перед точкой
  • при передачи функции куда-либо для дальнейшего вызова, контекст this будет потерян
  • контекст this нужно сохранять явно
  • использование стрелочных функций, позволит избежать потери контекста this