DevSurge 💦

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

Cover Image for Что такое this, bind, call и apply в JavaScript
Mark Nelyubin
Mark Nelyubin

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

Что есть this

В JavaScript this — это ссылка на объект. Объект, на который ссылается this, может меняться следующими способами:

  1. неявно (Implicit), в зависимости от того, является ли он глобальным, объявленным объектом или в конструктором,
  2. явно (Explicit), с помощью методов прототипа функции bind, call и apply.

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

Неявный (implicit) контекст

Перечислю четыре основных контекста, в которых this принимает значение неявным образом:

  1. глобальный контекст
  2. метод внутри объекта
  3. конструктор функции или класса
  4. 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.
  • для стрелочных функций есть свои нюансы — в качестве значения принимается объект не текущей области видимости, а на уровень выше.

Другие материалы

Cover Image for TypeScript Utility Types — вспомогательные типы и области их применения

TypeScript Utility Types — вспомогательные типы и области их применения

что такое Utility Types в TypeScript, расскажу про основные вспомогательные типы, и покажу, как применять их на реальных проектах.

Mark Nelyubin
Mark Nelyubin
Cover Image for Принципы SOLID с примерами на JS и Vue

Принципы SOLID с примерами на JS и Vue

Расскажу про принципы SOLID с актуальными примерами на JavaScript, Vue, React.

Mark Nelyubin
Mark Nelyubin