Зачем нам это нужно в JavaScript и как узнать его ценность

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

По мере того, как вы создаете все больше и больше проектов с использованием JavaScript, вы обнаружите, что от ключевого слова this никуда не денешься. Он присутствует практически в каждом контексте языка. Вы столкнетесь с этим, когда:

  • Использование методов обычных объектов
  • Ссылка на значения внутри классов
  • Попытка получить доступ к элементу или событию в DOM

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

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

это ссылка на объект

Что такое this? Попробуем дать этому простейшее определение:

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

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

Почему это?

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

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

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

Четыре правила, чтобы понять, к чему это относится

Существует четыре основных контекста, в которых this динамически присваивается другое значение:

  1. в глобальном контексте
  2. как метод объекта
  3. как функция-конструктор или конструктор класса
  4. как обработчик событий DOM

Давайте рассмотрим каждый из этих контекстов один за другим:

Глобальный контекст

В рамках отдельного сценария вы можете выяснить, чему это равно, путем записи this в консоль.

Попробуйте прямо сейчас и посмотрите, что у вас получится.

console.log(this); // window

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

Однако вы знаете, что у функций тоже есть свой контекст. Что насчет них?

Для объявлений функций он по-прежнему будет ссылаться на объект window:

function whatIsThis() {
  console.log(this); // window
}

whatIsThis();

Однако это поведение меняется, когда мы находимся в строгом режиме. Если перевести функцию в строгий режим, мы получим undefined:

function whatIsThis() {
  "use strict";

  console.log(this); // undefined
}

whatIsThis();

Это тот же результат, что и со стрелочной функцией:

const whatIsThis = () => console.log(this); // undefined
whatIsThis();

Теперь почему это улучшение undefined при работе с функциями, как с объявлениями функций в строгом режиме, так и с функциями стрелок вместо глобального объекта window? Выделите минутку и подумайте, почему это лучше.

Причина в том, что если this относится к глобальному объекту, очень легко добавить к нему значения, напрямую изменив объект:

function whatIsThis() {
  // "use strict";

  // console.log(this); // undefined
  this.something = 2;
  console.log(window.something);
}

whatIsThis(); // 2

Мы никогда не хотим, чтобы данные, относящиеся к функции, могли просочиться во внешнюю область. Это противоречит цели целиком привязки данных к функции.

Объектный метод

Когда у нас есть функция для объекта, у нас есть метод. Метод использует this для ссылки на свойства объекта. Итак, если у нас есть пользовательский объект с некоторыми данными, любой метод может уверенно использовать this, зная, что он будет ссылаться на данные самого объекта.

const user = {
  first: "Reed",
  last: "Barger",
  greetUser() {
    console.log(`Hi, ${this.first} ${this.last}`);
  },
};

user.greetUser(); // Hi, Reed Barger

Но что, если этот объект вложен в другой объект? Например, если мы поместим user в объект с именем userInfo с другими вещами?

const userInfo = {
  job: "Programmer",
  user: {
    first: "Reed",
    last: "Barger",
    greetUser() {
      console.log(`Hi, ${this.first} ${this.last}`);
    },
  },
};

userInfo.personalInfo.greetUser(); // Hi, Reed Barger

Пример все еще работает. Почему это работает?

Для любого метода this относится к объекту или другому способу мышления, объекту, который находится непосредственно слева от точки при вызове метода. Таким образом, в этом случае при вызове greetUser объект personalInfo находится сразу слева от точки. Так вот что это такое.

Однако, если мы попытаемся использовать this для получения данных от объекта userInfo:

const userInfo = {
  job: "Programmer",
  user: {
    first: "Reed",
    last: "Barger",
    greetUser() {
      console.log(`Hi, ${this.first} ${this.last}, ${this.job}`);
    },
  },
};

userInfo.personalInfo.greetUser(); // Hi, Reed Barger, undefined

Мы видим, что this не относится к userInfo. Правило здесь - смотреть непосредственно в левую часть точки при вызове метода, и вы узнаете, что такое this.

Контракторные функции + классы

Когда вы используете ключевое слово new, оно создает экземпляр класса или функции-конструктора, в зависимости от того, что вы используете. Когда экземпляр класса создается с new, ключевое слово this привязано к этому экземпляру, поэтому мы можем использовать this в любом из наших методов класса с уверенностью, зная, что мы можем ссылаться на свойства нашего экземпляра, такие как в этом примере first и age:

class User {
  constructor(first, age) {
    this.first = first;
    this.age = age;
  }
  getAge() {
    console.log(`${this.first} age is ${this.age}`);
  }
}

const bob = new User("Bob", 24);
bob.getAge(); // Bob's age is 24

Поскольку мы знаем, как классы под капотом основаны на функциях конструкторов и прототипном наследовании, мы знаем, что то же правило будет применяться и к функциям конструкторов:

function User(first, age) {
  this.first = first;
  this.age = age;
}

User.prototype.getAge = function () {
  console.log(`${this.first}'s age is ${this.age}`);
};

const jane = new User("Jane", 25);
jane.getAge(); // Jane's age is 25

Обработчик событий DOM

