Предисловие

Мне давно хотелось расставить все точки над вопросом определения this.

В этой статье я использовал информацию из открытых источников.

Большая часть информации взята с YouTube-канала As For JS, а также из документации на mdn с моим переводом. Я постарался максимально проверить материал.

Уважаемые читатели, можете оставлять свои замечания в комментариях, и я постараюсь исправить их в статье.

Введение в тему: "Как определить this в JavaScript"

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

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

Почему мы относимся к this как к идентификатору?

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

В JavaScript принципы довольно просты: у нас есть код, который выполняется. Среда, где этот код выполняется, имеет свои правила и называется Global Environment (Глобальная среда). Каждый раз, когда вызывается новая функция, создается новое окружение. При последующих вызовах создается новое окружение. Окружение существует только в контексте кода, который в данный момент выполняется. Если в коде встречается определение функции, вокруг этой функции создается свое собственное окружение. В окружении хранится вся необходимая информация для выполнения данного кода.

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


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

Функция имеет глобальный контекст, когда она вызывается вне каких-либо функций или классов. При этом она может находиться внутри блоков или стрелочных функций определенных в глобальной области видимости).

Значение this зависит от того, в каком контексте выполнения выполняется скрипт.

Как и callback, значение this определяется средой выполнения (вызывающим объектом).

На верхнем уровне скрипта, this относится к globalThis независимо от того, находится ли он в строгом режиме или нет.

globalThis это глобальное свойство содержащее глобальное this значение, которое обычно аналогично глобальному объекту.

Пример:

В случае запуска в среде Node JS в не строгом режиме globalThis будет объект global:

/*
<ref *1> Object [global] {
  global: [Circular *1],
  clearInterval: [Function: clearInterval],
  clearTimeout: [Function: clearTimeout],
  setInterval: [Function: setInterval],
  setTimeout: [Function: setTimeout] {
    [Symbol(nodejs.util.promisify.custom)]: [Getter]
  },
  queueMicrotask: [Function: queueMicrotask],
  clearImmediate: [Function: clearImmediate],
  setImmediate: [Function: setImmediate] {
    [Symbol(nodejs.util.promisify.custom)]: [Getter]
  }
}
*/

В случае запуска кода в браузере в не строгом режиме globalThis будет window:

// [object Window]

Если исходный код загружен как модуль, то this всегда будет undefined на верхнем уровне:

<script src="index3.js" type="module"></script>
console.log(this) // undefined

Базовый пример внутри функции.

Рассматриваем самый простой и базовый пример:

"use strict"
console.log("this is", this) // this is {}

Мы находимся в строгом режиме ("use strict").

Мы хотим понять с чем связан this. Алгоритм с точки зрения спецификации следующий:

Мы находимся внутри функции?

⇒ Нет.

⇒ Переходим к новой ветке на нашей схеме - “Мы внутри скрипта или внутри модуля?”


Script или Module

Я сразу отмечу, что на схемах определения this указано относительно strict mode и не учитывает изменение this внешним API.

Script и Module это два термина, которые описывают разные способы выполнения кода.

Script - код, который запускается по умолчанию.

Module - запуск кода с параметром module. Например когда мы используем import.

⇒ Задаем себе вопрос: “Я внутри модуля”?

  • Да

    ⇒ Тогда this равен undefined. Других вариантов существовать не может.

  • Нет

    this по умолчанию связан с global object.

    • Согласно спецификации Host-среда (например браузер, Node) имеет возможность по умолчанию изменить то значение this, которое было связано с глобальным объектом.

      Поэтому мы задаем вопрос: “Ноst изменяет this?”

      • Нет

        ⇒ Тогда this равен undefined

      • Да

        this равен значению, которое установил Host


Function Environment

Мы изучили ветку Script или Module и переходим в ветку Function Environment.

Под словами “нормальная функция” (normal function) мы будет иметь ввиду любую функцию, которая не является arrow function (стрелочной функцией). То есть это все функции, у которых между аргументами и их телом функции отсутствует символьная пара =>.

Мы помним, что наш основной вопрос это: “Я в коде функции?”.

