Как работает this, bind, call и apply в JavaScript

Ключевое слово this – очень важное понятие в JavaScript, но в работе с ним особенно легко запутаться как новичкам, так и опытным разработчикам, много работавшим с другими языками программирования. В JavaScript this – это ссылка на объект. Объект, на который ссылается это ключевое слово, может неявно варьироваться в зависимости от того, является ли он глобальным. Также он может явно варьироваться в зависимости от использования методов-прототипов Function: bind, call и apply.

Это немного сложная тема, но она пригодится вам в разработке ваших первых программ на JavaScript. Если вы пытаетесь получить доступ к элементу или событию в объектной модели документа (DOM), создаете классы для записи в стиле объектно-ориентированного программирования или используете свойства и методы обычных объектов, вы столкнетесь с ключевым словом this.

В этом мануале вы узнаете, как по контексту определить, на что неявно ссылается this, и как использовать методы bind, call и apply для явного определения значения this.

Неявный контекст

Существует четыре основных контекста, в которых можно неявно определить значение ключевого слова this:

  • глобальный контекст
  • как метод внутри объекта
  • как конструктор в функции или классе
  • как обработчик событий DOM

Глобальный контекст

В глобальном контексте this ссылается на глобальный объект. Когда вы работаете в браузере, глобальный контекст – это окно. Когда вы работаете в Node.js, глобальный контекст – это global.

Примечание: Если вы еще не знакомы с понятием области видимости в JavaScript, ознакомьтесь с нашим мануалом Переменные, области и поднятие переменных в JavaScript.

В качестве примеров мы будем использовать код в консоли браузера Developer Tools.

Читайте также: Использование консоли разработчика JavaScript

Если вы зарегистрируете значение this без какого-либо другого кода, вы увидите, к какому объекту относится this.

console.log(this)
Window {postMessage: ƒ, blur: ƒ, focus: ƒ, close: ƒ, parent: Window, …}

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

В статье Переменные, области и поднятие переменных в JavaScript мы говорили о том, что функции имеют собственный контекст для переменных. Сейчас можно подумать, что this будет следовать тем же правилам внутри функции, но это не так. Функция верхнего уровня сохранит ссылку this на глобальный объект.

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

function printThis() {
console.log(this)
}
printThis()
Window {postMessage: ƒ, blur: ƒ, focus: ƒ, close: ƒ, parent: Window, …}

Даже внутри функции this  все равно относится к window, или к глобальному объекту.

Однако при использовании строгого режима контекст this внутри функции в глобальном контексте будет undefined.

'use strict'
function printThis() {
console.log(this)
}
printThis()
undefined

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

За дополнительной информацией о строгом режиме и о том, какие изменения он вносит в отношении ошибок и безопасности, обращайтесь к документации на MDN.

Метод объекта

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

Читайте также: Объекты в JavaScript

const america = {
name: 'The United States of America',
yearFounded: 1776,
describe() {
console.log(`${this.name} was founded in ${this.yearFounded}.`)
},
}
america.describe()
"The United States of America was founded in 1776."

В этом примере this – america.

Во вложенном объекте this ссылается на текущую область метода. В следующем примере this.symbol в объекте details ссылается на details.symbol.

const america = {
name: 'The United States of America',
yearFounded: 1776,
details: {
symbol: 'eagle',
currency: 'USD',
printDetails() {
console.log(`The symbol is the ${this.symbol} and the currency is ${this.currency}.`)
},
},
}
america.details.printDetails()
"The symbol is the eagle and the currency is USD."

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

Конструктор функций

Когда вы используете ключевое слово new, оно создает экземпляр функции или класса конструктора. Конструкторы функций были стандартным способом инициализации пользовательского объекта до того, как в 2015 вместе с обновлением ECMAScript для JavaScript появился синтаксис класса.

Читайте также: Работа с классами в JavaScript

function Country(name, yearFounded) {
this.name = name
this.yearFounded = yearFounded
this.describe = function() {
console.log(`${this.name} was founded in ${this.yearFounded}.`)
}
}
const america = new Country('The United States of America', 1776)
america.describe()
"The United States of America was founded in 1776."

В этом контексте this ссылается на экземпляр Country, который содержится в константе america.

Конструктор класса

Конструктор в классе действует так же, как в функции.

class Country {
constructor(name, yearFounded) {
this.name = name
this.yearFounded = yearFounded
}
describe() {
console.log(`${this.name} was founded in ${this.yearFounded}.`)
}
}
const america = new Country('The United States of America', 1776)
america.describe()

Ключевое слово this в методе describe относится к экземпляру Country, которым является america.

"The United States of America was founded in 1776."

Обработчик событий DOM

В браузере есть специальный контекст this для обработчиков событий. В обработчике событий, вызываемом addEventListener ключевое слово this будет ссылаться на event.currentTarget. Чаще всего по мере необходимости разработчики просто используют event.target или event.currentTarget для доступа к элементам в DOM, но так как ссылка this изменяется в этом контексте, это важно знать.

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

const button = document.createElement('button')
button.textContent = 'Click me'
document.body.append(button)
button.addEventListener('click', function(event) {
console.log(this)
})
<button>Click me</button>

Как только вы вставите этот код в свой браузер, на странице появится кнопка с надписью «Click me». Если вы нажмете кнопку, вы увидите <button>Click me</button> в консоли, так как нажатие кнопки регистрирует элемент, который является самой кнопкой. Как видите, this ссылается на целевой элемент, который является элементом, к которому мы добавили прослушиватель событий.

Явный контекст

