Если вас смущает ключевое слово this в JavaScript, можете не переживать - оно вначале всех сбивает с толку. Однако это не означает, что без знания this можно дальше продолжать спокойно себе работать. this так часто используется в JavaScript и во всех учебных пособиях, что вам рано или поздно придется его освоить. И как только вы поймете this, вы осознаете, что все намного проще, чем вы думали.

К концу этой статьи вы будете знать, что такое this, для чего оно нужно и как его использовать.

Так что такое this

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

  • в глобальном контексте
  • в конструкторе объекта
  • в методе объекта
  • в простой функции
  • в стрелочной функции
  • в слушателе событий

Давайте рассмотрим, как this меняется в каждом из шести контекстов.

this в глобальном контексте

Когда this вызывается вне какой-либо функции в глобальной области видимости, в качестве его значения, по-умолчанию, используется объект window в браузере.

console.log(this)  // => window

this в глобальном контексте

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

this в конструкторе объекта

Когда вы создаете новый экземпляр объекта с ключевым словом new, this ссылается на экземпляр.

function Friend(name) {
  this.name = name
}
let ivan = new Friend('Иван')
let vasya = new Friend('Вася')

console.log(ivan)  // {name: 'Иван'}
console.log(vasya)  // {name: 'Вася'}

this в конструкторе объекта

Как видим, ivan является экземпляром Friend в приведенном выше коде. И теперь всякий раз, когда вы ссылаетесь на ivan, вы не получите случайным образом vasya. Устанавливать this, ссылающийся на экземпляр, имеет смысл.

this в методе объекта

Метод - это модный термин для функции, связанной с объектом, например:

let object = {
  aMethod() { ... }  // это метод
}

this в любом методе ссылается на сам объект:

let object = {
  aMethod() {
    console.log(this)
  }
}

object.aMethod()  // => object

this в методе объекта

Поскольку this относится к объекту, вы можете использовать методы для получения экземпляра объекта, например:

function Friend(name) {
  return {
    name,
    getName(){
      return this.name
    }
  }
}
const ivan = new Friend('Иван')
const vasya = new Friemd('Вася')

console.log(ivan.getName())  // => 'Иван'

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

this в простой функции

Простые функции - это функции, которые вы наверняка очень хорошо знаете (как показано ниже в примере). Анонимные функции, написанные в той же форме, также считаются простыми функциями.

function hello() {
  ...
}

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

function simpleFunction() {
  console.log(this)
}

const object = {
  method() {
    simpleFunction()
  }
}

simpleFunction()  // => window
object.method()  // => window

К сожалению, изменение this неожиданно для новичков. Ведь действительно, на первый взгляд кажется, что this в функции, вызываемой в методе method() должно быть неизменным и ссылаться на объект.

Чтобы понять, почему так происходит, рассмотрим следующий код. Здесь, функция this.hello выполняется позже в функции setTimeout.

const object = {
  doSomethingLater() {
    setTimeout(function() {
      this.hello()  // Ошибка
    }, 1000)
  },
  hello() {
    console.log('Привет Мир!')
  }
}

Приведенный выше код приведет к ошибке. Ошибка возникает из-за того, что в функции setTimeout значение this равно window, а в window нет метода hello.

Один из быстрых фиксов - создать переменную, в которой хранится ссылка на this. Такую переменную часто именуют self или that.

const object = {
  doSomethingLater() {
    const self = this;
    setTimeout(function() {
      self.hello()  // => 'Привет Мир!'
    }, 1000)
  },
  hello() {
    console.log('Привет Мир!')
  }
}

Второй способ решить эту проблему - использовать новые стрелочные функции ES6, о которых написано ниже.

this в стрелочной функции

this в стрелочной функции всегда такое же, ка this вокруг нее (т.е. в ее непосредственной области видимости). Таким образом, если вы используете стрелочные функции в методе объекта, контекст this остается объектом, а не window.

С помощью стрелочных функций пример выше можно было бы записать следующим образом:

