this: контекст выполнения функций

Про контекст и this часто спрашивают на собеседованиях. Ответим подробно и разберёмся в нюансах.

Время чтения: 8 мин

Кратко

Скопировано

Грубо говоря, this — это ссылка на некий объект, к свойствам которого можно получить доступ внутри вызова функции. Этот this — и есть контекст выполнения.

Но чтобы лучше понять, что такое this и контекст выполнения в JavaScript, нам потребуется зайти издалека.

Сперва вспомним, как мы в принципе можем выполнить какую-то инструкцию в коде.

Выполнить что-то в JS можно 4 способами:

  • вызвав функцию;
  • вызвав метод объекта;
  • использовав функцию-конструктор;
  • непрямым вызовом функции.

Функция

Скопировано

Первый и самый простой способ выполнить что-то — вызвать функцию.

        
          
          function hello(whom) {  console.log(`Hello, ${whom}!`)}hello('World')// Hello, World!
          function hello(whom) {
  console.log(`Hello, ${whom}!`)
}

hello('World')
// Hello, World!

        
        
          
        
      

Чтобы выполнить функцию, мы используем выражение hello и скобки с аргументами.

Когда мы вызываем функцию, значением this может быть лишь глобальный объект или undefined при использовании 'use strict'.

Глобальный объект

Скопировано

Глобальный объект — это, так скажем, корневой объект в программе.

Если мы запускаем JS-код в браузере, то глобальным объектом будет window. Если мы запускаем код в Node-окружении, то global.

Строгий режим

Скопировано

Можно сказать, что строгий режим — неказистый способ борьбы с легаси.

