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

Комментарии (7)
Dukat
13.06.2019 13:38Как говорится,
JavaScript makes me want to flip the table and say «Fuck this shit», but I can never be sure what «this» refers to.
Zenitchik
13.06.2019 14:37+2Шо, опять?.. Чёрт бы побрал
копрокорпоблоги с их перепечатыванием азбучных истин.
Aingis
13.06.2019 15:36Можно бесконечно смотреть на три вещи: как горит огонь, как течет вода и как ruvds публикует очередную статью про
this
в Javascript.
Причём перевод этой статьи уже был на Хабре: «Какой здесь this? Внутренняя работа объектов JavaScript».
rboots
13.06.2019 16:51Лет 10 назад каждый JS-программист свою реализацию наследования писал, а сейчас не знают чему this в каком случае равен, позор. Не программируйте на JavaScript если не освоили синтаксис и this для вас «закулисье».
rcanedu
14.06.2019 00:29Контекст иногда сложно определить однозначно. Особенно имея возможность его менять динамически. Об этом, кстати, постоянно пишет Крокфорд. В том числе в последней его книге.
Vahman
14.06.2019 20:44Да то не программисты, то блоги с нормой выработки. Насчёт сложностей this — всегда есть новички, которым интересно, но об этом уже много написано
alexesDev
Так лучше не писать, толку не много, но работает =)