Используя простую терминологию и реальный пример, этот пост объясняет, что такое this и почему он полезен.

Это для тебя

Я заметил, что многие объяснения this в JavaScript преподаются в предположении, что вы используете какой-то объектно-ориентированный язык программирования, такой как Java, C ++ или Python. Этот пост предназначен для тех из вас, кто не имеет предвзятого мнения о том, что вы думаете this о том, что это такое или каким должно быть. Я попытаюсь объяснить что this и почему просто и без лишнего жаргона.

Может быть, вы откладывали погружение в this, потому что это выглядело странно и страшно. Или, может быть, вы используете его только потому, что StackOverflow говорит, что он вам нужен для выполнения определенных действий в React.

Прежде чем мы углубимся в то, что this на самом деле и почему вы должны его использовать, нам сначала нужно понять разницу между функциональным программированием и объектно-ориентированным программированием.

Функциональное и объектно-ориентированное программирование

Вы можете знать или не знать, что JavaScript имеет как функциональные, так и объектно-ориентированные конструкции, поэтому вы можете сосредоточиться на одном или другом или использовать оба.

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

И я был прав.

Вроде, как бы, что-то вроде. Возможно, вам удастся сосредоточиться только на одной парадигме и никогда не узнать о другой, но вы будете ограничены как разработчик JavaScript. Чтобы проиллюстрировать различия между функциональным и объектно-ориентированным программированием, я собираюсь использовать массив данных друзей Facebook в качестве примера.

Допустим, вы создаете веб-приложение, в котором пользователь входит в систему с помощью Facebook, и вы показываете некоторые данные об их друзьях в Facebook. Вам нужно будет подключиться к конечной точке Facebook, чтобы получить данные их друзей. Он может содержать некоторую информацию, например firstName, _10 _, _ 11_, numFriends, friendData, birthday и lastTenPosts.

const data = [
  {
    firstName: 'Bob',
    lastName: 'Ross',
    username: 'bob.ross',    
    numFriends: 125,
    birthday: '2/23/1985',
    lastTenPosts: ['What a nice day', 'I love Kanye West', ...],
  },
  ...
]

Приведенные выше данные - это то, что вы получаете от (поддельного, воображаемого) API Facebook. Теперь вам нужно преобразовать его, чтобы он был в формате, который будет полезен вам и вашему проекту. Допустим, вы хотите показать каждому из друзей пользователя следующее:

  • Их имя в формате `${firstName} ${lastName}`
  • Три случайных сообщения
  • Количество дней до дня рождения

Функциональный подход

Функциональный подход заключается в передаче всего массива или каждого элемента массива в функцию, которая возвращает необходимые вам управляемые данные:

const fullNames = getFullNames(data)
// ['Ross, Bob', 'Smith, Joanna', ...]

Вы начинаете с необработанных данных (из API Facebook). Чтобы преобразовать его в полезные для вас данные, вы передаете данные в функцию, а на выходе вы получаете обработанные данные, которые вы можете использовать в своем приложении для отображения пользователю.

Вы можете представить себе нечто подобное для получения трех случайных сообщений и расчета количества дней до дня рождения этого друга.

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

Объектно-ориентированный подход

Объектно-ориентированный подход может быть немного сложнее для понимания тем, кто плохо знаком с программированием и изучением JavaScript. Идея состоит в том, что вы превращаете каждого друга в объект, в котором есть все необходимое для создания того, что вам нужно как разработчику.

Вы можете создавать объекты со свойством fullName и двумя функциями getThreeRandomPosts и getDaysUntilBirthday, специфичными для этого друга.

function initializeFriend(data) {
  return {
    fullName: `${data.firstName} ${data.lastName}`,
    getThreeRandomPosts: function() {
      // get three random posts from data.lastTenPosts
    },
    getDaysUntilBirthday: function() {
      // use data.birthday to get the num days until birthday
    }
  };
}
const objectFriends = data.map(initializeFriend)
objectFriends[0].getThreeRandomPosts() 
// Gets three of Bob Ross's posts

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

При чем тут это?

