Что такое this, bind, call и apply в JavaScript



В статье расскажу про ключевое слово this
, как меняется его значение в зависимости от контекста, про виды этого контекста, и про то, как задать контекст в явном виде вручную.
Что есть this
В JavaScript this — это ссылка на объект. Объект, на который ссылается this, может меняться следующими способами:
- неявно (Implicit), в зависимости от того, является ли он глобальным, объявленным объектом или в конструктором,
- явно (Explicit), с помощью методов прототипа функции
bind
,call
иapply
.
Далее вы узнаете, на что this
ссылается неявно в зависимости от контекста. Научитесь использовать bind
, call
, и apply
методы, чтобы явно задавать значение this
.
Неявный (implicit) контекст
Перечислю четыре основных контекста, в которых this принимает значение неявным образом:
- глобальный контекст
- метод внутри объекта
- конструктор функции или класса
- DOM обработчик событий
1. Глобальный контекст
В глобальном контексте this
ссылается на глобальный объект. Если вы работаете в браузере — глобальным объектом будет window
. Если работаете с Node.js, глобальный контекст будет global
(или undefined
в strict mode
).
Откройте консоль через панель разработчика в браузере и введите команду:
console.log(this);
// Window {postMessage: ƒ, blur: ƒ, focus: ƒ, close: ƒ, parent: Window, …}
Вы увидите, что this в качестве значения содержит Window, глобальный объект браузера.
В Node.js strict mode результат будет другим:
console.log(this); // undefined
Если мы напишем функцию верхнего уровня, никак не связанную ни с каким объектом, this
внутри функции по-умолчанию примет значение глобального объекта:
function globalFunc() {
return this;
}
console.log(globalFunc()); // Window или undefined (strict mode) в браузере, global или undefined в Node.js.
В моих статьях про переменные и замыкания вы могли узнать, что у функций есть собственный контекст для переменных. this
работает иначе — для функции, объявленной в глобальной области видимости, он примет значение глобального объекта.
2. Метод внутри объекта
Если мы используем обычную (не стрелочную) функцию в качестве метода объекта, this в качестве значения примет сам объект.
const globalObj = {
type: "global",
objMethod() {
return this;
},
};
console.log(globalObj.objMethod()); // { type: 'global', objMethod: [λ: objMethod] }
console.log(globalObj.objMethod() === globalObj); // true
Во вложенном объекте this
ссылается на текущую области видимости метода:
const multiLvlObj = {
type: "global",
details: {
id: 1,
secondLvl() {
return this;
},
},
firstLvl() {
return this;
},
};
console.log(multiLvlObj.firstLvl());
/*
{ type: 'global',
details: { id: 1, secondLvl: [λ: secondLvl] },
firstLvl: [λ: firstLvl] }
*/
console.log(multiLvlObj.details.secondLvl()); // { id: 1, secondLvl: [λ: secondLvl] }
Иными словами, this
ссылается на объект, находящийся слева от точки при вызове метода.
3. Конструктор функции и класса
Когда вы используете ключевое слово new
, оно создает экземпляр функции-конструктора или класса. Конструкторы функций были стандартным способом инициализации определяемого пользователем объекта до появления синтаксиса классов в обновлении ECMAScript 2015 (ES6) в JavaScript.
function User(name, yearOfBirth) {
console.log(this); // User {}
this.name = name;
this.yearOfBirth = yearOfBirth;
this.describe = function () {
console.log(`${this.name} родился в ${this.yearOfBirth}г.`);
};
console.log(this
); // User {name: 'Иван', yearOfBirth: 1991, describe: ƒ}
}
const ivan = new User("Иван", 1991);
ivan.describe(); // Иван родился в 1991г.
В данном контексте this
в качестве значения принимает экземпляр User
, который содержится в константе ivan
.
Конструктор класса действует так же, как и конструктор функции.
class Country {
constructor(name, yearFounded) {
this.name = name;
this.yearFounded = yearFounded;
}
describe() {
console.log(`${this.name} была основана в ${this.yearFounded}г.`);
}
logThis(){
console.log(this)
}
}
const russia = new Country("Россия", 862);
russia.describe(); // Россия была основана в 862г.
russia.logThis(); // Country {name: 'Россия', yearFounded: 862}
this
в качестве значения принимает экземпляр Country
, который содержится в константе russia
.
4. DOM обработчик событий
В браузере существует специальный контекст this
для обработчиков событий. В обработчике, который мы вызываем с помощью addEventListener
, this
ссылается на event.currentTarget
.
В реальности, разработчики просто используют event.target
или event.currentTarget
по мере необходимости, но полезно понимать, как меняется ссылка this
в таком контексте.
В следующем примере создадим кнопку, добавим текст, вставим в DOM
. Когда нажмем на кнопку, увидим значение this
, которое выведет целевой HTML-элемент.
const button = document.createElement('button')
button.textContent = 'Нажми на меня'
document.body.append(button)
button.addEventListener('click', function(event) {
console.log(this) // <button>Нажми на меня</button>
})
Неявный контекст обсудили, перейдем к следующей теме — Explicit Context.
Явный (Explicit) контекст
Во всех предыдущих примерах значение параметра this
определялось его контекстом — глобальная область видимости, метод объекта, конструктор, обработчик событий. С помощью методов call
, apply
или bind
, можно в явном виде определить, на что должен ссылаться this
.
Call и Apply
Это методы, которые позволяют вызвать функцию явно заданным значением this
.
const manager = {
id: 1,
salary: 100500
};
function logThis(){
console.log(this);
}
logThis(); // window
logThis.call(manager); // {id:1, salary: 100500}
logThis(); // window
Тот же результат получим с методом apply:
logThis.apply(manager); // {id: 1, salary: 100500}
Разница между call и apply
Оба этих метода вызывают функцию с заданным значением this
. Оба этих метода могут принимать дополнительные аргументы:
const manager = {
id: 1,
salary: 100500
};
function logEmployeeInfo(name, age){
console.log(`${name} is ${age} years old and earns ${this.salary} a year`)
}
logEmployeeInfo.call(manager, 'Peter', 33);
// Peter is 33 years old and earns 100500 a year
Разница лишь в том, в каком виде представлены эти аргументы. В call()
они передаются отдельно, а для apply()
передаются в качестве массива:
const manager = {
id: 1,
salary: 100500
};
function logEmployeeInfo(name, age){
console.log(`${name} is ${age} years old and earns ${this.salary} a year`)
}
logEmployeeInfo.apply(manager, ['Peter', 33]);
// Peter is 33 years old and earns 100500 a year
Использование call и apply
Помните, какое значение принимает this
в контексте, когда мы используем конструктор функцию, которую вызываем с ключевым словом new
?
function Laptop(screenSize, resolution){
this.screenSize = screenSize;
this.resolution = resolution;
console.log(this); // ???
}
const myLaptop = new Laptop('2560x1600', '13,3"');
this
принимает значение экземпляра, созданного с помощью этого конструктора:
function Laptop(screenSize, resolution){
// omitted
console.log(this); // Laptop {screenSize: '2560x1600', resolution: '13,3"'}
}
const myLaptop = new Laptop('2560x1600', '13,3"');
А теперь создадим еще один конструктор, который позволяет указать бренд ноутбука:
function Laptop(screenSize, resolution){
this.screenSize = screenSize;
this.resolution = resolution;
console.log(this);
}
function setBrand(brand){
this.brand = brand;
Laptop.call(this, '2560x1600', '13,3"');
console.log(this);
}
const myMac = new setBrand('Apple');
// setBrand {brand: 'Apple', screenSize: '2560x1600', resolution: '13,3"'}
Что здесь происходит:
- Вызываем конструктор
setBrand
. Для конструктора значениеthis
= экземпляр созданного объекта; - Создаем новое свойство объекта
this.brand
, значение которого получаем в качестве аргумента при вызове конструктора; - Вызываем конструктор-функцию
Laptop
, передавая ей экземпляр объекта созданногоsetBrand
. Сейчас это{brand: 'Apple'}
- Добавляем новые свойства этому экземпляру. Их значения мы передали в качестве аргументов с помощью метода
call
- В результате получаем объект со свойствами
brand, screenSize, resolution
Если переписать функцию с использованием apply, получится так:
function setBrand(brand){
this.brand = brand;
Laptop.apply(this,['2560x1600', '13,3"']);
console.log(this);
}
Это небольшой пример, иллюстрирующий использование методов call
/apply
. Покажу еще несколько примеров.
Вызвать метод, использовав свойства другого объекта:
const person = {
name: "John",
greet(greeting) {
console.log(`${greeting}, ${this.name}!`);
},
};
const anotherPerson = {
name: "Михаил",
};
person.greet.call(anotherPerson, "Hello"); // Hello, Михаил!
Преобразование массивоподобного объекта в массив:
function toArray(){
// arguments — массивоподобная структура, которая передается в качестве значения this метода slice
return [].slice.call(arguments)
}
toArray(1, 2, 3, 4, 5);
// после выхода ES6 проще сделать массив с помощью rest ...args
// const toArray = (...args) => args;
Bind
И call
, и apply
являются одноразовыми методами. Если вы вызовете метод с контекстом this
, он будет иметь его, но исходная функция останется неизменной.
Иногда вам может понадобиться использовать метод снова и снова с контекстом this другого объекта. В этом случае пригодится bind
для создания совершенно новой функции с явно связанным this
.
Рассмотрим такой пример:
const book = {
title: 'Трилогия желания',
author: 'Теодор Драйзер',
}
function summary() {
console.log(`Название: ${this.title}, автор: ${this.author}.`)
}
summary.call(book); // this получает значение book на момент вызова
// Output: Название: Трилогия желания, автор: Теодор Драйзер.
Что если мы часто обращаемся к резюме конкретно этой книги? Можем создать новую функцию, для которой к значению this
всегда привязана эта книга.
const book = {
title: 'Трилогия желания',
author: 'Теодор Драйзер',
}
function summary() {
console.log(`Название: ${this.title}, автор: ${this.author}.`)
}
const summaryOfTrilogy = summary.bind(book);
summaryOfTrilogy(); // Название: Трилогия желания, автор: Теодор Драйзер.
summaryOfTrilogy будет всегда возвращать значение, которое мы привязали к ней с помощью bind. Попытка привязать новое значение будет безуспешной, поэтому этой функции можно доверять, т.к. она всегда вернет ожидаемое значение this.
const book = {
title: 'Трилогия желания',
author: 'Теодор Драйзер',
}
function summary() {
console.log(`Название: ${this.title}, автор: ${this.author}.`)
}
const summaryOfTrilogy = summary.bind(book);
const book2 = {
title: 'Американская трагедия',
author: 'Теодор Драйзер',
}
summaryOfTrilogy.call(book2); // Название: Трилогия желания, автор: Теодор Драйзер.
Про то, как работает this в стрелочной функции, почитайте в моей статье про функции.
Итого
- значение this меняется в зависимости от контекста
- контекст может задаваться явно и не явно
- неявный контекст, который меняет значение this для функции — когда она объявлена глобально, в качестве метода объекта, в виде конструктора, DOM-обработчика событий
- явный контекст задается с помощью call, apply и bind. Первые два метода срабатывают при вызове и отличаются лишь форматом дополнительных аргументов, а bind позволяет навсегда установить значение this.
- для стрелочных функций есть свои нюансы — в качестве значения принимается объект не текущей области видимости, а на уровень выше.