const object = {
  doSomethingLater() {
    setTimeout(() => this.hello(), 1000)
  },
  hello() {
    console.log('Привет Мир!')
  }
}

Третий способ изменить значение this в любой функции - использовать либо bind, либо call, либо apply. Рассмотрим bind чуть позже.

this в слушателе событий

В слушателе событий this ссылается на элемент, который инициировал событие:

let button = document.querySelector('button')

button.addEventListener('click', function() {
  console.log(this)  // button
})

При создании более сложных компонентов вы можете создать слушатели событий внутри методов:

function Foo(elem) {
  return {
    listenClick() {
      elem.addEventListener('click', function () {
        ...
      })
    }
  }
}

Поскольку this ссылается к элементу в слушателе событий, то если вам нужно активировать другой метод в этом слушателе, для него необходимо создать ссылку на объект:

function Foo(elem) {
  return {
    listenClick() {
      const self = this;
      elem.addEventListener('click', function () {
        self.hello()
      })
    },
    hello() { console.log('Привет Мир!') }
  }
}

В качестве альтернативы вы можете использовать стрелочную функцию. В этом случае вы все равно сможете получить элемент с помощью event.currentTarget.

function Foo(elem) {
  return {
    listenClick() {
      elem.addEventListener('click', (e) => {
        console.log(e.currentTarget)  // элемент, на котором произошло событие
        this.hello()
      })
    },
    hello() { console.log('Привет Мир!') }
  }
}

Однако оба способа недостаточно хороши, если вам, к примеру, нужно будет удалить слушатель события, поскольку функции анонимны.

Чтобы удалить слушатель события, обратный вызов, переданный в качестве второго параметра, должен быть именованной функцией:

function helloFunction() {
  console.log('Привет Мир!')

  // Удаляем слушатель
  document.removeEventListener('click', helloFunction)
}

document.addEventListener('click', helloFunction)

Если же вам нужен this в качестве ссылки на объект в слушателе событий, вам нужно использовать bind, чтобы уже вручную создать контекст this.

function Foo(elem) {
  return {
    listenClick() {
      this.listener = this.hello.bind(this)
      elem.addEventListener('click', this.listener)
    },
    hello(e) {
      const elem = e.currentTarget;
      console.log('Привет Мир!');
      elem.removeEventListener('click', this.listener)
    }
  }
}

Приведенный выше код может запутать вас, если вы не понимаете bind. Давайте разберемся, что он делает.

Изменение this с помощью bind

bind - это метод, имеющийся в каждой функции. Он позволяет нам изменять контекст this. Этот метод принимает любое количество аргументов и возвращает связанную функцию.

function showThis() {
  console.log(this)
}
const boundFunc = showThis.bind( ... )

Первый параметр, который вы передаете в bind, становится this в связанной функции. Создав такую функцию, вы можете вызывать ее в любое время:

function showThis() {
  console.log(this)
}
const boundFunc = showThis.bind({name: 'Иван'});
boundFunc()  // => {name: 'Иван'}

Другие параметры, которые вы передаете в bind, будут переданы в качестве аргументов исходной функции.

function showParams(...args) {
  console.log(...args)
}
const boundFunc = showParams.bind(null, 1, 2, 3);
boundFunc()  // => 1, 2, 3

Примечание - bind не работает со стрелочными функциями.

Теперь давайте вернемся к коду с слушателями событий выше и проанализируем, что происходит:

function Foo(elem) {
  return {
    listenClick() {
      // связываем this.hello с ссылкой на экземпляр
      // устанавливаем привязанную функцию к this.listener, чтобы удалить ее позже
      this.listener = this.hello.bind(this)

      // добавляем слушатель события
      elem.addEventListener('click', this.listener)
    },
    hello(e) {
      // получаем элемент, чтобы удалить слушатель события
      const elem = e.currentTarget;

      // выполняем тело функции
      console.log('Привет Мир!');

      // удаляем слушатель события
      elem.removeEventListener('click', this.listener)
    }
  }
}