JavaScript - это многопарадигмальный язык, поддерживающий объектно-ориентированное программирование и динамическое связывание. Динамическое связывание - это мощная концепция, которая позволяет изменять структуру вашего кода JavaScript во время выполнения, но эта дополнительная мощность и гибкость достигается за счет некоторой путаницы, и большая часть этой путаницы связана с тем, как это ведет себя в JavaScript.
Динамическое связывание
Динамическое связывание - это процесс определения метода для вызова во время выполнения, а не во время компиляции. JavaScript выполняет это с помощью this
и цепочки прототипов. В частности, значение this
внутри метода определяется во время выполнения, и правила меняются в зависимости от того, как этот метод был определен.
Давай сыграем в игру. Я называю эту игру «Что такое this
?»
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 = [ 1, obj.getThis(), 2, obj.getThis.call(a), 3, obj.getThis2(), 4, obj.getThis2.call(a), 5, obj.getThis3(), 6, obj.getThis3.call(a), 7, obj.getThis4(), 8, obj.getThis4.call(a), ];
Прежде чем продолжить, запишите свои ответы. После этого console.log()
свои ответы, чтобы проверить их. Вы правильно угадали?
Давайте начнем с первого случая и спустимся вниз. В контексте модуля ES6 obj.getThis()
возвращает undefined
. В теге скрипта это window
. В Node REPL это global
.
Почему? Стрелочные функции никогда не могут иметь собственных this
привязок. Вместо этого они всегда отдают предпочтение лексической области видимости. В корневой области видимости модуля ES6 лексическая область видимости будет иметь неопределенное значение this
. obj.getThis.call(a)
также undefined
по той же причине. Для стрелочных функций this
нельзя переназначить даже с .call()
или .bind()
. Он всегда будет делегировать лексическому this
.
obj.getThis2()
получает свою привязку через обычный процесс вызова метода. Если предыдущая this
привязка отсутствует, а функция может иметь this
привязку (т.е. это не стрелочная функция), this
привязывается к объекту, для которого вызывается метод, с синтаксисом доступа к свойству .
или [squareBracket]
.
obj.getThis2.call(a)
немного сложнее. Метод call()
вызывает функцию с заданным значением this
и необязательными аргументами. Другими словами, он получает свою this
привязку из параметра .call()
, поэтому obj.getThis2.call(a)
возвращает объект a
.
С помощью obj.getThis3 = obj.getThis.bind(obj);
мы пытаемся привязать стрелочную функцию, которая, как мы уже определили, не будет работать, поэтому мы возвращаемся к undefined
(или window
, или global
) как для obj.getThis3()
, так и для obj.getThis3.call(a)
.
Вы можете связать обычные методы, поэтому obj.getThis4()
вернет obj
, как и ожидалось, а поскольку он уже был связан с obj.getThis4 = obj.getThis2.bind(obj);
, obj.getThis4.call(a)
учитывает первую привязку и возвращает obj
вместо a
.
Кривой шар
Та же проблема, но на этот раз с class
с использованием синтаксиса общедоступных полей (Этап 3 на момент написания, по умолчанию доступен в 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 answers = [ 1, obj2.getThis(), 2, obj2.getThis.call(a), 3, obj2.getThis2(), 4, obj2.getThis2.call(a), 5, obj2.getThis3(), 6, obj2.getThis3.call(a), 7, obj2.getThis4(), 8, obj2.getThis4.call(a), ];
Запишите свои ответы, прежде чем продолжить.
Готовый?
За исключением obj2.getThis2.call(a)
, все они возвращают экземпляр объекта. Исключение возвращает объект a
. Функция стрелки по-прежнему делегирует лексический this
. Разница в том, что лексический this
отличается для свойств класса. Под капотом это присвоение свойств класса компилируется примерно так:
class Obj { constructor() { this.getThis = () => this; } ...
Другими словами, стрелочная функция определяется внутри контекста функции-конструктора. Поскольку это класс, единственный способ создать экземпляр - использовать ключевое слово new
(пропуск new
вызовет ошибку ).
Одна из наиболее важных вещей, которые выполняет ключевое слово new
, - это создание нового экземпляра объекта и привязка this
к нему в конструкторе. Это поведение в сочетании с другими поведениями, которые мы уже упоминали выше, должно объяснить остальное.
Заключение
Как ты это сделал? Вы все правильно поняли? Хорошее понимание того, как this
ведет себя в JavaScript, сэкономит вам много времени на отладку сложных проблем. Если вы ответили неверно, это пригодится вам для практики. Поиграйте с примерами, затем вернитесь и проверьте себя снова, пока вы не сможете успешно пройти тест и объяснить кому-нибудь еще, почему методы возвращают то, что возвращают.
Если это оказалось сложнее, чем вы ожидали, вы не одиноки. Я тестировал довольно много разработчиков по этой теме, и я думаю, что пока только один разработчик прошел успешно.
То, что начиналось как поиск динамических методов, которые можно было перенаправить с помощью .call()
, .bind()
или .apply()
, стало значительно сложнее с добавлением class
и поведения стрелочных функций. Может быть полезно немного разделить. Помните, что стрелочные функции всегда делегируют this
лексической области видимости, и что class
this
фактически лексически привязана к функциям конструктора под капотом. Если вы когда-нибудь сомневаетесь в том, что такое this
, не забудьте использовать отладчик, чтобы убедиться, что объект такой, как вы думаете.
Помните также, что в JavaScript вы можете многое сделать, даже не используя this
. По моему опыту, почти все можно переопределить в терминах чистых функций, которые принимают все аргументы, к которым они применяются, как явные параметры (вы можете думать о this
как о неявном параметре с изменяемым состоянием). Логика, заключенная в чистые функции, детерминирована, что делает ее более тестируемой и не имеет побочных эффектов, а это означает, что, в отличие от манипуляции с this
, вы вряд ли что-нибудь сломаете. Каждый раз, когда вы мутируете this
, вы рискуете, что что-то еще, зависящее от значения this
, сломается.
Тем не менее, this
иногда бывает полезным. Например, чтобы разделить методы между большим количеством объектов. Даже в функциональном программировании this
может быть полезен для доступа к другим методам объекта для реализации алгебраических выводов для построения новых алгебр поверх существующих. Например, общий .flatMap()
может быть получен путем доступа к this.map()
и this.flatten()
:
flatMap (f) { return this.map(f).flatten(); }
Эрик Эллиотт - эксперт по распределенным системам и автор книг Создание программного обеспечения и Программирование приложений JavaScript. Как соучредитель DevAnywhere.io, он обучает разработчиков навыкам, необходимым для удаленной работы, и обеспечивает баланс между работой и личной жизнью. Он создает и консультирует команды разработчиков для криптопроектов, а также участвовал в разработке программного обеспечения для Adobe Systems, Zumba Fitness, The Wall Street Journal, ESPN, BBC и ведущие музыканты, в том числе Ашер, Фрэнк Оушен, Metallica и многие другие.
Он ведет уединенный образ жизни с самой красивой женщиной в мире.