Используя простую терминологию и реальный пример, этот пост объясняет, что такое 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.