Возможно, вы никогда не думали написать что-то вроде initializeFriend выше, и вы могли бы подумать, что что-то подобное может быть очень полезным. Однако вы можете заметить, что он не действительно объектно-ориентированный.

Единственная причина, по которой методы getThreeRandomPosts или getDaysUntilBirthday будут работать в приведенном выше примере, - это закрытие. У них все еще есть доступ к data после initializeFriend возвратов из-за закрытия. Для получения дополнительной информации о закрытии ознакомьтесь с You Don’t Know JS: Scope & Closures.

Что, если бы у вас был другой метод, назовем его greeting. Обратите внимание, что метод (в отношении объекта в JavaScript) - это просто атрибут, значением которого является функция. Мы хотим, чтобы greeting делал что-то вроде этого:

function initializeFriend(data) {
  return {
    fullName: `${data.firstName} ${data.lastName}`,
    getThreeRandomPosts: function() {
      // get three random posts from data.lastTenPosts
    },
    getDaysUntilBirthday: function() {
      // use data.birthday to get the num days until birthday
    },
    greeting: function() {
      return `Hello, this is ${fullName}'s data!`
    }
  };
}

Это сработает?

No!

Все в нашем вновь созданном объекте имеет доступ ко всем переменным в initializeFriend, но НЕ к каким-либо атрибутам или методам внутри самого объекта. Конечно, вы зададите вопрос:

Не могли бы вы просто использовать data.firstName и data.lastName, чтобы ответить на приветствие?

Да, вы могли бы. Но что, если бы мы также хотели указать в приветствии, сколько дней осталось до дня рождения этого друга? Мы должны каким-то образом найти способ вызвать getDaysUntilBirthday из greeting.

ВРЕМЯ ДЛЯ this!

Наконец, что это

this может относиться к разным вещам при разных обстоятельствах. По умолчанию this относится к глобальному объекту (в браузере это объект window), что не очень помогает. Правило this, которое нам сейчас полезно, следующее:

Если this используется в методе объекта и метод вызывается в контексте этого объекта, this относится к самому объекту.

Вы говорите «вызывается в контексте этого объекта»… что это вообще значит?

Не волнуйтесь, мы вернемся к этому позже!

Поэтому, если мы хотим вызвать getDaysUntilBirthday из greeting, мы можем просто вызвать this.getDaysUntilBirthday, потому что this в этом сценарии просто относится к самому объекту.

БОКОВОЕ ПРИМЕЧАНИЕ: Не используйте this в обычной оле-функции в глобальной области видимости или в области видимости другой функции! this - объектно-ориентированная конструкция. Следовательно, он имеет значение только в контексте объекта (или класса)!

Давайте проведем рефакторинг initializeFriend, чтобы использовать this:

function initializeFriend(data) {
  return {
    lastTenPosts: data.lastTenPosts,
    birthday: data.birthday,    
    fullName: `${data.firstName} ${data.lastName}`,
    getThreeRandomPosts: function() {
      // get three random posts from this.lastTenPosts
    },
    getDaysUntilBirthday: function() {
      // use this.birthday to get the num days until birthday
    },
    greeting: function() {
      const numDays = this.getDaysUntilBirthday()      
      return `Hello, this is ${this.fullName}'s data! It is ${numDays} until ${this.fullName}'s birthday!`
    }
  };
}

Теперь все, что нужно этому объекту, привязано к самому объекту после выполнения intializeFriend. Наши методы больше не полагаются на закрытие. Они используют только информацию, содержащуюся в самом объекте.

Хорошо, это один из способов использования this, но вы сказали, что this может быть много разных вещей в зависимости от контекста. Что это обозначает? Почему бы не всегда относиться к самому объекту?

Бывают случаи, когда вы хотите заставить this быть чем-то конкретным. Хороший пример - обработчики событий. Допустим, мы хотели открывать страницу друга в Facebook, когда пользователь нажимает на него. Мы могли бы добавить к нашему объекту метод onClick:

function initializeFriend(data) {
  return {
    lastTenPosts: data.lastTenPosts,
    birthday: data.birthday,
    username: data.username,    
    fullName: `${data.firstName} ${data.lastName}`,
    getThreeRandomPosts: function() {
      // get three random posts from this.lastTenPosts
    },
    getDaysUntilBirthday: function() {
      // use this.birthday to get the num days until birthday
    },
    greeting: function() {
      const numDays = this.getDaysUntilBirthday()      
      return `Hello, this is ${this.fullName}'s data! It is ${numDays} until ${this.fullName}'s birthday!`
    },
    onFriendClick: function() {
      window.open(`https://facebook.com/${this.username}`)
    }
  };
}

Обратите внимание, что мы добавили username к нашему объекту, чтобы у onFriendClick был доступ к нему, чтобы мы могли открыть новое окно со страницей Facebook этого друга. Теперь нам просто нужно написать HTML:

<button id="Bob_Ross">
  <!-- A bunch of info associated with Bob Ross -->
</button> 

А теперь JavaScript:

const bobRossObj = initializeFriend(data[0])
const bobRossDOMEl = document.getElementById('Bob_Ross')
bobRossDOMEl.addEventListener("onclick", bobRossObj.onFriendClick)

В приведенном выше коде мы создаем объект для Боба Росса. Мы получаем элемент DOM, связанный с Бобом Россом. А теперь мы хотим выполнить onFriendClick метод, чтобы открыть страницу Боба в Facebook. Должно работать как положено, не так ли?

Неа!

Что пошло не так?

Обратите внимание, что функция, которую мы выбрали для обработчика onclick, была bobRossObj.onFriendClick. Уже заметили проблему? Что, если бы мы переписали это так:

bobRossDOMEl.addEventListener("onclick", function() {
  window.open(`https://facebook.com/${this.username}`)
})

Теперь вы видите проблему? Когда мы устанавливаем обработчик onclick равным bobRossObj.onFriendClick, на самом деле мы получаем функцию, которая хранится в bobRossObj.onFriendClick, и передаем ее в качестве аргумента. Он больше не «привязан» к bobRossObj, что означает, что this больше не относится к bobRossObj. Фактически он относится к глобальному объекту, что означает, что this.username не определено. Похоже, нам сейчас не повезло.

ПРИШЛО ВРЕМЯ для bind!

Явно связывая это

Что нам нужно сделать, так это явно привязать this к bobRossObj. Мы можем сделать это с помощью bind:

const bobRossObj = initializeFriend(data[0])
const bobRossDOMEl = document.getElementById('Bob_Ross')
bobRossObj.onFriendClick = bobRossObj.onFriendClick.bind(bobRossObj)
bobRossDOMEl.addEventListener("onclick", bobRossObj.onFriendClick)

Ранее this устанавливался на основе правила по умолчанию. Используя bind, мы явно устанавливаем значение this в bobRossObj.onFriendClick как сам объект или bobRossObj.

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

Стрелочные функции

Вы могли заметить, что стрелочные функции - модная новинка. Людям они нравятся, потому что они лаконичны и элегантны. Возможно, вы знаете, что они немного отличаются от обычных функций, но, возможно, вы не совсем понимаете, в чем разница.

Пожалуй, самый простой способ описать, чем отличаются стрелочные функции:

Независимо от того, this относится к тому месту, где объявлена ​​функция стрелки, this относится к тому же самому объекту внутри этой функции стрелки.

Хорошо ... это бесполезно ... Я думал, что это нормальная функция?

Давайте объясним на нашем initializeFriend примере. Допустим, мы хотели добавить небольшую вспомогательную функцию в greeting:

function initializeFriend(data) {
  return {
    lastTenPosts: data.lastTenPosts,
    birthday: data.birthday,
    username: data.username,    
    fullName: `${data.firstName} ${data.lastName}`,
    getThreeRandomPosts: function() {
      // get three random posts from this.lastTenPosts
    },
    getDaysUntilBirthday: function() {
      // use this.birthday to get the num days until birthday
    },
    greeting: function() {
      function getLastPost() {
        return this.lastTenPosts[0]
      }
      const lastPost = getLastPost()           
      return `Hello, this is ${this.fullName}'s data!
             ${this.fullName}'s last post was ${lastPost}.`
    },
    onFriendClick: function() {
      window.open(`https://facebook.com/${this.username}`)
    }
  };
}