Напишем пример в котором this будет определяться внутри функции.

"use strict"

function doLogThis() {
  let doArrowThing = () => console.log("this is", this)
  doArrowThing()
}

doLogThis() // this is undefined

В этом коде вызывается doLogThis. Внутри doLogThis определилась стрелочная функция doArrowThing и потом она была вызвана.

Алгоритм будет следующий:

  • Мы находимся внутри нормальной функции?

    • Нет!

      Мы находимся внутри стрелочной функции.

      ⇒ Переходим к ветке для стрелочной функции


Arrow function

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

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

Например, в глобальном коде (не в строгом режиме), this всегда globalThis независимо от строгости, из-за привязки к глобальному контексту:

const globalObject = this;
const foo = () => this;

console.log(foo() === globalObject); // true

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

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

const globalObject = this;
const foo = () => this;
const obj = { name: "obj" };

console.log(foo.call(obj) === globalObject); // true

const boundFoo = foo.bind(obj);
console.log(boundFoo() === globalObject); // true

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


Вызов нормальной функции

Правило, которое нужно запомнить. В момент вызова любой нормальной функции (не стрелочной) у нее всегда есть аргумент this без исключения. Если аргумент this ни каким образом не задан, тогда по умолчанию он всегда связан с значением undefined.

Пример:

"use strict"

function doLogThis() {
	console.log('this is', this) // undefined
}

doLogThis()

this и call, apply, bind

Если мы находимся внутри нормальной функции в первую очередь необходимо посмотреть как эта функция вызывается.

Пример:

function SomeObject() {}

SomeObject.prototype.someMethod = function() {
  console.log('Hello!');
};

const newObj = new SomeObject();

newObj.someMethod(); // Выведет: Hello!

Методы call, apply, bind позволяют задать this явным образом.

"use strict"

function doLogThis() {
	console.log("this is", this)
}

let thisArg = 1

doLogThis.call(thisArg) // 1
doLogThis.apply(thisArg) // 1
doLogThis.bind(thisArg)() // 1

В нашем примере функция doLogThis вызывается с использованием методов call, apply, bind с переданным значением thisArg (все сразу указаны для примера), а это значит this равен thisArg (тому, что мы передали в методы).

Многие могут подумать, что this связан с какой-то сложной объектной структурой, но это не так. Используя call, apply, bind мы можем связать this и с любым примитивным значением.

Разберем методы call, bind и apply более подробно.

call()

call() - это метод, который позволяет вызвать функцию, определяя контекст (this) и передавая параметры в виде списка.

const person1 = {
  name: 'Alice',
  greet: function(place) {
    console.log(`Hello, ${this.name}! Welcome to ${place}.`);
  }
};

const person2 = {
  name: 'Bob'
};

person1.greet.call(person2, 'the park');

// Вывод: Hello, Bob! Welcome to the park.

В этом примере мы использовали call() для вызова метода greet объекта person1, но с контекстом объекта person2. Это позволило нам использовать метод greet объекта person1, но сделать так, чтобы this указывало на person2.

apply()

apply() похож на call(), но принимает аргументы в виде массива

const numbers = [2, 4, 6, 8, 10];
const maxNumber = Math.max.apply(null, numbers);

console.log(maxNumber); // Вывод: 10

Здесь мы использовали apply() для вызова метода Math.max, который обычно принимает список чисел как аргументы, но в данном случае мы передали массив numbers. apply() позволяет использовать массив в качестве аргументов функции.

bind()

bind() создает новую функцию с привязанным контекстом (this), которую можно вызвать позже.

const obj = {
  value: 30,
  getValue: function() {
    return this.value;
  }
};

const getValue = obj.getValue;
const boundGetValue = obj.getValue.bind(obj);

console.log(getValue()); // Вывод: undefined (так как контекст не определен)
console.log(boundGetValue()); // Вывод: 30

getValue без привязки контекста возвращает undefined, так как контекст теряется при сохранении функции в переменной. Но boundGetValue, созданная с помощью bind(), сохраняет контекст объекта obj и успешно возвращает 30.


this и new

Пример:

"use strict"

