понедельник, 28 сентября 2020 г.

Контекст вызова (this)

Источник 

В JavaScript this — это текущий контекст исполнения функции.

Функцию можно вызвать четырьмя способами:

  • вызов функции:
    alert('Hello World!'),
  • вызов метода:
    console.log('Hello World!'),
  • вызов конструктора:
    new RegExp('\\d'),
  • непрямой вызов:
    alert.call(undefined, 'Hello World!'),

и каждый из них определяет свой контекст, и поведение this слегка не соответствует ожиданиям начинающих разработчиков. Кроме того, strict mode (строгий режим) также влияет на контекст исполнения.

Помимо четырёх способа вызова, существуют также связанная функция (с помощью метода .bind()) и стрелочная функция.

  1. При вызове функции значение this (контекст вызова) равен глобальному объекту или undefined в строгом режиме.
  2. При вызове метода значение this (контекст вызова) равен объекту, которому принадлежит данный метод. (Следует избегать характерных ошибок - см. ниже [1] и [2]).
  3.  При вызове конструктора значение this (контекст вызова) равен созданному при помощи ключевого слова new объекту (экземпляр класса).
  4.  При непрямом вызове - с помощью методов .call() или .apply()- значение this (контекст вызова) равен первому аргументу методов .call() или .apply(), соответственно.
  5. Связанная функция - функция, возвращаемая с помощью метода .bind(). Значение this (контекст вызова) при вызове такой функции равен первому аргументу метода .bind().
  6.  Значение this (контекст вызова) стрелочной функции - это контекст, в котором определена данная стрелочная функция, т.е. стрелочная функция заимствует this из внешней функции, в которой она определена, либо глобальный объект или undefined в строгом режиме. При этом контекст стрелочной функции изменить нельзя.


[1] "Отделение" метода:

let user = {
  name: "Джон",
  hi() { alert(this.name); }
};
 
// вызов метода объекта
user.hi(); // Вывод: "Джон"
// разделим получение метода объекта и его вызов
let hi = user.hi;
hi(); // Ошибка, потому что значением this является undefined

Для работы вызовов типа user.hi(), JavaScript использует трюк – точка '.' возвращает не саму функцию, а специальное значение «ссылочного типа», называемого Reference Type.

Этот ссылочный тип (Reference Type) является внутренним типом (мы не можем явно использовать его, но он используется внутри языка).

Значение ссылочного типа – это «триплет»: комбинация из трёх значений
(
base, name, strict), где:

base – это объект,
name – это имя свойства объекта,
strict – это режим исполнения. Является true, если действует строгий режим (use strict).

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

// значение ссылочного типа (Reference Type)
(user, "hi", true)

Когда скобки () применяются к значению ссылочного типа (происходит вызов), то они получают полную информацию об объекте и его методе, и могут поставить правильный this (т.е. user в данном случае, по base).

Ссылочный тип – исключительно внутренний, промежуточный, используемый, чтобы передать информацию от точки . до вызывающих скобок ().

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

Таким образом, значение this передаётся правильно, только если функция вызывается напрямую с использованием синтаксиса точки obj.method() или квадратных скобок obj['method']() (они делают то же самое). Существуют различные варианты решения проблемы потери значения this. Например, такие как func.bind().

[2] this во внутренней функции:

var numbers = {
   numberA: 5,
   numberB: 10,
   sum: function() {
     console.log(this === numbers); // => true
     function calculate() {
       // this is window or undefined in strict mode
       console.log(this === numbers); // => false
       return this.numberA + this.numberB;
     }
     return calculate();
   }
};

// NaN or throws TypeError in strict mode
console.log(numbers.sum()); 

numbers.sum() — это вызов метода объекта, поэтому контекстом sum является объект numbers. Функция calculate определена внутри sum, поэтому можно было бы ожидать, что this — это объект numbers и в calculate(). Тем не менее, calculate() — это вызов функции, а не метода, и поэтому его this — это глобальный объект window или undefined в strict mode. Даже если контекстом внешней функции sum является объект numbers, он не "передается" во вложенную функцию.