Это сработает? Если нет, как мы можем изменить его, чтобы он работал?

Нет, не пойдет. Поскольку getLastPost не вызывается в контексте объекта, this внутри getLastPost возвращается к правилу по умолчанию, которое является глобальным объектом.

Вы говорите, что он не вызывается «в контексте объекта»… разве вы не знаете, что он вызывается внутри объекта, который возвращается из initializeFriend? Если это не называется «в контексте объекта», тогда я не знаю, что это такое.

Я знаю, что «в контексте объекта» - это расплывчатая терминология. Возможно, хороший способ определить, вызывается ли функция «в контексте объекта», - это обсудить, как вызывается функция, и определить, «прикреплен» ли объект к функции.

Давайте поговорим о том, что происходит, когда мы выполняем bobRossObj.onFriendClick(). «Возьмите мне объект bobRossObj, найдите атрибут onFriendClick и вызовите функцию, назначенную этому атрибуту».

Теперь давайте поговорим о том, что происходит, когда мы выполняем getLastPost(). «Возьмите мне функцию с именем getLastPost и вызовите ее». Обратите внимание, как не было упоминания об объекте?

Хорошо, вот хитрый вопрос, чтобы проверить свои знания. Допустим, есть функция functionCaller, в которой все, что она делает, это вызывает функции:

functionCaller(fn) {
  fn()
}

Что, если бы мы сделали это: functionCaller(bobRossObj.onFriendClick)? Вы бы сказали, что onFriendClick был вызван «в контексте объекта»? Будет ли определяться this.username?

Давайте поговорим об этом: «Возьмите объект bobRossObj и найдите атрибут onFriendClick. Возьмите его значение (которое оказывается функцией), передайте его в functionCaller и назовите fn. Теперь выполните функцию с именем fn ». Обратите внимание, что функция «отсоединяется» от bobRossObj перед вызовом и поэтому не вызывается «в контексте объекта bobRossObj», что означает, что this.username будет неопределенным.

Стрелочные функции спешат на помощь:

function initializeFriend(data) {
  return {
    lastTenPosts: data.lastTenPosts,
    birthday: data.birthday,
    username: data.username,    
    fullName: `${data.firstName} ${data.lastName}`,
    getThreeRandomPosts: function() {
      // get three random posts from this.lastTenPosts
    },
    getDaysUntilBirthday: function() {
      // use this.birthday to get the num days until birthday
    },
    greeting: function() {
      const getLastPost = () => {
        return this.lastTenPosts[0]
      }
      const lastPost = getLastPost()           
      return `Hello, this is ${this.fullName}'s data!
             ${this.fullName}'s last post was ${lastPost}.`
    },
    onFriendClick: function() {
      window.open(`https://facebook.com/${this.username}`)
    }
  };
}

Наше правило сверху:

Независимо от того, что this относится к месту объявления стрелочной функции, this относится к тому же самому объекту внутри этой стрелочной функции.

Стрелочная функция объявлена ​​внутри greeting. Мы знаем, что когда мы используем this в greeting, это относится к самому объекту. Следовательно, this внутри стрелочной функции относится к самому объекту, что нам и нужно.

Заключение

this - иногда сбивающий с толку, но полезный инструмент для разработки приложений JavaScript. Это определенно не все, что нужно для this. Некоторые темы, которые не были затронуты:

  • call и apply
  • как this меняется, когда задействован new
  • как this меняется с ES6class

Я призываю вас задать себе вопросы о том, что, по вашему мнению, this должно быть в определенных ситуациях, а затем проверить себя, запустив этот код в браузере. Если вы хотите узнать больше о this, ознакомьтесь с Вы не знаете JS: это и прототипы объектов.

А если вы хотите проверить себя, посмотрите YDKJS Exercises: this & Object Prototypes.