Включается строгий режим с помощью директивы 'use strict' в начале блока, который должен выполняться в строгом режиме:

        
          
          function nonStrict() {  // Будет выполняться в нестрогом режиме.}function strict() {  'use strict'  // Будет выполняться в строгом режиме.}
          function nonStrict() {
  // Будет выполняться в нестрогом режиме.
}

function strict() {
  'use strict'
  // Будет выполняться в строгом режиме.
}

        
        
          
        
      

Также можно настроить строгий режим для всего файла, если указать 'use strict' в начале.

Значение this

Скопировано

Вернёмся к this. В нестрогом режиме при выполнении в браузере this при вызове функции будет равен window:

        
          
          function whatsThis() {  console.log(this === window)}whatsThis()// true
          function whatsThis() {
  console.log(this === window)
}

whatsThis()
// true

        
        
          
        
      

То же — если функция объявлена внутри функции:

        
          
          function whatsThis() {  function whatInside() {    console.log(this === window)  }  whatInside()}whatsThis()// true
          function whatsThis() {
  function whatInside() {
    console.log(this === window)
  }

  whatInside()
}

whatsThis()
// true

        
        
          
        
      

И то же — если функция будет анонимной и, например, вызвана немедленно:

        
          
          ;(function () {  console.log(this === window)})()// true
          ;(function () {
  console.log(this === window)
})()
// true

        
        
          
        
      

В приведённом выше примере вы можете заметить ; перед анонимной функцией. Дело в том, что существующий механизм автоподстановки точек с запятой (ASI) срабатывает лишь в определённых случаях, в то время как строка, начинающаяся с (, не входит в перечень этих случаев. Поэтому опытные разработчики зачастую добавляют ; в тех случаях, когда их код может быть скопирован и добавлен в существующий.

В строгом режиме — значение будет равно undefined:

        
          
          'use strict'function whatsThis() {  console.log(this === undefined)}whatsThis()// true
          'use strict'

function whatsThis() {
  console.log(this === undefined)
}

whatsThis()
// true

        
        
          
        
      

Метод объекта

Скопировано

Если функция хранится в объекте — это метод этого объекта.

        
          
          const user = {  name: 'Alex',  greet() {    console.log('Hello, my name is Alex')  },}user.greet()// Hello, my name is Alex
          const user = {
  name: 'Alex',
  greet() {
    console.log('Hello, my name is Alex')
  },
}

user.greet()
// Hello, my name is Alex

        
        
          
        
      

user.greet() — это метод объекта user.

В этом случае значение this — этот объект.

        
          
          const user = {  name: 'Alex',  greet() {    console.log(`Hello, my name is ${this.name}`)  },}user.greet()// Hello, my name is Alex
          const user = {
  name: 'Alex',
  greet() {
    console.log(`Hello, my name is ${this.name}`)
  },
}

user.greet()
// Hello, my name is Alex

        
        
          
        
      

Обратите внимание, что this определяется в момент вызова функции. Если записать метод объекта в переменную и вызвать её, значение this изменится.

        
          
          const user = {  name: 'Alex',  greet() {    console.log(`Hello, my name is ${this.name}`)  },}const greet = user.greetgreet()// Hello, my name is
          const user = {
  name: 'Alex',
  greet() {
    console.log(`Hello, my name is ${this.name}`)
  },
}

const greet = user.greet
greet()
// Hello, my name is

        
        
          
        
      

При вызове через точку user.greet() значение this равняется объекту до точки (user). Без этого объекта this равняется глобальному объекту (в обычном режиме). В строгом режиме мы бы получили ошибку «Cannot read properties of undefined».

Чтобы такого не происходило, следует использовать bind(), о котором мы поговорим чуть позже.

Вызов конструктора

Скопировано

Конструктор — это функция, которую мы используем, чтобы создавать однотипные объекты. Такие функции похожи на печатный станок, который создаёт детали LEGO. Однотипные объекты — детальки, а конструктор — станок. Он как бы конструирует эти объекты, отсюда название.

По соглашениям конструкторы вызывают с помощью ключевого слова new, а также называют с большой буквы, причём обычно не глаголом, а существительным. Существительное — это та сущность, которую создаёт конструктор.

Например, если конструктор будет создавать объекты пользователей, мы можем назвать его User, а использовать вот так:

        
          
          function User() {  this.name = 'Alex'}const firstUser = new User()firstUser.name === 'Alex'// true
          function User() {
  this.name = 'Alex'
}

const firstUser = new User()
firstUser.name === 'Alex'
// true

        
        
          
        
      

При вызове конструктора this равен свежесозданному объекту.

В примере с User значением this будет объект, который конструктор создаёт:

        
          
          function User() {  console.log(this instanceof User)  // true  this.name = 'Alex'}const firstUser = new User()firstUser instanceof User// true
          function User() {
  console.log(this instanceof User)
  // true
  this.name = 'Alex'
}

const firstUser = new User()
firstUser instanceof User
// true

        
        
          
        
      

На самом деле, многое происходит «за кулисами»:

  • При вызове сперва создаётся новый пустой объект, и он присваивается this.
  • Выполняется код функции. (Обычно он модифицирует this, добавляет туда новые свойства.)
  • Возвращается значение this.

Если расписать все неявные шаги, то:

        
          
          function User() {  // Происходит неявно:  // this = {};  this.name = 'Alex'  // Происходит неявно:  // return this;}
          function User() {
  // Происходит неявно:
  // this = {};

  this.name = 'Alex'

  // Происходит неявно:
  // return this;
}

        
        
          
        
      

То же происходит и в ES6-классах, узнать о них больше можно в статье про объектно-ориентированное программирование.

        
          
          class User {  constructor() {    this.name = 'Alex'  }  greet() {    /*...*/  }}const firstUser = new User()
          class User {
  constructor() {
    this.name = 'Alex'
  }

  greet() {
    /*...*/
  }
}

const firstUser = new User()

        
        
          
        
      

Как не забыть о new

Скопировано

При работе с функциями-конструкторами легко забыть о new и вызвать их неправильно:

        
          
          const firstUser = new User() // ✅const secondUser = User() // ❌
          const firstUser = new User() // ✅
const secondUser = User() // ❌

        
        
          
        
      

Хотя на первый взгляд разницы нет, и работает будто бы правильно. Но на деле разница есть:

        
          
          console.log(firstUser)// User { name: 'Alex' }console.log(secondUser)// undefined
          console.log(firstUser)
// User { name: 'Alex' }

console.log(secondUser)
// undefined

        
        
          
        
      

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

        
          
          function User() {  if (!(this instanceof User)) {    throw Error('Error: Incorrect invocation!')  }  this.name = 'Alex'}// илиfunction User() {  if (!new.target) {    throw Error('Error: Incorrect invocation!')  }  this.name = 'Alex'}const secondUser = User()// Error: Incorrect invocation!
          function User() {
  if (!(this instanceof User)) {
    throw Error('Error: Incorrect invocation!')
  }

  this.name = 'Alex'
}

// или

function User() {
  if (!new.target) {
    throw Error('Error: Incorrect invocation!')
  }

  this.name = 'Alex'
}

const secondUser = User()
// Error: Incorrect invocation!

        
        
          
        
      

Непрямой вызов

Скопировано

Непрямым вызовом называют вызов функций через call() или apply().

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

        
          
          function greet() {  console.log(`Hello, ${this.name}`)}const user1 = { name: 'Alex' }const user2 = { name: 'Ivan' }greet.call(user1)// Hello, Alexgreet.call(user2)// Hello, Ivangreet.apply(user1)// Hello, Alexgreet.apply(user2)// Hello, Ivan
          function greet() {
  console.log(`Hello, ${this.name}`)
}

const user1 = { name: 'Alex' }
const user2 = { name: 'Ivan' }

greet.call(user1)
// Hello, Alex
greet.call(user2)
// Hello, Ivan

greet.apply(user1)
// Hello, Alex
greet.apply(user2)
// Hello, Ivan

        
        
          
        
      

В обоих случаях в первом вызове this === user1, во втором — user2.

Разница между call() и apply() — в том, как они принимают аргументы для самой функции после this.

call() принимает аргументы списком через запятую, apply() же — принимает массив аргументов. В остальном они идентичны:

        
          
          function greet(greetWord, emoticon) {  console.log(`${greetWord} ${this.name} ${emoticon}`)}const user1 = { name: 'Alex' }const user2 = { name: 'Ivan' }greet.call(user1, 'Hello,', ':-)')// Hello, Alex :-)greet.call(user2, 'Good morning,', ':-D')// Good morning, Ivan :-Dgreet.apply(user1, ['Hello,', ':-)'])// Hello, Alex :-)greet.apply(user2, ['Good morning,', ':-D'])// Good morning, Ivan :-D
          function greet(greetWord, emoticon) {
  console.log(`${greetWord} ${this.name} ${emoticon}`)
}

const user1 = { name: 'Alex' }
const user2 = { name: 'Ivan' }

greet.call(user1, 'Hello,', ':-)')
// Hello, Alex :-)
greet.call(user2, 'Good morning,', ':-D')
// Good morning, Ivan :-D
greet.apply(user1, ['Hello,', ':-)'])
// Hello, Alex :-)
greet.apply(user2, ['Good morning,', ':-D'])
// Good morning, Ivan :-D

        
        
          
        
      

Связывание функций

Скопировано

Особняком стоит bind(). Это метод, который позволяет связывать контекст выполнения с функцией, чтобы «заранее и точно» определить, какое именно значение будет у this.

        
          
          function greet() {  console.log(`Hello, ${this.name}`)}const user1 = { name: 'Alex' }const greetAlex = greet.bind(user1)greetAlex()// Hello, Alex
          function greet() {
  console.log(`Hello, ${this.name}`)
}

const user1 = { name: 'Alex' }

const greetAlex = greet.bind(user1)
greetAlex()
// Hello, Alex

        
        
          
        
      

Обратите внимание, что bind(), в отличие от call() и apply(), не вызывает функцию сразу. Вместо этого он возвращает другую функцию — связанную с указанным контекстом навсегда. Контекст у этой функции изменить невозможно.

        
          
          function getAge() {  console.log(this.age);}const howOldAmI = getAge.bind({age: 20}).bind({age: 30})howOldAmI();//20
          function getAge() {
  console.log(this.age);
}

const howOldAmI = getAge.bind({age: 20}).bind({age: 30})

howOldAmI();
//20

        
        
          
        
      

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

Скопировано

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

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

        
          
          function greetWaitAndAgain() {  console.log(`Hello, ${this.name}!`)  setTimeout(() => {    console.log(`Hello again, ${this.name}!`)  })}const user = { name: 'Alex' }user.greetWaitAndAgain = greetWaitAndAgain;user.greetWaitAndAgain()// Hello, Alex!// Hello again, Alex!
          function greetWaitAndAgain() {
  console.log(`Hello, ${this.name}!`)
  setTimeout(() => {
    console.log(`Hello again, ${this.name}!`)
  })
}

const user = { name: 'Alex' }

user.greetWaitAndAgain = greetWaitAndAgain;
user.greetWaitAndAgain()

// Hello, Alex!
// Hello again, Alex!

        
        
          
        
      

При использовании обычной функции внутри контекст бы потерялся, и чтобы добиться того же результата, нам бы пришлось использовать call(), apply() или bind().

На практике

Скопировано

Саша Беспоясов советует

Скопировано

🛠 Гибкий, нефиксированный контекст в JS — это одновременно и удобно, и опасно.

Удобно это тем, что мы можем писать очень абстрактные функции, которые будут использовать контекст выполнения, для своей работы. Так мы можем добиться полиморфизма.

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

🛠 Всегда используйте 'use strict'.

Это относится скорее не конкретно к контексту, а в целом рекомендация для написания кода 🙂

Однако и с контекстом строгий режим позволит раньше обнаружить закравшуюся ошибку. Например:

В нестрогом режиме, если мы забудем new, name станет полем на глобальном объекте.

        
          
          function User() {  this.name = 'Alex'}const user = User()// window.name === 'Alex';// user === window
          function User() {
  this.name = 'Alex'
}

const user = User()
// window.name === 'Alex';
// user === window

        
        
          
        
      

В строгом мы получим ошибку, потому что изначально контекст внутри функции в строгом режиме — undefined:

        
          
          function User() {  'use strict'  this.name = 'Alex'}const user = User()// Uncaught TypeError: Cannot set property 'name' of undefined.
          function User() {
  'use strict'
  this.name = 'Alex'
}

const user = User()
// Uncaught TypeError: Cannot set property 'name' of undefined.

        
        
          
        
      

🛠 Всегда используйте new и ставьте проверки в конструкторе.

При использовании конструкторов всегда используйте new. Это обезопасит вас от ошибок и не будет вводить в заблуждение разработчиков, которые будут читать код после.

А для защиты «от дурака» желательно ставить проверки внутри конструктора:

        
          
          function User() {  if (!(this instanceof User)) {    throw Error('Error: Incorrect invocation!')  }  this.name = 'Alex'}const secondUser = User() // Error: Incorrect invocation!
          function User() {
  if (!(this instanceof User)) {
    throw Error('Error: Incorrect invocation!')
  }

  this.name = 'Alex'
}

const secondUser = User() // Error: Incorrect invocation!

        
        
          
        
      

🛠 Авто-байнд для методов класса.

В ES6 появились классы, но они не работают в старых браузерах. Обычно разработчики транспилируют код — то есть переводят его с помощью разных инструментов в ES5.

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

        
          
          class User {  name: 'Alex'  greet() {    console.log(`Hello ${this.name}`)  }}// this.name может быть undefined;// this может быть undefined.
          class User {
  name: 'Alex'
  greet() {
    console.log(`Hello ${this.name}`)
  }
}

// this.name может быть undefined;
// this может быть undefined.

        
        
          
        
      

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

На собеседовании

Скопировано
Задать вопрос в рубрику
🤚 Я знаю ответ

Сергей Власов  отвечает

Скопировано

В первом случае просто была вызвана функция, которая ничего не возвращает. Значение переменной будет равно undefined

        
          
          const animal = Animal() // ❌console.log(animal) // undefined
          const animal = Animal() // ❌
console.log(animal) // undefined

        
        
          
        
      

Во втором случае перед функцией Animal стоит оператор new. Функция Animal становится конструктором. Она выполняется, но так как this внутри функции не используется, и сама функция ничего не возвращает, то ничего не происходит. Результатом операции становится новый объект, который ссылается на функцию Animal как на конструктор. Этот объект присваивается переменной animal

        
          
          const animal = new Animal() // ✅
          const animal = new Animal() // ✅

        
        
          
        
      

Если Animal имеет вид:

        
          
          function Animal() {  this.name = 'Cat'}
          function Animal() {
  this.name = 'Cat'
}

        
        
          
        
      

То переменная animal, созданная с помощью new, будет иметь доступ к полю name:

        
          
          console.log(animal)// Animal { name: 'Cat' }// Если мы явно не возвращаем ничего из конструктора,// то получаем сам объект в качестве результата.
          console.log(animal)
// Animal { name: 'Cat' }
// Если мы явно не возвращаем ничего из конструктора,
// то получаем сам объект в качестве результата.

        
        
          
        
      

Рассмотрим возврат значения из конструктора

Скопировано

Обычно в функции-конструкторе не используется оператор return. Если return используется срабатывают два правила:

  1. При вызове return с объектом, вместо this вернётся этот объект.
  2. При вызове return с пустым или с примитивным значением, оно будет проигнорировано.

return с объектом возвращает этот объект, во всех остальных случаях возвращается this

        
          
          function Animal() {  this.foo = 'BARBARBAR'  return {    foo: 'bar' // ⬅️ возвращает этот объект  }}const animal = new Animal()console.log(animal.foo)// Вернет `bar`
          function Animal() {
  this.foo = 'BARBARBAR'
  return {
    foo: 'bar' // ⬅️ возвращает этот объект
  }
}

const animal = new Animal()
console.log(animal.foo)
// Вернет `bar`

        
        
          
        
      

А вот пример с примитивом после return:

        
          
          function Animal() {  this.foo = 'BARBARBAR'  return 'bar' // ⬅️ возвращает this}const animal = new Animal()console.log(animal.foo)// Вернет BARBARBAR
          function Animal() {
  this.foo = 'BARBARBAR'
  return 'bar' // ⬅️ возвращает this
}

const animal = new Animal()
console.log(animal.foo)
// Вернет BARBARBAR