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 и многие другие.

Он ведет уединенный образ жизни с самой красивой женщиной в мире.