Хабрахабр

[Перевод] Что записано в this? Закулисье JavaScript-объектов

JavaScript — это мультипарадигменный язык, поддерживающий объектно-ориентированное программирование и динамическую привязку методов — мощную концепцию, которая позволяет структуре JavaScript-кода меняться во время выполнения программы. Это даёт разработчикам серьёзные возможности, это делает язык гибким, но за всё надо платить. В данном случае платить приходится понятностью кода. Серьёзный вклад в эту цену вносит ключевое слово this, вокруг особенностей поведения которого собрано много такого, что способно запутать программиста.

Динамическая привязка методов

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

Я называю её «Что записано в this?». Давайте поиграем в одну игру. Перед вами её первый вариант — код ES6-модуля:

const a = { a: 'a'
};
const obj =
};
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 привязать нельзя. Почему? Метод вызывается в ES6-модуле, в его лексической области видимости this будет иметь значение undefined. Такие функции используют this из окружающей их лексической области видимости. Значение this при работе со стрелочными функциями не может быть переназначено даже с помощью .call() или .bind(). По той же причине undefined возвратить и вызов obj.getThis.call(a). Это значение всегда будет соответствовать this из лексической области видимости, в которой находятся такие функции.

Если this к подобному методу не привязывали, и при условии того, что этот метод не является стрелочной функцией, то есть — он поддерживает привязку this, ключевое слово this оказывается привязанным к тому объекту, для которого метод вызывается с использованием синтаксиса доступа к свойствам объекта через точку или с помощью квадратных скобок. Команда obj.getThis2() демонстрирует порядок работы с this при использовании обычных методов объекта.

Метод call() позволяет вызвать функцию с заданным значением this, которое указывают в виде необязательного аргумента. С конструкцией obj.getThis2.call(a) разобраться уже немного сложнее. Другими словами, в данном случае this берётся из параметра .call(), в результате вызов obj.getThis2.call(a) возвращает объект a.

Как мы уже выяснили, сделать этого нельзя. С помощью команды obj.getThis3 = obj.getThis.bind(obj); мы пытаемся привязать к this метод, представляющий собой стрелочную функцию. В результате вызовы obj.getThis3() и obj.getThis3.call(a) возвращают undefined.

Вызов obj.getThis4.call(a) возвращает obj, а не, как можно было бы ожидать, a. К this можно привязывать методы, представляющие собой обычные функции, поэтому obj.getThis4(), как и ожидается, возвращает obj. Как результат, при выполнении obj.getThis4.call(a) учитывается состояние метода, в котором он пребывал после выполнения первой привязки. Дело в том, что мы, прежде чем вызывать эту команду, уже привязали this командой obj.getThis4 = obj.getThis2.bind(obj);.

Использование 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.

Готово?

Этот же вызов вернёт объект a. Здесь все вызовы методов, за исключением obj2.getThis2.call(a), вернут ссылку на экземпляр объекта. Разница между этим примером и предыдущим заключается в различии областей видимости, из которых берётся this. Стрелочные функции всё ещё берут this из лексической области видимости.

А именно, тут мы работаем со свойствами классов, что и определяет особенности поведения этого кода.

Дело в том, что в ходе подготовки кода к выполнению запись значений в свойства классов происходит примерно так:

class Obj { constructor() { this.getThis = () => this; }
...

Иначе говоря, получается, что стрелочная функция оказывается объявленной внутри контекста функции-конструктора. Так как мы работаем с классом, единственным способом создания его экземпляра является использование ключевого слова new (если забыть об этом ключевом слове — будет выдано сообщение об ошибке).

Эта особенность, с учётом того, о чём мы уже говорили в предыдущем разделе, должна помочь вам разобраться в происходящем. Важнейшие задачи, решаемые ключевым словом new, заключаются в создании нового экземпляра объекта и в привязке this к конструктору.

Итоги

Справились ли вы с задачами, приведёнными в этом материале? Хорошее понимание того, как в JavaScript ведёт себя ключевое слово this, сэкономит вам массу времени при отладке, при поиске неочевидных причин непонятных ошибок. Если вы ответили на некоторые из вопросов неправильно, это значит, что вам будет полезно попрактиковаться.

После того как разберётесь в этом сами — найдите кого-нибудь, готового вас выслушать, и расскажите ему о том, почему методы из заданий возвращают именно то, что возвращают. Поэкспериментируйте с кодом примеров, а потом опять испытайте себя, и так — до тех пор, пока у вас не получится ответить на все вопросы правильно.

Я проверял на знание особенностей this довольно много разработчиков, и я так думаю, что только один из них был абсолютно точен во всех своих ответах. Если всё это кажется вам более сложным, чем вы ожидали, то знайте, что вы в этом не одиноки.

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

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

Опыт подсказывает мне, что практически любой JS-код можно переписать в виде чистых функций, которые принимают все аргументы, с которыми работают, в виде явным образом заданного списка параметров (this можно воспринимать как неявным образом заданный параметр с мутабельным состоянием). Кроме того, помните о том, что очень многое в JavaScript можно сделать и не используя this в коде. Такие функции не имеют побочных эффектов, что означает, что при работе с ними, в отличие от манипуляций с this, вы вряд ли «сломаете» что-нибудь, находящееся за их пределами. Логика, заключённая в чистых функциях, детерминирована, что улучшает их тестируемость. Всегда, когда вы меняете this, вы сталкиваетесь с потенциальной проблемой, которая заключается в том, что что-то, зависящее от this, может перестать правильно работать.

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

Показать больше

Похожие публикации

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»