function doLogThis() {
	console.log("this is", this)
}

new doLogThis(); // {}
new doLogThis; // {}

В данном случае вызов doLogThis произошел при помощи ключевого слова new.

Ключевое слово new вызывает функцию, кроме стрелочной функции.

Ключевое слово new вызывает функцию и this связывается с пустым объектом {}.

Использовать () для создания call expression не обязательно.


Call API

Теперь поговорим о том, что в вполне понятном JavaScript может возникнуть "неожиданное" поведение.

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

В нашей логике путаница может начинаться тогда, когда мы сталкиваемся с внешним API.

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

Как реализовано конкретное API, можно узнать только из его документации.

Например в HTML5 есть API addEventListener, которое позволяет привязывать к определенным событиям стандарта HTML5 функцию - обработчик.

В документации прямо заявляется о том, что вызов обработчика, будет осуществлен с привязкой this к объекту событие которого он обслуживает.

Самый простой способ увидеть это:

"use strict"

function doHandleClick() {
  console.log("this is", this)
}

document.body.addEventListener("click", doHandleClick)

Если мы используем код в браузере, то увидим, что this равен не undefined, а body.

Почему так произошло? Согласно стандарту HTML5 addEventListener обслуживает любой вызов обработчика события таким образом, при вызове этой функции в callback передается event currentTarget. На этом моменте происходит дозволенное нарушение спецификации языка JavaScript. По этой причине меняется значение this внутри doHandleClick.

Перепроверить это можно так:

"use strict"

function doHandleClick() {
  console.log("this is", this)
}

document.body.addEventListener("click", doHandleClick.bind({ name: "Pavel" }))

Теперь благодаря использованию bind наше значение this будет равно объекту { name: "Pavel" }.


this и dot нотация

Дот нотацией (dot notation) в JavaScript, называют синтаксис, когда два идентификатора разделены между собой точкой (dot). Например theObj.theProperty.

Его полным аналогом является синтаксис theObj["theProperty"].

Вызов функции в dot нотации выглядит следующим образом:

theObj.doTheing();
// или
theObj["doTheing"]();

Большая часть задач на собеседованиях относительно чему равен this по сути крутится вокруг dot нотации. По этой причине многие ошибочно думают, что this это контекст.

"use strict"

function doLogThis() {
	console.log("this is", this)
}

const theObj = { name: "Pavel" }

theObj.doLogThis = doLogThis
theObj.doLogThis() // { name: 'Pavel', doLogThis: [Function: doLogThis] }

Если функция была вызвана в dot notation, тогда this равно тому идентификатору, который стоит перед точкой.

Пример наоборот:

"use strict"

const theObj = {
  name: "Pavel",
  doLogThis: function () {
    console.log("this is", this)
  }
}

const doLogThisGlobal = theObj.doLogThis
doLogThisGlobal() // this is undefined

Внутри theObj определена функция doLogThis как нормальная функция. doLogThisGlobal связан с функцией theObj.doLogThis. Происходит вызов функции doLogThisGlobal.

Следующий пример:

"use strict"

const theObj = {
  name: "Pavel",
  doLogThis: function () {
    console.log("this is", this)
  }
}

setTimeout(theObj.doLogThis, 1)

Этот пример очень часто встречается на собеседованиях.

theObj определен так же как и в предыдущем примере. theObj.doLogThis должен вызваться через 1мс с помощью setTimeout.

Идем по нашему алгоритму:

  • Видим call, apply, bind?

    • Нет

      • Видим new?

        • Нет

          • Функция вызвана в dot нотации?

            • Да (внутри setTimeout)

Тоже самое произойдет если код будет таким:

"use strict"

const theObj = {
  name: "Pavel",
  doLogThis: function () {
    console.log("this is", this)
  }
}
const doLogThis = theObj.doLogThis

setTimeout(doLogThis, 1)

Почему this стало равным объекту window при запуске в браузере?

Дело в том, что setTimeout является внешним API. Host может определить this так, как ему нужно.

