this и все его воплощения

Что такое this?

Ключевое слово/переменная this: это специальный идентификатор, который указывает на данные в зависимости от условий выполнения кода. Создается для каждого контекста выполнения. Обычно указывает на владельца функции, в которой this используется. Очень часто можно услышать, что this это некий "контекст". Это неверно, this находится в контексте, но никак не является им.

this такой разный..

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

this в обычной функции

Внутри функции this равняется объекту window, но если у вас стоит строгий режим use strict тогда undefiend.

"use strict";
function showThis (){
    console.log(this); 
}

showThis(); // мы получим undefined

Без строгого режима:

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

showThis(); // получим объект window
this

А что если внутри функции будет еще функция с this?

"use strict"

function showThis2(a, b) { // создаем функцию с 2 аргументами, 
    console.log(this); // undefined

    function sum() { // внутри еще одну функцию
        console.log(this); // здесь значение this будет таким же либо window либо undefiend.
        return a + b; 
    }

    console.log(sum()); // undefined
}

showThis2(1, 2); // 3

В общем в обычной функции будет window либо undefined в зависимоси от режима. Даже если функция находится внутри другой функции!

this у методов объекта

Метод принадлежит объекту, поэтому тут this будет равна этому объекту.

"use strict"
const obj = { // Объект 
    a: 20,
    b: 15,
    sum() { // внутри метод
        function what() {
            console.log(this); // будет undefiend. Потому что это функция внутри метода, а не сам метод объекта,
        }
        console.log(this); // this будет объектом в котором находится функция
        what();
    }
};

obj.sum()

Важно понимать, что функция внутри метода не является методом.

thisInObject

this в конструкторах и классах

this в конструкторах и классах - это новый экземпляр объекта.

function User(name, age) { // создаем функцию конструктор.
    this.name = name; // эти this обращаются к объекту которые мы будем создавать 
    this.age = age; // Посути вместо this каждый раз подставляется новый экземпляр объекта.
    this.human = true; // если мы создадим объект lox, то выглядело бы это так lox.age и тд.
    this.hello = function () {
        console.log('hello' + this.name);
    };


}

let ivan = new User('ivan', 24); // this принимает этот экземпляр
let dima = new User('Dima', 30); // и теперь этот. Здесь при создании мы сразу передаем значения в наши свойства name и age

dima.hello(); // выдаст (helloDima)
console.log(ivan);

Немного рассказываю, что происходит. Мы создаем переменную ivan, в нее помещаем наш конструктор и указываем свойства. Внутри конструктора под this подставляется наш ivan, и ему записываются свойства (значения из аргументов в функции) ivan.name = name, ivan.age = age и так далее. Мы таким образом конструируем объект. Когда мы создадим dima, вместо this уже как бы подставится dima и так далее.

thisInClasses

Стрелочные функции и this

Главное отличие стрелочной функции это то, что при вызове стрелочной функции у this нет поведения по умолчанию из за этого, во время вызова стрелочной функции this остается нетронутым и будет указывать на this из родительской области делает он это с уже известной нам scope chain(цепочке областей видимости). Из этого делаем вывод, что неважно как мы вызываем стрелочную функцию, важно то, где она создается.

const obj = { // наш объект
    num: 2,
    sayNumber: function () { // метод этого объекта
        const say = () => {
            console.log(this); // стрелочная функция внутри метода
        };

        say();
    }

};

obj.sayNumber(); // значение this будет объект  {num: 2, sayNumber: ƒ}
// у метода в объекте this сам объект, стрелочная функция взяла тот же this, что и у метода в котором она находится.
const obj = {
    x: 10,
    f1() {
        // используем стрелочную функцию
        setTimeout(() => {  
            console.log(this.x);  // this будет ссылаться на родительский объект obj
        }, 1000);
    }
};

obj.f1(); // выведет 10 через 1 секунду

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

