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

в 9:30, , рубрики: javascript, Блог компании RUVDS.com, разработка, Разработка веб-сайтов

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

Что записано в this? Закулисье JavaScript-объектов - 1

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

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

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js