В описании таймеров в спецификации HTML5 мы можем найти следующую информацию. В тот момент, когда наш обработчик (handler), который должен быть вызван по прошествии определенного времени, будет вызываться, this должно быть связано с глобальным контекстом (global). В случае, если наше окружение - это script, тогда это будет window, а в случае с module - undefined.

При запуске в среде Node.js мы наблюдаем тот же эффект, но теперь Host назначил this как объект Timeout. Если мы посмотрим спецификацию Node.js, то увидим, что таймер - это, на самом деле, класс Timeout.

Напоминаю, что подобные изменения не касаются случаев, когда мы назначаем this явным образом, используя call, bind, apply.


Особенности this в non strict mode

При вызове normal function, this связывается не с undefined, а с global object.

В случае вызова с использованием dot нотации: theObj.doThing() this будет связан со значением ToObject(theObj)

Например:

String.prototype.doThingStrict = function () {
	"use strict"
	console.log("this is", this)
}

String.prototype.doThing = function () {
  console.log("this is", this)
}

"Yo".doThingStrict() // this будет связан со значением "Yo"
"Yo".doThing() // this будет связан со значением new String("Yo")

Если наш код выполняется в "use strict", то тогда this всегда связан с тем с чем он и был связан без не явных преобразований.

В не strict режиме this всегда проходит через объект-обертку.


Классы, объекты и this

this внутри объекта:

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

// Пример объекта с методами
const user = {
  name: "John",
  greet: function() {
    console.log(`Hello, ${this.name}!`);
  },
  updateName: function(newName) {
    this.name = newName;
    console.log(`Name updated to ${this.name}`);
  }
};

user.greet(); // Вывод: Hello, John!
user.updateName("Alice"); // Вывод: Name updated to Alice
user.greet(); // Вывод: Hello, Alice!

Здесь методы greet и updateName объекта user могут использовать this, чтобы получить доступ к свойству name объекта и изменить его значение.


Использование this во вложенных объектах:

Когда объект содержит вложенные объекты и методы, this все еще ссылается на объект, из которого был вызван метод, а не на объект, содержащий вложенный метод.

// Пример объекта с вложенным методом
const school = {
  name: "ABC School",
  classroom: {
    number: 101,
    displayInfo: function() {
      console.log(`Classroom ${this.number} at ${school.name}`);
    }
  }
};

school.classroom.displayInfo(); // Вывод: Classroom 101 at ABC School

Здесь метод displayInfo, находясь внутри объекта classroom, все равно использует this для доступа к свойству number объекта classroom и свойству name объекта school.


Конструкторы

Когда функция используется в качестве конструктора (с new), то this привязывается к создаваемому новому объекту, независимо от того, к какому объекту осуществляется доступ к функции-конструктору.

Значение this становится значением new выражения, если только конструктор не возвращает другое не примитивное значение.

function C() {
  this.a = 37;
}

let o = new C();
console.log(o.a); // 37


function C2() {
  this.a = 37;
  return { a: 38 };
}

o = new C2();

console.log(o.a); // 38

Во втором примере (C2), поскольку объект был возвращен во время конструирования, новый объект, к которому this был привязан, отбрасывается.

По сути, это делает оператор this.a = 37; мертвым кодом. Он не совсем мертв, потому что выполняется, но его можно устранить без каких-либо внешних эффектов.


super()

Когда функция вызывается в форме super.method() в JavaScript, значение this внутри этой функции будет таким же, как и значение this вокруг вызова super.method(), и обычно не будет равно объекту, на который ссылается super. Это происходит потому, что super.method не является доступом к члену объекта, как в примерах выше - это специальный синтаксис с другими правилами привязки.

class Parent {
  showThis() {
    console.log(this);
  }
}

class Child extends Parent {
  showParentThis() {
    super.showThis();
  }
}

const instance = new Child();

instance.showParentThis(); // Child {}

В этом примере при вызове instance.showParentThis(), super.showThis() вызывает метод showThis() из родительского класса, но значение this внутри showThis() будет таким же, как и значение this внутри instance.showParentThis(). То есть, оно будет указывать на экземпляр класса Child, а не на сам объект, на который ссылается super.


Контекст класса

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

