Что означает «this» в JavaScript
Рассказываем, как разобраться, что в JavaScript значит this
…
Объект this
в JavaScript — предмет многих шуток, поскольку разобраться с ним не так просто. Однако я видел, как разработчики прибегали к гораздо более сложным и специфичным для предметной области решениям — только чтобы не использовать this
. Если вам тоже не всегда понятно, что означает this
, надеюсь, моя статья прояснит этот вопрос. Будем считать, это мое руководство по this
.
Начнем с самой конкретной ситуации и постепенно перейдем к менее конкретным. Структура статьи напоминает if (…) … else if () … else if (…) …
, так что можете переходить к первому разделу, который соответствует нужному вам коду.
- Если функция определена как стрелочная функция.
- Иначе: если функция или класс вызываются с помощью
new
. - Иначе: если у функции есть «привязанное» значение
this
. - Иначе: если объект
this
задан во время вызова. - Иначе: если функция вызывается через родительский объект (
parent.func()
). - Иначе: если область действия функции или родительского объекта находятся в строгом режиме.
- Иначе.
Если функция определена как стрелочная функция #
const arrowFunction = () => {
console.log(this);
};
В этом случае значение this
всегда такое же, как у this
в области действия родительского элемента:
const outerThis = this;
const arrowFunction = () => {
// Всегда выводит `true`:
console.log(this === outerThis);
};
Стрелочные функции хороши тем, что внутреннее значение this
не изменяется: оно всегда такое же, как у внешнего this
.
Другие примеры #
В случае стрелочных функций значение this
нельзя изменить с помощью bind
:
// Выводит `true`: значение `this` привязки игнорируется:
arrowFunction.bind({foo: 'bar'})();
В случае стрелочных функций значение this
нельзя изменить с помощью call
или apply
:
// Выводит `true`: вызванное значение `this` игнорируется:
arrowFunction.call({foo: 'bar'});
// Выводит `true`: примененное значение `this` игнорируется:
arrowFunction.apply({foo: 'bar'});
В случае стрелочных функций значение this
нельзя изменить с помощью вызова функции как элемента другого объекта:
const obj = {arrowFunction};
// Выводит `true`: родительский объект игнорируется:
obj.arrowFunction();
В случае стрелочных функций значение this
нельзя изменить с помощью вызова функции как конструктора:
// TypeError: arrowFunction не является конструктором (TypeError: arrowFunction is not a constructor)
new arrowFunction();
«Привязанные» методы экземпляра #
Если нужно, чтобы объект this
всегда ссылался на экземпляр класса, лучше всего использовать стрелочные функции и поля класса:
class Whatever {
someMethod = () => {
// Всегда экземпляр класса Whatever:
console.log(this);
};
}
Такой подход удобен при использовании методов экземпляра в качестве прослушивателей событий в компонентах (например, в компонентах React или веб-компонентах).
Может показаться, что описанное выше нарушает правило «значение this
всегда такое же, как у this
в области действия родительского элемента», однако если представить поля класса как синтаксический сахар для подготовки значений в конструкторе, всё становится понятно:
class Whatever {
someMethod = (() => {
const outerThis = this;
return () => {
// Всегда выводит `true`:
console.log(this === outerThis);
};
})();
}
// …примерно соответствует следующему:
class Whatever {
constructor() {
const outerThis = this;
this.someMethod = () => {
// Всегда выводит `true`:
console.log(this === outerThis);
};
}
}
Среди альтернативных вариантов — привязка существующей функции в конструкторе и назначение функции в конструкторе. Если по какой-то причине использовать поля класса нельзя, удобной альтернативой будет назначение функции в конструкторе:
class Whatever {
constructor() {
this.someMethod = () => {
// …
};
}
}
Иначе: если функция или класс вызываются с помощью new
#
new Whatever();
В коде выше мы вызываем Whatever
(или функцию-конструктор, если это класс). В этом случае значение this
будет равно результату Object.create(Whatever.prototype)
.
class MyClass {
constructor() {
console.log(
this.constructor === Object.create(MyClass.prototype).constructor,
);
}
}
// Выводит `true`:
new MyClass();
Это же верно и для конструкторов в старом стиле:
function MyClass() {
console.log(
this.constructor === Object.create(MyClass.prototype).constructor,
);
}
// Выводит `true`:
new MyClass();
Другие примеры #
При вызове через new
значение this
нельзя изменить с помощью bind
:
const BoundMyClass = MyClass.bind({foo: 'bar'});
// Выводит `true`: значение `this` привязки игнорируется:
new BoundMyClass();
При вызове через new
значение this
нельзя изменить с помощью вызова функции как элемента другого объекта:
const obj = {MyClass};
// Выводит `true`: родительский объект игнорируется:
new obj.MyClass();
Иначе: если у функции есть «привязанное» значение this
#
function someFunction() {
return this;
}
const boundObject = {hello: 'world'};
const boundFunction = someFunction.bind(boundObject);
При каждом вызове boundFunction
значением ее this
будет объект, переданный в bind
(boundObject
).
// Выводит `false`:
console.log(someFunction() === boundObject);
// Выводит `true`:
console.log(boundFunction() === boundObject);
Другие примеры #
При вызове привязанной функции значение this
нельзя изменить с помощью call
или apply
:
// Выводит `true`: вызванное значение `this` игнорируется:
console.log(boundFunction.call({foo: 'bar'}) === boundObject);
// Выводит `true`: примененное значение `this` игнорируется:
console.log(boundFunction.apply({foo: 'bar'}) === boundObject);
При вызове привязанной функции значение this
нельзя изменить с помощью вызова функции как элемента другого объекта:
const obj = {boundFunction};
// Выводит `true`: родительский объект игнорируется:
console.log(obj.boundFunction() === boundObject);
Иначе: если объект this
задан во время вызова #
function someFunction() {
return this;
}
const someObject = {hello: 'world'};
// Выводит `true`:
console.log(someFunction.call(someObject) === someObject);
// Выводит `true`:
console.log(someFunction.apply(someObject) === someObject);
Значение this
— это объект, переданный в call
или apply
.
К сожалению, иногда другое значение this
может устанавливаться, например, прослушивателями событий DOM, в результате чего при использовании такого this
код становится тяжелым для понимания.
Неправильно
element.addEventListener('click', function (event) {
// Выводит `element`, поскольку спецификация DOM устанавливает для `this` значение,
// равное элементу, к которому прикреплен обработчик.
console.log(this);
});
В случаях вроде описанного выше я стараюсь избегать this
и использую такой подход:
Правильно
element.addEventListener('click', (event) => {
// Лучше всего — получить из родительской области действия:
console.log(element);
// Если это невозможно — из объекта события:
console.log(event.currentTarget);
});
Иначе: если функция вызывается через родительский объект (parent.func()
) #
const obj = {
someMethod() {
return this;
},
};
// Выводит `true`:
console.log(obj.someMethod() === obj);
В этом случае функция вызывается как элемент obj
, поэтому значение this
будет obj
. Это происходит во время вызова, поэтому если функция вызывается без родительского объекта или с другим родительским объектом, связь разрывается:
const {someMethod} = obj;
// Выводит `false`:
console.log(someMethod() === obj);
const anotherObj = {someMethod};
// Выводит `false`:
console.log(anotherObj.someMethod() === obj);
// Выводит `true`:
console.log(anotherObj.someMethod() === anotherObj);
Значение someMethod() === obj
— false
, потому что someMethod
не вызывается как элемент obj
. На такое можно наткнуться, если попытаться сделать что-то вроде такого:
const $ = document.querySelector;
// TypeError: запрещенный вызов (TypeError: Illegal invocation)
const el = $('.some-element');
Такое не срабатывает, потому что реализация querySelector
обращается к значению собственного this
и ожидает, что это будет что-то вроде узла DOM, а код выше разрывает эту связь. Правильный способ добиться желаемого:
const $ = document.querySelector.bind(document);
// Или:
const $ = (...args) => document.querySelector(...args);
Интересный факт: не все API используют this
во внутренних структурах. Консольные методы (например, console.log
), были изменены таким образом, чтобы избегать ссылок на this
, поэтому привязка log
к console
необязательна.
Иначе: если область действия функции или родительского объекта находятся в строгом режиме #
function someFunction() {
'use strict';
return this;
}
// Выводит `true`:
console.log(someFunction() === undefined);
В этом случае значение this
не определено. Выражение 'use strict'
в функции не требуется, если родительская область действия находится в строгом режиме (и все модули — тоже).
Иначе #
function someFunction() {
return this;
}
// Выводит `true`:
console.log(someFunction() === globalThis);
В этом случае значение this
такое же, как у globalThis
.
Уф! #
Ну вот! Это всё, что я знаю об объекте this
. Есть вопросы? Я что-то пропустил? Смело пишите мне в Twitter.
Огромное спасибо Матиасу Байненсу, Ингвару Степаняну и Томасу Штейнеру за проверку статьи.