"use strict";
const obj = {
    name: "Dima",

    getName: () => {
        console.log(`hello ${this.name} `);
    }
};


obj.getName(); // hello undefined 

///////////////////////////
// Метод с обычной функцией.
const obj = {
    name: "Dima",

    getName: function () {
        console.log(`hello ${this.name} `);
    }
};


obj.getName(); // hello Dima 

В этом примере мы получили undefined потому что родитель объекта obj это глобальная область и тут как мы помним this будет undefined в строгом режиме.

this в событии с обычной функцией

При работе с DOM это api устанавливает значение this в значение элемента к которому привязано событие. То есть если обработчик с обычной функцией, то this как и event.target будет сам элемент события.

const btnThis = document.querySelector('.btn-this');

btnThis.addEventListener('click', function () {
        this.style.backgroundColor = 'red'; // при нажатии перекрасим кнопку в красный, так как this - элемент события
});

Со стрелочной функцией мы будем получать this все так же от родителя.

"use strict" // вне зависимости от режима
const btnThis = document.querySelector('.btn-this');

btn.addEventListener('click', () => {
  console.log(this); // window
})

Ручная привязка значения this

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

function sayName(sername) {           
    
};

const us = {
    name: 'diman',
    age: 10
};

Для этого есть два метода и один который сам создает новую функцию.

call()

function sayName(surname) {
    console.log(this);
    console.log(`${this.name} ${surname}`);
    console.log(this.age);
}

let us = {
    name: 'dima',
    age: 10
}

sayName.call(us, 'anime');// просто вызываем у функции метод.
// далее передаем объект, а после аргументы для функции.

Как видим теперь this это объект us и мы можем использовать его свойства внутри функции с аргументами.

call

apply()

Этот метод делает одно и тоже, что и call. Но у них разный синтаксис.

sayName.apply(us, ['anime']); // здесь все тоже самое только аргументы внутри массива

bind()

Тут все почти также, только мы создаем новую функцию.

function count(num) {
    return this * num; //  здесь в this будет двойка, мы ее передали ниже.
}

const double = count.bind(2); // здесь мы по сути поместли в count новую функцию и забиндили в this = 2

// мы навсегда забиндили функцию! создали ей this ввиде двойки и теперь просто вызываем нашу double и передам нужный аргумент 
console.log(double(10)); // 20

Еще немного стрелочных функций

Также внутри стрелочной функции нельзя использовать методы call(), apply() и bind(), потому что стрелочная функция автоматически указывает this из родительской области и это поведение мы никак не можем изменить.

const a = {c:1};
const b = {c:2};

function hi() {
    return  () => {
        console.log(this.c); // 1
    };
}

const hello = hi.call(a); // явно указали, что this в контексте hi будет объект a
hello.call(b); // Тут мы ссылаемся на нашу стрелочную анонимную функцию и пытаемся изменить this

В итоге мы получим 1. Потому что стрелочная функция берет this родителя.

const a = {c:1};
const b = {c:2};

function hi() {
    return  () => {
        return () => {
            return () => {
                console.log(this.c) // 1
            }
        }
    };
}

const hello = hi.call(a); 
hello.call(b); 

Даже если мы сделаем такую цепь стрелочных функций, то ничего не изменится. Стрелочная функция будет идти по scope chain из одной функции в другую пока не найдет значение для this, все как и с обычными переменными(идентификаторами). При этом this не является частью лексического окружения, но цепочка областей видимости используется для определения того, какое значение this должно быть унаследовано стрелочной функцией.

ReferenceType & this

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

  • ReferenceType - это встроенный тип ecmascript который используется на уровне реализации языка. Комбинирует в себе три значения (base, name, strict), где: base - это объект, name - имя свойства и strict - true, если используется строгий режим "use strict". ReferenceType используется внутри языка для передачи информации об объекте и его методе между точкой . и вызывающими скобками ().