Ключевое слово this в JavaScript имеет разное значение в каждом из этих контекстов.

Экземплярный контекст относится к конструкторам, методам и начальным значениям для объектов (public или private). В этом случае this указывает на создаваемый экземпляр класса.

class Example {
  constructor(value) {
    this.instanceValue = value; // this ссылается на созданный экземпляр класса
  }
  
  instanceMethod() {
    console.log(this === exampleInstance); // this ссылается на экземпляр класса Example
  }
}

const exampleInstance = new Example(10);

exampleInstance.instanceMethod(); // true

В данном примере мы видим, что this равен инстансу exampleInstance

Статический контекст касается статических методов, начальных значений статических полей и блоков статической инициализации. Здесь this отличается, указывая на сам класс (или подкласс), а не на конкретный экземпляр.

class Example {
  constructor(value) {
    this.instanceValue = value; // this ссылается на созданный экземпляр класса
  }
  
  static staticMethod() {
    console.log(this === Example); // this ссылается на сам класс Example
  }
}

const exampleInstance = new Example(10);

Example.staticMethod(); // true

В данном примере мы видим, что this равен классу Example

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

class Animal {
  constructor(name) {
    this.name = name;
  }
}

const myAnimal = new Animal('Leo');

console.log(myAnimal.name); // Вывод: 'Leo'

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

class Counter {
  constructor() {
    this.count = 0;
  }

  increase() {
    this.count++;
  }
}

const myCounter = new Counter();

myCounter.increase();

console.log(myCounter.count); // Вывод: 1

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

class Calculator {
  static add(a, b) {
    return a + b;
  }
}

console.log(Calculator.add(2, 3)); // Вывод: 5

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

class Rectangle {
  static defaultWidth = 100;
  height;
  
  constructor(height) {
    this.height = height;
  }
  
  getArea() {
    return Rectangle.defaultWidth * this.height;
  }
}

const myRect = new Rectangle(20);

console.log(myRect.getArea()); // Вывод: 2000

Функции со стрелками в инициализаторах полей связаны с контекстом создания объекта для полей экземпляра и с классом для статических полей.

class Person {
  #age;

  constructor(name, age) {
    this.name = name;
    this.#age = age;
    this.greet = () =&gt; {
      return `Hello, my name is ${this.name} and I am ${this.#age} years old.`;
    };
  }
  
  static introduce() {
    return "I am a person!";
  }
}

const john = new Person("John", 30);

console.log(john.greet()); // Вывод: "Hello, my name is John and I am 30 years old."
console.log(Person.introduce()); // Вывод: "I am a person!"

Здесь у класса Person есть поле name, которое устанавливается в конструкторе. Мы также создаем метод greet, используя стрелочную функцию внутри инициализатора поля. Этот метод использует значения name и #age, установленные при создании экземпляра, чтобы сформировать приветствие с именем и возрастом объекта Person.

Также есть статический метод introduce, который просто возвращает строку, и в данном случае он принадлежит самому классу, а не конкретному экземпляру.


Конструкторы производных классов

Производный класс - это класс, который наследует свойства и методы от другого класса, называемого базовым классом или родительским классом.

Конструкторы производных классов в JavaScript отличаются от конструкторов базового класса тем, что они не имеют начальной связи с this. При вызове super() создается связь с this внутри конструктора, что по сути эквивалентно выполнению этой строки кода, где Base - базовый класс:

this = new Base();

Если обратиться к this перед вызовом super(), это вызовет ошибку. Производные классы не должны возвращать данные перед вызовом super(), за исключением случаев, когда конструктор возвращает объект (и переопределяет значение this) или если у класса вообще нет конструктора.

class Base {}

class Good extends Base {} // Корректный пример, нет необходимости в явном конструкторе

class AlsoGood extends Base {
  constructor() {
    return { a: 5 }; // Работает, так как возвращается объект, переопределяя значение this
  }
}

class Bad extends Base {
  constructor() {} // Некорректно, вызовет ошибку из-за отсутствия вызова super()
}