В браузере есть специальный this контекст для обработчиков событий. В обработчике событий, вызываемом addEventListener, this будет ссылаться на event.currentTarget. Чаще всего разработчики просто используют event.target или event.currentTarget по мере необходимости для доступа к элементам в DOM, но, поскольку ссылка this изменяется в этом контексте, это важно знать.

В следующем примере мы создадим кнопку, добавим к ней текст и добавим в DOM. Когда мы регистрируем значение this в обработчике событий, он распечатывает цель.

const button = document.createElement("button");
button.textContent = "Click";
document.body.appendChild(button);

button.addEventListener("click", function (event) {
  console.log(this); // <button>Click me</button>
});

Как только вы вставите это в свой браузер, вы увидите кнопку, добавленную к странице, с надписью «Click». Если вы нажмете кнопку, в консоли появится <button>Click</button>, поскольку нажатие на кнопку регистрирует элемент, которым является сама кнопка. Следовательно, как вы можете видеть, это относится к целевому элементу, к которому мы добавили прослушиватель событий.

Явно устанавливая значение этого

Во всех предыдущих примерах значение this определялось его контекстом - будь то глобальный, в объекте, в сконструированной функции или классе или в обработчике событий DOM. Однако с помощью функций call, apply или bind вы можете явно определить, на что this следует ссылаться.

.call () и .apply ()

Call и apply очень похожи - все они нужны для вызова функции в определенном контексте. Опять же this относится к объекту. Например, скажем, у нас есть объект, значения которого мы хотим использовать для функции:

const user = {
  name: "Reed",
  title: "Programmer",
};

function printUser() {
  console.log(`${this.first} is a ${this.title}.`);
}

printUser(); // "undefined is a undefined"

На этом этапе между функцией и объектом нет связи. Но используя call или apply, мы можем вызвать функцию, как если бы она была методом объекта:

printUser.call(user);
// or:
printUser.apply(user);

Мы можем увидеть, как call и apply задают контекст this с помощью следующего кода, снова используя нашу whatIsThis функцию:

function whatIsThis() {
  console.log(this);
}

whatIsThis.call({ first: "Reed" }); // { first: ‘Reed’}

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

Передача аргументов в .call () и .apply ()

Но что, если вы хотите использовать функцию, для работы которой требуются параметры? Такие, как это:

const user = {
  name: "Reed",
  title: "Programmer",
};

function printBio(city, country) {
  console.log(`${this.name} works as a ${this.title} in ${city}, ${country}.`);
}

printBio.call(user);

Если вы попытаетесь использовать call, как раньше, вы увидите, что мы устанавливаем this контекст для функции, но нам также нужно передавать аргументы с call.

Мы можем сделать это, указав эти аргументы после аргумента this, разделенные запятыми:

printBio.call(user, "New York City", "USA");

Однако в этом apply отличается. Единственное различие между call и apply состоит в том, что он принимает дополнительные аргументы в виде массива:

printBio.apply(user, ["New York City", "USA"]);

.связывать()

И call, и apply являются методами одноразового использования - если вы вызовете метод с this контекстом, он будет иметь его, но исходная функция останется неизменной.

Иногда вам может потребоваться снова и снова использовать метод с this контекстом другого объекта, и в этом случае вы можете использовать метод bind для создания совершенно новой функции с явно привязанным this.

const userBio = printBio.bind(user);

userBio();

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

const userBio = printBio.bind(user);

userBio();

const user2 = {
  name: "Doug",
  title: "Entrepreneur",
};

userBio.bind(user2);

userBio();

Хотя в этом примере выполняется попытка привязки userBio еще раз, он сохраняет исходный контекст this с первого раза.

Стрелочные функции не имеют этого

Стрелочные функции не имеют собственной this привязки. Вместо этого они переходят к следующему контексту выполнения.

const user = {
  first: "Bob",
  fn() {
    console.log(this.first);
  },
  arrowFn: () => {
    console.log(this.first);
  },
};

user.fn(); // ‘Bob’
user.arrowFn(); // undefined

Резюме

Давайте рассмотрим четыре различных способа вызова функции, которые определяют ее this привязку:

  1. в глобальном контексте: относится к глобальному объекту или undefined в строгом режиме / для стрелки fn
  2. как метод объекта: относится к объекту слева от точки при вызове метода
  3. как функция-конструктор или конструктор класса: относится к самому экземпляру при вызове с new
  4. как обработчик событий DOM: относится к самому элементу

В глобальной области или контексте this является глобальным объектом, обычно window в нестрогом режиме и undefined для строгого режима и стрелочных функций.

Для метода объекта, для чего this был в значительной степени разработан, при его вызове обращайте внимание непосредственно на левую часть точки. Это объект, к которому привязан this.

Для конструктора функций или классов использование new автоматически привяжет this к созданному экземпляру, поэтому все методы, добавленные к прототипу, могут использовать эти свойства экземпляра.

И, наконец, для нормальной функции, а не для функции стрелки, перейдите к обработчику событий DOM (addEventListener), this относится к самому элементу DOM

Просто следуйте этим правилам, и вы всегда сможете разобраться в том, что такое this!