Во всех предыдущих примерах значение this  определялось его контекстом –глобальным, в объекте, в функции или классе или в обработчике событий DOM. Но используя call, apply или bind, вы можете явно определить, на что ссылается this.

Трудно точно определить, когда нужно использовать call, apply или bind, так как это зависит от контекста вашей программы. Метод bind может быть особенно полезен, если вы хотите использовать события для доступа к свойствам одного класса в другом классе. Например, если вы хотите написать простую игру, вы можете разделить пользовательский интерфейс и ввод-вывод на один класс, а игровую логику и состояние – на другой. Так как игровая логика должна иметь доступ к вводу, например нажатию клавиши и кликам, вам нужно связать (bind) события, чтобы получить доступ к значению this класса игровой логики.

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

Методы call и apply

call и apply очень похожи – они вызывают функцию с указанным контекстом this и дополнительными аргументами. Единственная разница между call и apply заключается в том, что call требует, чтобы аргументы передавались по одному, а apply принимает их в виде массива.

В этом примере мы создадим объект и функцию, которая ссылается на this, но не имеет контекста this.

const book = {
title: 'Brave New World',
author: 'Aldous Huxley',
}
function summary() {
console.log(`${this.title} was written by ${this.author}.`)
}
summary()
"undefined was written by undefined"

Поскольку summary и book не связаны, сам по себе вызов summary будет выводить только неопределенное значение undefined, так как он ищет эти свойства в глобальном объекте.

Примечание: Попытка сделать это в строгом режиме приведет к Uncaught TypeError: Cannot read property ‘title’ of undefined, так как само ключевое слово this будет undefined.

Тем не менее, вы можете использовать call и apply для вызова контекста this для book в функции.

summary.call(book)
// or:
summary.apply(book)
"Brave New World was written by Aldous Huxley."

Теперь между book и summary существует связь. Давайте точно узнаем, на что ссылается this.

function printThis() {
console.log(this)
}
printThis.call(book)
// or:
whatIsThis.apply(book)
{title: "Brave New World", author: "Aldous Huxley"}

В этом случае this фактически становится объектом, переданным в качестве аргумента.

Как мы уже говорили, call и apply почти одинаковы, но есть одно небольшое отличие. Помимо возможности передавать контекст this в качестве первого аргумента, вы также можете передавать apply дополнительные аргументы.

function longerSummary(genre, year) {
console.log(
`${this.title} was written by ${this.author}. It is a ${genre} novel written in ${year}.`
)
}

При использовании call каждое дополнительное значение, которое вы хотите передать, отправляется в качестве дополнительного аргумента.

longerSummary.call(book, 'dystopian', 1932)
"Brave New World was written by Aldous Huxley. It is a dystopian novel written in 1932."

Если вы попытаетесь отправить те же аргументы с помощью apply, произойдет вот что:

longerSummary.apply(book, 'dystopian', 1932)
Uncaught TypeError: CreateListFromArrayLike called on non-object at <anonymous>:1:15

При использовании apply вы должны передать все аргументы в массиве.

longerSummary.apply(book, ['dystopian', 1932])
"Brave New World was written by Aldous Huxley. It is a dystopian novel written in 1932."

Разница между передачей аргументов по отдельности или в массиве невелика, но об этом важно знать. Использовать метод apply иногда может быть проще и удобнее, так как он не требует изменения вызова функции, если некоторые детали параметров изменились.

Метод bind

И call, и apply являются одноразовыми методами — если вы вызываете метод с контекстом this, он примет его, но исходная функция останется неизменной.

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

const braveNewWorldSummary = summary.bind(book)
braveNewWorldSummary()
"Brave New World was written by Aldous Huxley"

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

const braveNewWorldSummary = summary.bind(book)
braveNewWorldSummary() // Brave New World was written by Aldous Huxley.
const book2 = {
title: '1984',
author: 'George Orwell',
}
braveNewWorldSummary.bind(book2)
braveNewWorldSummary() // Brave New World was written by Aldous Huxley.

Хотя этот пример пытается связать braveNewWorldSummary еще раз, он сохраняет оригинальный контекст this.

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

Стрелочные функции не имеют привязки this. Вместо этого они переходят на следующий уровень исполнения.

Читайте также: Определение функций в JavaScript

const whoAmI = {
name: 'Leslie Knope',
regularFunction: function() {
console.log(this.name)
},
arrowFunction: () => {
console.log(this.name)
},
}
whoAmI.regularFunction() // "Leslie Knope"
whoAmI.arrowFunction() // undefined

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

В этом примере мы создадим и добавим кнопку в DOM, как  мы делали раньше, но у класса будет прослушиватель событий, который будет изменять текстовое значение кнопки при нажатии.

const button = document.createElement('button')
button.textContent = 'Click me'
document.body.append(button)
class Display {
constructor() {
this.buttonText = 'New text'
button.addEventListener('click', event => {
event.target.textContent = this.buttonText
})
}
}
new Display()

Если вы нажмете кнопку, текстовое содержимое изменится на значение buttonText. Если бы вы не использовали здесь стрелочную функцию, this было бы равно event.currentTarget, и вы не смогли бы использовать его для доступа к значению в классе без явного его связывания. Эта тактика часто используется в методах классов в таких средах, как React.

Заключение

В этой статье вы узнали о ключевом слове this в JavaScript и о множестве различных значений, которые оно могло бы иметь в неявном контексте и при явном связывании посредством методов bind, call и apply. Также вы знаете, как отсутствие привязки this  в стрелочных функциях можно использовать для ссылки на другой контекст. Обладая этими знаниями, вы сможете определить значение this в своих программах.

Tags: ,