new Good(); // ОК
new AlsoGood(); // ОК
new Bad(); // Ошибка: Необходимо вызвать конструктор родительского класса перед доступом к 'this' или возвратом из конструктора производного класса

Примеры определения this

Задача 1: Нормальная функция и стрелочная функция в объекте

const obj = {
  regularFunc: function() {
    console.log(this);
  },
  
  arrowFunc: () => {
    console.log(this);
  }
};

obj.regularFunc(); // Ответ: obj (текущий объект)
obj.arrowFunc(); // Ответ: глобальный объект или undefined (стрелочная функция не имеет собственного this, используется внешний контекст)

Задача 2: Внешнее API в методе объекта

const obj = {
  name: 'Alice',
  
  greet: function() {
    console.log(`Hello, ${this.name}!`);
  },

  delayedGreet: function() {
    setTimeout(this.greet, 1000);
  }
};

obj.greet(); // Ответ: Hello, Alice!
obj.delayedGreet(); // Ответ: "Hello, undefined!" или ошибка (this потеряно из-за вызова функции setTimeout в другом контексте)

В этом примере контекст this изменяется из-за специфики функции setTimeout.

Когда функция this.greet передается в качестве коллбека для setTimeout, она теряет связь с объектом obj. Это происходит потому, что функция setTimeout запускает переданную функцию в глобальном контексте (в нестрогом режиме) или в undefined (в строгом режиме).

В результате this внутри функции greet больше не указывает на объект obj.

Для сохранения контекста можно воспользоваться методом bind() или стрелочной функцией для явного привязывания контекста:

const obj = {
  name: 'Alice',
  
  greet: function() {
    console.log(`Hello, ${this.name}!`);
  },
  
  delayedGreet: function() {
    setTimeout(this.greet.bind(this), 1000); // использование bind() для сохранения контекста
    // или setTimeout(() => this.greet(), 1000); // использование стрелочной функции
  }
};

obj.greet(); // Ответ: Hello, Alice!
obj.delayedGreet(); // Ответ: "Hello, Alice!" (контекст this сохранен)

Задача 3: Использование this в классе

class Counter {
  constructor() {
    this.count = 0;
  }

  increment() {
    this.count++;
    console.log(this);
  }
}

const myCounter = new Counter();
myCounter.increment(); // Ответ: myCounter (экземпляр класса Counter)

Задача 4: Привязка контекста через bind()

function greet() {
  console.log(`Hello, ${this.name}!`);
}

const person = { name: 'John' };
const boundFunc = greet.bind(person);

boundFunc(); // Ответ: "Hello, John!" (this привязан к объекту person)

Задача 5: Использование this в стрелочной функции внутри метода

const obj = {
  name: 'Sarah',
  sayHi: function() {
    const greet = () => {
      console.log(Hi, ${this.name}!);
    };

    greet();
  }
};

obj.sayHi(); // Ответ: "Hi, Sarah!" (стрелочная функция использует this родительского контекста)

Задача 6: Использование this внутри коллбека события

const button = document.getElementById('myButton');

button.addEventListener('click', function() {
  console.log(this);
});

// Ответ: DOM элемент кнопки (this указывает на элемент, на котором произошло событие)

Задача 7: Метод объекта, вызванный из другого контекста

const obj = {
  name: 'Emma',
  
  greet: function() {
    console.log(`Hello, ${this.name}!`);
  }
};

const anotherObj = { name: 'Jack' };

anotherObj.sayHello = obj.greet;
anotherObj.sayHello(); // Ответ: "Hello, Jack!" (метод вызывается в контексте anotherObj)

Задача 8: Использование this в конструкторе класса

class Car {
  constructor(make) {
    this.make = make;
    console.log(this);
  }
}

const myCar = new Car('Toyota'); // Ответ: myCar (экземпляр класса Car)

Задача 9: Вызов метода из метода с другим this

const obj = {
  name: 'Michael',
  
  greet: function() {
    console.log(`Hello, ${this.name}!`);
  },
  
  callGreet: function() {
    this.greet(); // вызов метода из контекста объекта
  }
};

obj.callGreet(); // Ответ: "Hello, Michael!" (метод вызывается в контексте объекта obj)