Так вот ReferenceType возвращается при доступе к свойству объекта с помощью точки obj.property или квадратных скобках obj[property]. Так же ReferenceType возвращается когда выполняется обращение к переменной и так же для деклараций функции. Теперь давайте сразу посмотрим на пример, так будет сразу ясно что к чему:

const user = {
    name: "Dimasik",
    hi() { console.log(this.name); }
};

user.hi(); // Dimasik

В этом прекрасном примере мы вызываем hi объекта user с помощью точки, так вот точка нам возвращает не функцию, а значения Reference Type: где base = user, name = hi, strict = true или false. После скобки () вызываются на Reference Type и таким образом они получают всю информацию об объекте и его методе и используется это для того, что бы установить правильное значение this на основе значения base и вызвать нужный метод на основе значения name.

  • Коротко и ясно: когда мы вызываем метод через точку и получаем ReferenceType, то значение base будет значением this.
const user = {
    name: "Dimasik",
    hi() { console.log(this.name); } // у глобального объекта нет такого свойство, поэтому будет undefined
};

const hello = user.hi; 
// в строгом режиме, если мы обращаемся к несуществующему свойству, то будет TypError
hello(); // undefined || TypError

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

  • Если слева от скобок () вызова нет ReferenceType, тогда this будет undefiend или window.

Функциональные выражения. GetValue

  • GetValue - это встроенный метод ReferenceType. Он возвращает истинный тип получаемого через ReferenceType объекта. То есть если это функция, то вернется Function. GetValue всегда срабатывает в зоне выражения.

  • Зона выражения - это часть в контексте выполнения, где мы ожидаем выражение. Или это часть, где что-то может быть вычислено в значение. То есть все наши выражения создают зону выражения: присваивание =, операторы || или другие логические операторы, тернарный оператор, перечисление через запятую и тому подобное.

"use strict";
// вместо ReferenceType мы получим Function. Вспоминаем, что если !ReferenceType, то this будет undefiend или window
const hi = function() { 
    console.log(this); // undefined
};

hi();

Функция hi вызывается в зоне выражения. Поэтому значение this внутри функции будет равно undefined.
Еще пару примеров:

var x = 10; 
const obj = {
    x:1,
    f1: function () {
        return console.log(this.x); 
    }
}

obj.f1(); // 1

// в этих случаях у нас this = window. Так как слева от скобок в зоне выражения, мы получаем Function
(obj.f1 = obj.f1)(); // 10
(obj.f1 || obj.f1)(); // 10
(obj.f1, obj.f1)(); // 10

Так как я использовал var то эта переменная станет частью объекта window. Поэтому мы ее и получим.

[[construct]] & [[call]]. Конструкторы.

Теперь немного взглянем что происходит с this в конструкторах. Конструктором является та функция которую мы вызываем оператором new.

  • [[construct]] - это метод, который вызывается при создании нового объекта с помощью оператора new. Он создает новый объект и устанавливает его прототипом функцию-конструктор.

  • [[call]] - это метод, который вызывается при вызове функции. Он проставляет в this объект, на котором была вызвана функция.

function Person(name, age) {
    this.name = name;
    this.age = age;
}

const diman = new Person('Dimasiks', 26);
  1. Вызываем функцию конструктор с помощью new.
  2. Создается новый объект с помощью внутреннего метода [[construct]] и этот же метод устанавливает новому объекту прототип функцию-конструктор. diman будет иметь прототип Person.prototype.
  3. Вызывается внутренний метод [[call]] для функции-конструктора и передает ему созданный объект в качестве значения this. То есть внутри функции-конструктора this ссылается на созданный объект.
  4. Внутри функции-конструктора мы используем this для определения свойств объекта. Мы устанавливаем свойства name и age объекта this на значения ‘Dimasiks’ и 26.
  5. JavaScript возвращает созданный объект из функции-конструктора.
Обновлено 05.04.2023