Очень популярный вопрос в моей менторской практике: что такое this и как оно работает? Давайте так: я возьму и сразу расскажу, что это такое.

У функции в JS есть контекст выполнения. Так вот, this — и есть этот самый контекст выполнения :) стало понятнее? Вот-вот. Давайте разбираться подробнее.

Как мы можем выполнить функцию в JavaScript? Есть всего четыре способа:

  • напрямую вызвав функцию: func();

Вызвав функцию напрямую в нестрогом режиме (о котором стоит сразу забыть и никогда не использовать), this будет равен глобальному объекту global (или window, если мы о браузере, но мы тут все же про Node.js больше). Если же вызвать функцию напрямую в строгом режиме, то this у этой функции будет равен undefined. Вот так все просто.

  • вызвав метод объекта: obj.func();

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

  • вызвав функцию-конструктор, создав объект, для чего у нас есть ключевое слово new:
function Book(name) {
  this.name = name;
}

const book = new Book('Bible');
book.name === 'Bible'; // true

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

function Book(name) {
  // this = {} (this создается как некий объект)
 // return this (когда мы создаем объект конструктором, мы, собственно, и возвращаем этот самый this
}

Как-то так. Все еще не слишком сложно, если вглядеться.

Разберем последний вариант вызова функции — непрямой ее вызов. Тут мы встретим три новых метода: bind(), call() и apply().

Собственно, что мы можем с ними сделать? А сделать мы можем довольно интересную штуку — назначить контекст выполнения функции явно. Глядите:

function logNameEmailAndBirthDate(birthDate) {
  console.log(`${this.name}, ${this.email}, ${birthDate}`);
}

const user1 = { name: 'Ivan', email: '[email protected]' };
const user2 = { name: 'Petr', email: '[email protected]' };

logNameEmailAndBirthDate.call(user1, '1 Oct 1991'); // this === user1
logNameEmailAndBirthDate.apply(user2, ['3 Feb 1993']); // this === user2

Что здесь происходит? А все просто: и call(), и apply() первым аргументом принимают this для функции, которую мы через эти call(), и apply() не напрямую вызываем. То есть в первом вызове logNameEmailAndBirthDate() (через call()) this === user1, во втором (через apply()) this === user2. Единственная разница между call() и apply() — первый принимает аргументы для функции через запятую, второй — массивом.

А метод bind() делает похожую штуку, но — немного иначе. Смотрите:

function logNameAndEmail() {
  console.log(`${this.name}: ${this.email}`);
}

const bob = { name: 'Bob', email: '[email protected]' };
const logNameAndEmailForBob = logNameAndEmail.bind(bob);

logNameAndEmailForBob(); // Bob: [email protected]

Что здесь происходит? Мы вызываем bind() для logNameAndEmail(), который привязывает контекст (bob) к logNameAndEmail(), возвращая новую функцию (в отличие от call(), и apply()), связанную с этим контекстом навсегда — изменить его потом не получится.

Стрелочные функции

Отдельно стоит упомянуть стрелочные функции (const func1 = () => {}) — у них своего контекста выполнения, своего this попросту нет. Это работает так, будто они — результат выполнения bind() c ближайшим по иерархии контекстом.

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

Каррирование

Один из классных примеров можно найти в функциональном программировании — каррирование. Каррирование — это трансформация функций так, чтобы они принимали значения не так: func (a, b, c), а так: func(a)(b)(c).

Зачем это нужно? Давайте рассмотрим это на примере функции, суммирующей все свои аргументы. Вы скажете, что для этого можно сделать так:

function sumAll (...spr) {
  return spr.reduce((partialSum, a) => partialSum + a, 0);
}

И — да, так можно. Но мы все же возьмем это как пример для каррирования, тем более, что он будет не единственным. Давайте напишем ту самую трансформирующую функцию curry(), используя наши знания о методе apply():

function sum(a, b, c) {
  return a + b + c;
}

function curry (func) {
  return function curried(...args) {
    if (args.length >= func.length) {
      return func.apply(this, args);
    } else {
      return function(...args2) {
        return curried.apply(this, args.concat(args2));
      }
    }
  }
};

const curriedSummer = curry(sum);

// 6, можем просто вызвать функцию частично
console.log( curriedSummer(1, 2, 3) );
// все еще 6, мы прокаррировали первый аргумент
console.log( curriedSummer(1)(2,3) );
// и тут 6, мы прокаррирровали все аргументы
console.log( curriedSummer(1)(2)(3) );

Функция curry(func) выглядит сложновато — давайте разберем ее. Она возвращает функцию curried(), принимающую неограниченное количество аргументов. Что она делает. Она:

  • если количество переданных в curried() аргументов больше или равно количеству аргументов при объявлении функции func() (в примере ниже sum()), то мы просто вызываем функцию func() , используя apply()
  • если же количество переданных аргументов меньше количества аргументов при объявлении функции, мы не выполним func() сразу: вместо этого мы вернем новую функцию, которая снова применит curried, передав предыдущие аргументы вместе с новыми. И так будет происходить до тех пор, пока аргументов будет достаточно для вызова func(), которую мы и вызовем.

Пример с суммированием надуман, конечно. Но есть известный пример с функцией для логирования. Смотрите: у нас есть вот такая вот функция:

function log(level, date, message) {
  console.log(`[${date.getHours()}:${date.getMinutes()}] [${level}] ${message}`)
}

// и этим даже можно пользоваться:

log("DEBUG", new Date(), 'debug message')
// [23:33] [DEBUG] debug message

Но что, если мы применим к ней каррирование?

const curriedLog = curry(log);

// после этого мы сможем пользоваться ей, как раньше:
curriedLog("DEBUG", new Date(), 'debug message')
// [23:33] [DEBUG] debug message

// но мы можем сделать и так:
curriedLog("DEBUG")(new Date(), 'debug message')
// [23:33] [DEBUG] debug message

Пока вообще не понятно, зачем мы это все затеяли. Но смотрите, что полезного мы можем из этого вывести:

const debugLog = curriedLog("DEBUG");

debugLog(new Date(), 'debug message');
// [23:33] [DEBUG] debug message

Видите? С помощью каррирования первого аргумента мы можем для разных уровней логирования создать функции debugLog(), errorLog() и так далее, и нам не придется постоянно передавать level логирования в функцию! Мы можем даже пойти дальше и откаррировать второй аргумент, получив функцию типа debugLogForCurrentDate(message), в которую даже дату передавать не нужно! Этот пример кажется надуманным, но можно найти этому множество гораздо более полезных применений.

Итоги

Гибкость контекста выполнения функции в JS делает его очень гибким языком, позволяя делать очень многие штуки, которые в других, менее гибких языках так красиво просто не делаются. И, в общем-то, this — это не очень-то и сложно :)