Задача 10: Использование this в глобальной области видимости

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

logThis(); // Ответ: глобальный объект или undefined (в строгом режиме)

Итого

  • this в JavaScript это не контекст. Никогда им не был и не будет.

  • this в JavaScript это особый идентификатор, который определен локально для всех normal function и по умолчанию задан как undefined для “strict mode” или как global object для non strict mode.

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

  • Внешнее API может связать this с произвольным значением в зависимости мнения создателя API, но только в случае, если это будет normal function для которого мы не задали this методов call, bind, apply.

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

    Если мы не находимся внутри функции, то мы либо в скрипте, либо в модуле.

    Если мы в модуле, то это undefined

    Если в скрипте, читаем спецификацию по Host системе, которая может установить this так, как ей нужно.

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

    Если эта функция не стрелочная, то мы смотрим, как она вызывалась.

    Если эта функция стрелочная, то мы обращаем внимание на родительское окружение.

    Таким образом, если у нас функция не стрелочная, и мы смотрим, как она вызывалась. Дальше все просто: this задается явным образом при помощи трех методов - call, bind, apply.

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

    Следующий вариант изменить this - это ключевое слово new, которое все привыкли видеть как слово, которое, казалось бы, вызывает конструктор. Мы создаем в нем новые объекты, но с точки зрения спецификации ключевое слово new всего лишь вызывает функцию, внутри которой оно связывает this с пустым объектом.

    Если мы не видим ключевого слова new, мы дальше смотрим, происходит ли вызов функции в дот-нотации.

    Если вызов в дот-нотации, то this всегда будет равен тому, что стоит перед точкой.

    Если все эти пункты мы прошли, то this равен undefined или global object (strict mode / non strict mode).

  • Финальный алгоритм определения this в JavaScript выглядит следующим образом:

Пользуясь возможностью я хочу рассказать о своем YouTube канале Open JS. На этом канале я публикую обучающие ролики по JavaScript.

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


  1. space2pacman
    12.01.2024 15:12

    Кто там следующий в очереди постить статью про this? Я тоже хочу.


    1. Open-JS Автор
      12.01.2024 15:12

      Я специально посмотрел статьи про this на хабре. С подобным удобным алгоритмом поиска значения this статей нет. Многим разработчикам может пригодиться.


  1. Tasta_Blud
    12.01.2024 15:12
    -2

    м, this в javascript... наверное, это единственный язык, в котором this означает не исполняемый объект (за исключением замыканий, примем что они эфемерны), а может быть вообще всё что угодно, и вызывая из одного метода this.другойМетод, нельзя быть уверенным, что это сработает именно так, как ожидается: приходится использовать другие конструкции.

    з.ы. я, конечно, понимаю, что язык изначально объектный (не объектно-ориентированный) и задумывался как очень простой язык, но - это не повод.

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


  1. Alexandroppolus
    12.01.2024 15:12
    +3

    В итоговой блок-схеме неправильно обрабатывается редкий (и в общем-то бесполезный для "практического программирования", но тем не менее) кейс борьбы приоритетов между new и bind. На самом деле new - сильнее:

    (function() {
        'use strict';
        function f() {
            console.log(this.x);
        }
        f.prototype.x = 'proto';
        f.call({x: 'call'}); // call подставляет свой this
        const bound = f.bind({x: 'bound'});
        bound.call({x: 'call'}); // bind сильнее чем call
        new bound(); // new игнорирует bind
    })();

    В консоли будет call, bound, proto, причем независимо от наличия 'use strict'

    Философия ключевого слова super и связанного с ним "домашнего объекта" хорошо разобрана в Учебнике


  1. stepanov21
    12.01.2024 15:12

    Блок схема очень похожа на схему из видео, где Мурыч объясняет про this )


    1. Open-JS Автор
      12.01.2024 15:12

      Именно его канал я и указан в предисловие)


  1. Notactic
    12.01.2024 15:12

    Душно и бесполезно, но мне понравилось, спасибо :D


  1. MVMmaksM
    12.01.2024 15:12

    Спецификация, спецификация, спеуификация...