Как упоминалось в одной из моих предыдущих статей, полное освоение JavaScript может оказаться долгим путешествием. Возможно, вы встречали this на своем пути в качестве разработчика JavaScript. Когда я только начинал, я впервые увидел это при использовании eventListeners и jQuery. Позже мне приходилось часто использовать его с React, и я уверен, что вы тоже это делали. Это не значит, что я действительно понял, что это такое и как полностью контролировать это.

Однако очень полезно овладеть концепцией, лежащей в основе этого, и, если подходить к ней с ясным умом, это тоже не очень сложно.

Копаясь в этом

Объяснение this может привести к большой путанице из-за простого наименования ключевого слова.

this тесно связан с тем, в каком контексте вы находитесь в своей программе. Давайте начнем с самого начала. В нашем браузере, если вы просто наберете this в консоли, вы получите window-объект, самый внешний контекст для вашего JavaScript. В Node.js, если мы это сделаем:

console.log(this)

мы получаем {}, пустой объект. Это немного странно, но похоже, что Node.js так себя ведет. Если вы это сделаете

(function() {
  console.log(this);
})();

однако вы получите объект global, самый внешний контекст. В этом контексте хранятся setTimeout, setInterval. Не стесняйтесь немного поэкспериментировать с ним, чтобы увидеть, что вы можете с ним сделать. Отсюда почти нет разницы между Node.js и браузером. Я буду использовать window. Просто помните, что в Node.js это будет объект global, но на самом деле это не имеет значения.

Помните: контекст имеет смысл только внутри функций

Представьте, что вы пишете программу, ничего не вкладывая в функции. Вы просто пишете одну строку за другой, не переходя по конкретным структурам. Это означает, что вам не нужно отслеживать, где вы находитесь. Вы всегда на одном уровне.

Когда вы начинаете использовать функции, у вас могут быть разные уровни вашей программы, и this представляет, где вы находитесь, какой объект вызвал функцию.

Отслеживание вызывающего объекта

Давайте посмотрим на следующий пример и посмотрим, как this изменяется в зависимости от контекста:

const coffee = {
  strong: true,
  info: function() {
    console.log(`The coffee is ${this.strong ? '' : 'not '}strong`)
  },
}
coffee.info() // The coffee is strong

Поскольку мы вызываем функцию, объявленную внутри объекта coffee, наш контекст изменяется именно на этот объект. Теперь мы можем получить доступ ко всем свойствам этого объекта через this. В нашем примере выше мы могли бы просто сослаться на него напрямую, выполнив coffee.strong. Становится интереснее, когда мы не знаем, в каком контексте, в каком объекте мы находимся или когда все становится немного сложнее. Взгляните на следующий пример:

Классы и экземпляры

Классы можно использовать для абстрагирования вашего кода и обмена поведением. Постоянно повторять объявление функции info в последнем примере - нехорошо. Поскольку классы и их экземпляры на самом деле являются объектами, они ведут себя одинаково. Однако следует отметить, что объявление this в конструкторе на самом деле является предсказанием на будущее, когда будет создан экземпляр.

Давайте взглянем:

class Coffee {
  constructor(strong) {
    this.strong = !!strong
  }
  info() {
    console.log(`This coffee is ${this.strong ? '' : 'not '}strong`)
  }
}
const strongCoffee = new Coffee(true)
const normalCoffee = new Coffee(false)
strongCoffee.info() // This coffee is strong
normalCoffee.info() // This coffee is not strong

Ловушка: беспрепятственно вложенные вызовы функций

Иногда мы оказываемся в контексте, которого на самом деле не ожидали. Это может произойти, когда мы неосознанно вызываем функцию внутри контекста другого объекта. Очень распространенный пример - использование setTimeout или setInterval:

// BAD EXAMPLE
const coffee = {
  strong: true,
  amount: 120,
  drink: function() {
    setTimeout(function() {
      if (this.amount) this.amount -= 10
    }, 10)
  },
}
coffee.drink()

Как вы думаете, что такое coffee.amount?

...

..

.

Это все еще 120. Сначала мы были внутри объекта coffee, поскольку внутри него объявлен метод drink. Мы только что сделали setTimeout и больше ничего. Именно так.

