Фотография: "Любопытный" Liliana Saeb (CC BY 2.0)


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


Динамическое связывание


При динамическом связывании определение метода для вызова происходит во время выполнения, а не во время компиляции. JavaScript выполняет это с помощью this и цепочки прототипов. В частности, внутри метода 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 = [
  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 и проверьте свои ответы с помощью console.log(). Угадали?


Начнем с первого случая и продолжим по порядку. obj.getThis() возвращает undefined, но почему? У стрелочных функций никогда не бывает своего this. Вместо этого они всегда берут this из лексической области видимости (прим. лексический this). Для корня модуля ES6 лексическая область будет иметь неопределенное (undefined) значение this. obj.getThis.call(a) также не определен по той же причине. Для стрелочной функций this не может быть переопределён, даже с .call() или .bind(). this всегда будет браться из лексической области.


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


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


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


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


Крученый мяч


Решим ту же задачу, но на этот раз для описания объекта используем class с публичными полями (нововведения Stage 3 на момент написания этой статьи доступны в Chrome по умолчанию и с @babel/plugin-offer-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)
];

Подумайте над ответами, прежде чем продолжить.


Готовы?


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


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

Другими словами, стрелочная функция определяется внутри контекста конструктора. Так как это класс, то единственный способ создать экземпляр – это использовать ключевое слово new (опущение new приведет к ошибке). Одна из самых важных вещей, которые делает ключевое слово new, – это создание нового экземпляра объекта и привязка this к нему в конструкторе. Это поведение в сочетании с другими поведениями, которые мы уже упоминали выше, должно объяснить остальное.


Заключение


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


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


То, что начиналось как поиск динамических методов, которые вы могли перенаправить с помощью .call(), .bind() или .apply(), стало значительно сложнее с добавлением методов класса и стрелочных функций. Возможно, вам стоит ещё раз заострить на этом внимание. Помните, что стрелочные функции всегда берут this из лексической области видимости, и class this на самом деле лексически ограничен конструктором класса под капотом. Если вы когда-либо засомневаетесь в this, то помните, что можно использовать отладчик (debugger), чтобы проверить его значение.


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


Тем не менее, иногда this полезен. Например, для обмена методами между большим количеством объектов. Даже в функциональном программировании this можно использовать для доступа к другим методам объекта, чтобы реализовать алгебраические преобразования, необходимые для построения новых алгебр поверх существующих. Так, универсальный .flatMap() может быть получен с помощью this.map() и this.constructor.of().




Спасибо за помощь с переводом wksmirnowa и VIBaH_dev

Комментарии (3)


  1. Vahman
    17.05.2019 23:36
    +1

    поэтому, в отличие от моментов изменения this

    Могу показаться занудой, но this не изменяется. Он всегда указывает туда куда и должен, знает об этом разработчик или нет. В JS this это всегда контекст исполнения.


    1. sharpfellow Автор
      18.05.2019 00:40

      Спасибо, что кажетесь занудой, поправил, чтобы было меньше вопросов.
      this скорее контекст вызова, так как в js контекст исполнения (на англ. execution context) играет информационную роль. Он определяется с запуском функции и содержит информацию о локальных переменных, о месте в коде, где был вызов функции; и данный контекст помещается в стек выполнения, согласно которому движок выполняет функции


  1. taliban
    18.05.2019 15:05

    Все, я туплю ) удаляю камент