Ключевые слова this, call, apply и bind применяются для указания тех или иных элементов в JavaScript-коде. Новичкам в работе с ними легко запутаться, из-за чего скрипт исполняется некорректно или приводит к появлению ошибок в будущем. Хотя ошибки с определениями допускают и опытные разработчики. Метод this в JavaScript это ссылка на какой-то объект в коде. Ее диапазон может быть ограничен как всем скриптом, так и какой-то его частью, например, функцией, массивом и так далее. Методы call, apply и bind отвечают за вызов и обработку выбранного элемента в коде.
Для удобства рассмотрим, основные особенности всех четырех методов, а также в каких ситуациях они используются.
Неявный контекст в JavaScript
Контекстом выполнения в JavaScript называют концепцию, описывающую окружение выполнения кода. Скрипт не может выполняться без какого-либо контекста, так как он обязательно с чем-то связан: другими функциями, HTML/CSS-каркасом, пользовательскими данными. В JS выделяются три типа контекста выполнения:
- Глобальный контекст. Применим к коду, находящемуся вне какой-либо функции. Характеризуется наличием глобального объекта. По умолчанию это window, то есть окно браузера. В программе может быть использован только один глобальный контекст.
- Контекст выполнения функции. Для каждой новой функции в коде создается свой контекст. Если глобальный контекст может быть только один на весь скрипт, то количество контекстов выполнения функции не ограничено.
- Контекст выполнения eval. Это уже контекст внутри функции eval. Она применяется очень редко, поэтому подробно рассматривать ее не будем.
При этом у ключевого слова this есть четыре контекста, в которых его можно неявно задать:
- глобальный контекст;
- метод внутри объекта;
- конструктор в функции или классе;
- обработчик DOM-событий.
Дальше подробно рассмотрим применение this в этих контекстах.
Глобальный контекст
В нем this ссылается на глобальный объект – window для браузера или global для Node.js. Для примера будем рассматривать работу в браузере без Node.js. Попробуем вывести this в консоле без обращения. В итоге вы получите описание глобального объекта windows, так как this по умолчанию будет ссылаться на него.
Результат выполнения this в глобальном контексте без конкретного запроса в консоли браузера
Теперь давайте попробуем сделать то же самое, но уже внутри какой-то функции. По-идеи, выполнение команды в таком случае должно происходить не в глобальном контексте, соответственно this по умолчанию должен ссылаться на что-то другое или вообще выдавать ошибку. Запишем пустую функцию и выполним ее в консоле:
function printThis() {
console.log(this)
}
printThis()
Window {postMessage: ƒ, blur: ƒ, focus: ƒ, close: ƒ, parent: Window, …}
Как видите, даже внутри функции this будет по умолчанию обращаться к глобальному объекту. Однако, если выполнить эту же функцию в режиме ‘use script’, то значение this будет undefined. Запускать отладку JS-кода как раз предпочтительнее в “строгом режиме”, потому что он позволяет избежать ошибки с неверными определениями в коде.
Метод внутри объекта
Не стоит путать с функцией, хотя они очень похожи по своему предназначению. Метод содержит в себе основные определения функции, которые задаются разработчиком или пользователем. Рассмотрим пример метода ниже:
const moscow = {
name: 'Moscow',
yearFounded: 1147,
describe() {
console.log(`${this.name} was founded in ${this.yearFounded}.`)
},
}
moscow.describe()
"Moscow was founded in 1147."
В этом примере this это метод moscow и все, что в него входит. Также к методу можно привязывать любые значения, находящиеся внутри метода. Записывается это так: this.название_объекта. Ссылаться this всегда будет на объект, функцию или другой элемент, находящийся слева от точки. Если это значение не заполнить, то в консоли будет выведен результат undefined.
Конструктор в функции или классе
Для создания экземпляра функции или класса принято использовать ключевое слово new. С 2015 года используются все реже, так как в JavaScript появилось обновление синтаксиса ECMAScript. Вот пример конструктора функции с использованием this:
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 действует и в конструкторе классов.
DOM обработчик событий
В этом случае this ссылается на конкретное событие, например, клик по кнопке. Соответственно для него действуют немного другие правила, чем при ссылке на элементы, объекты или функции. Если обработчик событий вызывается через addEventListener, то this будет по умолчанию ссылаться на event.currentTarget. Стоит понимать, что event.target и event.currentTarget различаются между собой и поведение скрипта меняется в зависимости от их использования.
Рассмотрим, как это работает на примере с кнопкой, которая будет просто выводить информацию в консоли браузера:
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>
При нажатии на кнопку происходит регистрация элемента, который является кнопкой. Его код и выводится в консоли браузера.
Call, apply и bind
С this разобрались – это ссылка какой-то участок глобально внутри скрипта или локально внутри определенного участка кода. Однако, если требуется ссылаться одновременно на несколько компонентов, то могут возникнуть проблемы. Например, придется писать дополнительный код с this, чтобы отдельно обращаться к каждому элементу. Это приведет как увеличению самого кода, так и количества потенциальных ошибок в нем. Специально для этих случаев предусмотрены call, apply и bind. С их помощью можно легко определить, на что должен ссылаться this.
Основная сложность с их использованием – не всегда понятно, когда применять то или иное ключевое слово. Многое зависит от контекста вашей программы и предполагаемых возможностей ее масштабирования в будущем. Дело в том, что код может исполняться корректно в моменте, но при его расширении используемые варианты вызова начинают давать сбой.
Рассмотрим гипотетический пример. Вам нужно разработать браузерную мини-игру. Пользовательский интерфейс и систему ввода-вывода было решено поместить в один класс, а игровую логику и данные о состояниях объектов в другой. Чтобы играть нормально работала, нужно придумать как правильно связать оба класса между собой. Стоит учитывать, что внутри классов может быть много взаимосвязанных между собой объектов. Использовать this для корректной связи двух классов не получится, так как это решение приведет к появлению множественных ошибок.
Чтобы правильно определить наиболее подходящий метод, нужно узнать к каким объектам относятся ключевые this в коде. Подробно про это было написано выше. Для рассматриваемого примера лучше всего подойдет метод bind. Он лучше работает с событиями. Так как вам нужно, чтобы взаимодействие пользователя с интерфейсом было связано с игровой логикой через события, то bind является наиболее оптимальным вариантом.
Подробно использование всех трех методов рассмотрим ниже.
Использование методов call и apply
Рассматривать их будем вместе, так как они схожи между собой. Их задача – вызов функции с указанным контекстом this и дополнительными аргументами. Разница между двумя методами только в виде приема аргументов: call принимает аргументы исключительно по одному, а apply может принимать несколько и формировать из них массив.
Для примера создадим функцию без контекста this, но ссылающуюся на него:
const book = {
title: 'Brave New World',
author: 'Aldous Huxley',
}
function summary() {
console.log(`${this.title} was written by ${this.author}.`)
}
summary()
Вывод результата выполнения функции в консоли браузера
При попытке ее выполнения в текущем виде через консоль браузера вы получите undefined вместо нужного текста. Дело в том, что блоки summary и book не связаны между собой. Метод this изначально ищет подходящие элементы внутри своей функции (summary), а потом глобально. При этом он не может “заглянуть” в другие блоки и подтянуть данные из них.
Ситуацию исправит использование метода call или apply (для этого примера выбранный метод не имеет значения):
summary.call(book)
// or:
summary.apply(book)
Вывод корректного результата в консоли браузера
На последок посмотрим на различия методов call и apply. Например, вам нужно передать несколько аргументов для функции. Оба метода это позволяют сделать, но подход у них разный:
Для call запись выглядит так: longerSummary.apply(book, 'dystopian', 1932)
Для apply вот так: longerSummary.apply(book, ['dystopian', 1932])
Если нужно передать много аргументов, то метод apply все же будет предпочтительнее.
Метод bind
Call и apply – это одноразовые методы, то есть они принимают данные из this, но при этом сама функция остается неизменной. Для динамических веб-приложений, например, той же браузерной игры, такой подход не очень удобен: пользователь постоянно вводит новые данные и код должен быстро под них подстраиваться. В таких ситуациях используется метод bind. Он умеет создавать новые функции с явно привязанным this.
При каждом вызове константы braveNewWorldSummary будет возвращено исходное значение 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 в качестве аргумента стоит book2, данные он берет из того блока, что использовался в коде изначально.
Стрелочные функции
Рассмотренные методы call, apply и bind к ним неприменимы, так как такие функции не имеют привязки к this. Зато здесь идет сразу переход к следующему уровню исполнения. Примечательно, что использовать метод this в таких функция вы все еще можете, но работать он будет немного иначе.
Вот пример двух функций с результатами их выполнения в консоли:
const whoAmI = {
name: 'Leslie Knope',
regularFunction: function() {
console.log(this.name)
},
arrowFunction: () => {
console.log(this.name)
},
}
whoAmI.regularFunction() // "Leslie Knope"
whoAmI.arrowFunction() // undefined
Как видите, при стандартном использовании this стрелочная функция вернула значение undefind. Такие функции обычно используются, когда нужно, чтобы this ссылалось на внешний код. Например, у вас есть скрипт события и функция, описывающая действие. Однако триггер запуска находится за пределами этой функции в теле основного скрипта. В таком случае саму функцию лучше сделать стрелочной.
Заключение
Новичку сложно разобраться в разнице между this, call, apply и bind. Все 4 метода ссылаются на какой-то компонент кода, но делают это по-разному. Метод this отвечает за простые ссылки, call и apply уже позволяют передавать аргументы, а bind фиксировать изначальное значение. Также нужно не забывать про стрелочные функции. В ней отсутствуют привязки call, apply и bind, а this работает только на внешний код. Научившись различать особенности всех четырех методов, вы сможете писать сложные и, главное, корректно работающие скрипты на JavaScript.