Как я объяснил ранее, метод setTimeout фактически объявлен в объекте window. При его вызове мы фактически снова переключаем контекст на window. Это означает, что наши инструкции на самом деле пытались изменить window.amount, но в итоге ничего не сделали из-за оператора if. Чтобы позаботиться об этом, мы должны bind наши функции (см. Ниже).

Реагировать

Надеемся, что с использованием React это скоро уйдет в прошлое, благодаря Hooks. На данный момент нам все еще нужно bind все (подробнее об этом позже) так или иначе. Когда я начинал, я понятия не имел, зачем я это делаю, но на данный момент вы уже должны знать, почему это необходимо.

Давайте посмотрим на два простых компонента класса React:

Когда мы теперь нажмем кнопку, отображаемую Child, мы получим сообщение об ошибке. Почему? Потому что React изменил наш контекст при вызове метода _getCoffee.

Я предполагаю, что React действительно вызывает метод рендеринга наших компонентов в другом контексте, через вспомогательные классы или аналогичные (хотя мне пришлось бы копнуть глубже, чтобы узнать наверняка). Следовательно, this.state не определено, и мы пытаемся получить доступ к this.state.coffeeCount. Вы должны получить что-то вроде Cannot read property coffeeCount of undefined.

Чтобы решить эту проблему, вам нужно bind (мы займемся этим) методы в наших классах, как только мы передадим их из компонента, в котором они определены.

Давайте посмотрим на еще один общий пример:

Мы передаем increaseCount из одного класса в другой. Когда мы вызываем метод increaseCount в Viking, мы уже изменили контекст, и this фактически указывает на Viking, что означает, что наш метод increaseCount не будет работать должным образом.

Решение - привязать

Самое простое решение для нас - это bind методы, которые будут передаваться из нашего исходного объекта или класса. Есть разные способы привязки функций, но наиболее распространенным (также в React) является привязка их в конструкторе. Поэтому нам нужно добавить эту строку в конструктор Battle перед строкой 18:

this.increaseCount = this.increaseCount.bind(this)

Вы можете привязать любую функцию к любому контексту. Это не означает, что вам всегда нужно связывать функцию с контекстом, в котором она объявлена ​​(однако это наиболее распространенный случай). Вместо этого вы можете привязать его к другому контексту. С bind вы всегда устанавливаете контекст для объявления функции. Это означает, что все вызовы этой функции будут получать связанный контекст как this. Есть еще два помощника для настройки контекста.

Стрелочные функции `() =› {} `автоматически привязывают функцию к контексту объявления

Подать заявку и позвонить

Они оба делают в основном одно и то же, только синтаксис отличается. Для обоих вы передаете контекст в качестве первого аргумента. apply принимает массив для других аргументов, а call вы можете просто разделить другие аргументы запятыми. Что они теперь делают? Оба эти метода устанавливают контекст для вызова одной конкретной функции. При вызове функции без call контекст устанавливается в контекст по умолчанию (или даже связанный контекст). Вот пример:

class Salad {
  constructor(type) {
    this.type = type
  }
}
function showType() {
  console.log(`The context's type is ${this.type}`)
}
const fruitSalad = new Salad('fruit')
const greekSalad = new Salad('greek')
showType.call(fruitSalad) // The context's type is fruit
showType.call(greekSalad) // The context's type is greek
showType() // The context's type is undefined

Вы можете догадаться, в каком контексте был последний вызов showType()?

..

.

Вы правы, это самая внешняя область, window. Следовательно, type это undefined, window.type нет

Вот и все, надеюсь, теперь у вас есть четкое представление о том, как использовать this в JavaScript. Не стесняйтесь оставлять предложения к следующей статье в комментариях.

Об авторе: Лукас Гисдер-Дубе стал соучредителем и руководил стартапом в качестве технического директора в течение полутора лет, создавая техническую команду и архитектуру. Покинув стартап, он преподавал программирование в качестве ведущего инструктора в Ironhack, а сейчас создает стартап-агентство и консалтинговое агентство в Берлине. Посетите dube.io, чтобы узнать больше.