В огромном мире JavaScript ключевое слово this часто ставит разработчиков в тупик, как новичков, так и опытных профессионалов.
Эта статья направлена на то, чтобы демистифицировать эту важнейшую концепцию, углубившись в ее тонкости, объяснив ее поведение в различных контекстах и, наконец, продемонстрировав, как манипулировать ее значением для конкретных случаев использования.
Основы «этого»
По своей сути ключевое слово this в JavaScript относится к объекту, которому оно принадлежит. Однако его значение определяется тем, как вызывается функция, а не тем, где она объявлена.
Вот разбивка типичных правил для определения того, на что ссылается this
:
Привязка по умолчанию
В наиболее распространенном случае стандартный вызов функции this
ссылается на глобальный объект.В браузере глобальным объектом являетсяwindow
. Однако в строгом режиме это значение остается равным undefined
.
function demoFunction() { console.log(this); } demoFunction(); // logs: Window {...} // (or `undefined` if in Strict Mode*) // strict mode forces developer to be explicit about // the context in which they are calling a function, hence undefined
Неявное связывание
Когда функция вызывается как метод объекта, this
привязывается к объекту, для которого вызывается метод.
const obj = { name: 'John', sayName: function() { console.log(this.name); } }; obj.sayName(); // logs: "John"
Явная привязка
Вы можете установить значение this
явно с помощью call()
, apply()
или bind()
. Эти методы позволяют вам напрямую указать, каким должен быть this
.
function sayName() { console.log(this.name); } const person1 = { name: 'Alice' }; const person2 = { name: 'Bob' }; sayName.call(person1); // logs: "Alice" sayName.call(person2); // logs: "Bob"
Новая привязка
Когда функция вызывается с ключевым словом new
, JavaScript рассматривает ее как конструктор. В этом случае this
привязывается к создаваемому новому объекту.
function Person(name) { this.name = name; } const alice = new Person('Alice'); console.log(alice.name); // logs: "Alice"
Стрелочные функции
Стрелочные функции в ES6 не имеют собственной привязки
this
.
Вместо этого они наследуютthis
от объемлющей (внешней) лексической области видимости.
Это означает, что стрелочные функции не имеют собственных this
. Вместо этого они «захватывают» значение this
из окружающего кода функции, не являющейся стрелкой («окружающий контекст выполнения»).
function outerFunction() { this.value = 42; return () => console.log(this.value); } const obj = { value: 99, myFunction: outerFunction() }; obj.myFunction(); // logs: 42, not 99javascriptCopy code
Слушатели событий
Внутри прослушивателей событий this
обычно относится к элементу, к которому было подключено событие.
const button = document.querySelector('button'); button.addEventListener('click', function() { console.log(this); // logs: <button>...</button> });
Хотя эти правила предлагают общее руководство для понимания this
, они могут пересекаться в определенных сценариях, что затрудняет прогнозирование их значения.
В частности, поведение this
отличается от традиционной функции по отношению к стрелочной функции, поэтому давайте углубимся в это и сразу поймем различия, чтобы
Традиционная функция против стрелочных функций
Традиционные функции и функции со стрелками обрабатывают ключевое слово `this` по-разному.
В традиционных функциях значение this
может быть непредсказуемым в зависимости от того, как вызывается функция (как метод, конструктор, простой вызов функции и т. д.). Такое поведение является частым источником ошибок.
Для стрелочных функций this
связан лексически или статически. Это означает, что стрелочные функции не имеют собственных this
. Вместо этого они «захватывают» значение this
из окружающего кода функции, отличной от стрелки. Функция стрелки гарантирует, что this
является предсказуемым и используется правильный контекст.
Разберемся в разнице с помощью разных реализаций.
1. Простой вызов функции
Когда функция вызывается вне какого-либо конкретного контекста объекта, значение this
обычно относится к глобальному объекту (window
в браузерах и global
в Node.js). Однако в строгом режиме this
будет undefined
.
Стрелочные функции захватывают this
из окружающего контекста
function traditionalFunction() { console.log(this); } traditionalFunction(); // logs: `window` in browsers (or `undefined` in strict mode) const anotherObj = { method: traditionalFunction }; anotherObj.method(); // logs: `anotherObj` const arrowFunction = () => { console.log(this); }; const anotherObj = { method: arrowFunction }; anotherObj.method(); // logs: `window` in browsers, because that's the // surrounding context in which `arrowFunction` was defined.
2.Обратные вызовы
Обычный вариант использования стрелочных функций — внутри обратных вызовов, где поведение this
часто может приводить к ошибкам:
С традиционной функцией:
function Timer() { this.seconds = 0; setInterval(function() { this.seconds++; console.log(this.seconds); }, 1000); } const timer = new Timer(); // NaN gets logged every second, // because `this` inside the `setInterval` refers to `window` // or is `undefined` in strict mode.
Использование функции стрелки
function BetterTimer() { this.seconds = 0; setInterval(() => { this.seconds++; console.log(this.seconds); }, 1000); } const betterTimer = new BetterTimer(); // Logs increasing numbers every second // because the arrow function captures the `this` of `BetterTimer`.
3. Распространенные ошибки
До появления стрелочных функций разработчики часто использовали обходные пути для получения правильного this
function YetAnotherTimer() { this.seconds = 0; var self = this; // workarounds setInterval(function() { self.seconds++; console.log(self.seconds); }, 1000); } const yetAnotherTimer = new YetAnotherTimer(); // Logs increasing numbers every second, // because we stored `this` in `self`.
Стрелочные функции устраняют необходимость в таком обходном пути.
function YetAnotherTimer() { this.seconds = 0; setInterval(() => { this.seconds++; console.log(this.seconds); }, 1000); } const yetAnotherTimer = new YetAnotherTimer();
4. Функция конструктора
Когда в качестве конструктора используется традиционная функция (с ключевым словом new
), this
относится к вновь созданному экземпляру.
Функции со стрелками нельзя использовать в качестве конструкторов
function Person(name) { this.name = name; this.displayThis = function() { console.log(this); }; } const john = new Person('John'); john.displayThis(); // logs the newly created instance (`john`) // ----------------------------------> const ArrowConstructor = () => {}; // This will throw an error. // const instance = new ArrowConstructor();
4. Вложенные функции
Внутри метода, если есть вложенная традиционная функция, this
в этой вложенной функции не наследует this
окружающего метода. Вместо этого он ведет себя как простой вызов функции
const group = { title: "My Group", members: ["John", "Lena", "Chris"], showMembers: function() { this.members.forEach(function(member) { // `this` inside this function doesn't refer to `group` console.log(`${this.title}: ${member}`); }); } }; group.showMembers(); // logs "undefined: John", "undefined: Lena", "undefined: Chris"
Стрелочные функции не привязывают свои собственные `this`. Они захватывают значение `this` из окружающего контекста.
const obj = { value: "Hello", show: () => { console.log(this.value); } }; obj.show(); // logs: `undefined` because `this` refers to the global context.
5. Манипулирование «этим» с помощью «вызова», «применения» и «привязки»
Эти методы позволяют разработчикам явно указывать, на что должно ссылаться это значение.
вызов
Метод call вызывает функцию с заданным значением this и аргументами.
function describe(prefix, suffix) { console.log(prefix + this.value + suffix); } const objA = { value: "apple" }; const objB = { value: "banana" }; describe.call(objA, "This is an ", "."); // Outputs: "This is an apple." describe.call(objB, "Here's a ", "!"); // Outputs: "Here's a banana!"
применять
.apply()
очень похож на .call()
, но вместо отдельных аргументов он принимает массив (или подобный массиву объект) аргументов. Это полезно, когда у вас есть список аргументов, которые нужно передать функции и вызвать ее в определенном контексте.
function addNumbers(a, b, c) { return this.base + a + b + c; } const obj = { base: 10 }; const result = addNumbers.apply(obj, [1, 2, 3]); console.log(result); // Outputs: 16
связывать
.bind()
возвращает новую функцию с привязанным к ней определенным значением this
, но не выполняет процесс. Это особенно полезно при создании функции с предопределенным контекстом для последующего использования. Он обычно используется с прослушивателями событий или когда вы хотите, чтобы функция сохраняла определенный контекст независимо от того, где и как она вызывается.
const obj = { value: 42, getValue: function() { return this.value; } }; const unboundGetValue = obj.getValue; console.log(unboundGetValue()); // undefined or throws error in strict mode const boundGetValue = obj.getValue.bind(obj); console.log(boundGetValue()); // 42
Эти методы предлагают разработчикам гибкость в зависимости от их конкретных потребностей:
- Хочу ли я создать новую функцию с привязанным контекстом для последующего использования? Используйте
.bind()
. - Хочу ли я вызвать функцию с определенным контекстом и отдельными аргументами прямо сейчас? Используйте
.call()
. - Хочу ли я вызвать функцию с определенным контекстом и массивом аргументов прямо сейчас? Используйте
.apply()
.
Эти отдельные методы обеспечивают большую гибкость при работе с функциями и контекстами их выполнения в JavaScript.
Заключение
Ключевое слово this — это динамическая функция JavaScript, которая корректирует свое значение в зависимости от контекста, в котором вызывается функция. Хотя поначалу это может показаться сложным, понимание его поведения в различных сценариях имеет решающее значение для написания эффективного JavaScript.
Независимо от того, определяете ли вы методы для объекта, работаете с конструкторами или явно устанавливаете контекст с помощью `call`, `apply` или `bind`, твердое понимание `this`, несомненно, поднимет вашу игру на JavaScript.