JavaScript — это мультипарадигменный язык, поддерживающий объектно-ориентированное программирование и динамическую привязку методов — мощную концепцию, которая позволяет структуре JavaScript-кода меняться во время выполнения программы. Это даёт разработчикам серьёзные возможности, это делает язык гибким, но за всё надо платить. В данном случае платить приходится понятностью кода. Серьёзный вклад в эту цену вносит ключевое слово this
, вокруг особенностей поведения которого собрано много такого, что способно запутать программиста.
Динамическая привязка методов
Динамическая привязка позволяет задать, во время выполнения программы, а не во время её компиляции, метод, который нужно вызвать при выполнении некоей команды. В JavaScript этот механизм реализуется с помощью ключевого слова this
и цепочки прототипов. В частности, конкретное значение this
внутри метода определяется во время выполнения программы, при этом правила определения этого значения меняются в зависимости от того, как был объявлен этот метод.
Давайте поиграем в одну игру. Я называю её «Что записано в this?». Перед вами её первый вариант — код ES6-модуля:
const a = {
a: 'a'
};
const obj = {
getThis: () => this,
getThis2 () {
return this;
}
};
obj.getThis3 = obj.getThis.bind(obj);
obj.getThis4 = obj.getThis2.bind(obj);
const answers = [
obj.getThis(),
obj.getThis.call(a),
obj.getThis2(),
obj.getThis2.call(a),
obj.getThis3(),
obj.getThis3.call(a),
obj.getThis4(),
obj.getThis4.call(a)
];
Прежде чем читать дальше — подумайте о том, что попадёт в массив answers
и запишите ответы. После того как вы это сделаете — проверьте себя, выведя массив answers
с помощью console.log()
. Удалось ли вам правильно «расшифровать» значение this
в каждом из случаев?
Разберём эту задачу, начав с первого примера. Конструкция obj.getThis()
возвращает undefined
. Почему? К стрелочной функции this
привязать нельзя. Такие функции используют this
из окружающей их лексической области видимости. Метод вызывается в ES6-модуле, в его лексической области видимости this
будет иметь значение undefined
. По той же причине undefined
возвратить и вызов obj.getThis.call(a)
. Значение this
при работе со стрелочными функциями не может быть переназначено даже с помощью .call()
или .bind()
. Это значение всегда будет соответствовать this
из лексической области видимости, в которой находятся такие функции.
Команда obj.getThis2()
демонстрирует порядок работы с this
при использовании обычных методов объекта. Если this
к подобному методу не привязывали, и при условии того, что этот метод не является стрелочной функцией, то есть — он поддерживает привязку this
, ключевое слово this
оказывается привязанным к тому объекту, для которого метод вызывается с использованием синтаксиса доступа к свойствам объекта через точку или с помощью квадратных скобок.
С конструкцией obj.getThis2.call(a)
разобраться уже немного сложнее. Метод call()
позволяет вызвать функцию с заданным значением this
, которое указывают в виде необязательного аргумента. Другими словами, в данном случае this
берётся из параметра .call()
, в результате вызов obj.getThis2.call(a)
возвращает объект a
.
С помощью команды obj.getThis3 = obj.getThis.bind(obj);
мы пытаемся привязать к this
метод, представляющий собой стрелочную функцию. Как мы уже выяснили, сделать этого нельзя. В результате вызовы obj.getThis3()
и obj.getThis3.call(a)
возвращают undefined
.
К this
можно привязывать методы, представляющие собой обычные функции, поэтому obj.getThis4()
, как и ожидается, возвращает obj
. Вызов obj.getThis4.call(a)
возвращает obj
, а не, как можно было бы ожидать, a
. Дело в том, что мы, прежде чем вызывать эту команду, уже привязали this
командой obj.getThis4 = obj.getThis2.bind(obj);
. Как результат, при выполнении obj.getThis4.call(a)
учитывается состояние метода, в котором он пребывал после выполнения первой привязки.
Использование this в классах
Вот второй вариант нашей игры — та же задача, но теперь уже основанная на классах. Здесь используется синтаксис объявления общедоступных полей классов (в данный момент предложение по этому синтаксису находится на третьем этапе согласования, он по умолчанию доступен в Chrome, пользоваться им можно и с помощью @babel/plugin-proposal-class-properties
).
class Obj {
getThis = () => this
getThis2 () {
return this;
}
}
const obj2 = new Obj();
obj2.getThis3 = obj2.getThis.bind(obj2);
obj2.getThis4 = obj2.getThis2.bind(obj2);
const answers2 = [
obj2.getThis(),
obj2.getThis.call(a),
obj2.getThis2(),
obj2.getThis2.call(a),
obj2.getThis3(),
obj2.getThis3.call(a),
obj2.getThis4(),
obj2.getThis4.call(a)
];
Прежде чем читать дальше — подумайте над кодом и запишите своё видение того, что попадёт в массив answers2
.
Готово?
Здесь все вызовы методов, за исключением obj2.getThis2.call(a)
, вернут ссылку на экземпляр объекта. Этот же вызов вернёт объект a
. Стрелочные функции всё ещё берут this
из лексической области видимости. Разница между этим примером и предыдущим заключается в различии областей видимости, из которых берётся this
.
А именно, тут мы работаем со свойствами классов, что и определяет особенности поведения этого кода.
Дело в том, что в ходе подготовки кода к выполнению запись значений в свойства классов происходит примерно так:
class Obj {
constructor() {
this.getThis = () => this;
}
...
Иначе говоря, получается, что стрелочная функция оказывается объявленной внутри контекста функции-конструктора. Так как мы работаем с классом, единственным способом создания его экземпляра является использование ключевого слова new
(если забыть об этом ключевом слове — будет выдано сообщение об ошибке).
Важнейшие задачи, решаемые ключевым словом new
, заключаются в создании нового экземпляра объекта и в привязке this
к конструктору. Эта особенность, с учётом того, о чём мы уже говорили в предыдущем разделе, должна помочь вам разобраться в происходящем.
Итоги
Справились ли вы с задачами, приведёнными в этом материале? Хорошее понимание того, как в JavaScript ведёт себя ключевое слово this
, сэкономит вам массу времени при отладке, при поиске неочевидных причин непонятных ошибок. Если вы ответили на некоторые из вопросов неправильно, это значит, что вам будет полезно попрактиковаться.
Поэкспериментируйте с кодом примеров, а потом опять испытайте себя, и так — до тех пор, пока у вас не получится ответить на все вопросы правильно. После того как разберётесь в этом сами — найдите кого-нибудь, готового вас выслушать, и расскажите ему о том, почему методы из заданий возвращают именно то, что возвращают.
Если всё это кажется вам более сложным, чем вы ожидали, то знайте, что вы в этом не одиноки. Я проверял на знание особенностей this
довольно много разработчиков, и я так думаю, что только один из них был абсолютно точен во всех своих ответах.
Та подсистема языка, которая, в самом начале, выглядела как динамический поиск методов, на который можно было влиять помощью .call()
, .bind()
или .apply()
, стала выглядеть гораздо сложнее после появления стрелочных функций и классов.
Видимо, тут полезно будет отметить основные особенности классов и стрелочных функций в плане использования this
. Помните о том, что стрелочные функции всегда пользуются this
из их лексической области видимости, а ключевое слово this
в классах, на самом деле, привязано к функции-конструктору класса. А если вы когда-нибудь почувствуете, что не знаете точно, на что указывает this
, воспользуйтесь отладчиком для того чтобы проверить свои предположения на этот счёт.
Кроме того, помните о том, что очень многое в JavaScript можно сделать и не используя this
в коде. Опыт подсказывает мне, что практически любой JS-код можно переписать в виде чистых функций, которые принимают все аргументы, с которыми работают, в виде явным образом заданного списка параметров (this
можно воспринимать как неявным образом заданный параметр с мутабельным состоянием). Логика, заключённая в чистых функциях, детерминирована, что улучшает их тестируемость. Такие функции не имеют побочных эффектов, что означает, что при работе с ними, в отличие от манипуляций с this
, вы вряд ли «сломаете» что-нибудь, находящееся за их пределами. Всегда, когда вы меняете this
, вы сталкиваетесь с потенциальной проблемой, которая заключается в том, что что-то, зависящее от this
, может перестать правильно работать.
Несмотря на вышесказанное надо отметить, что this
— это полезная концепция. Например, её можно применить для того, чтобы организовать совместное использование некоего метода множеством объектов. Даже в функциональном программировании this
может пригодиться для вызова из одного метода объекта других его методов, что позволяет создавать что-то новое на базе существующих конструкций.
Автор: ru_vds