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. В нестрогом режиме при выполнении в браузере 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"

При работе с функциями-конструкторами легко забыть о 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")greet.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")

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" }greetWaitAndAgain.call(user)// Hello, Alex!// Hello again, Alex!
          function greetWaitAndAgain() {
  console.log(`Hello, ${this.name}!`)
  setTimeout(() => {
    console.log(`Hello again, ${this.name}!`)
  })
}

const user = { name: "Alex" }

greetWaitAndAgain.call(user)

// 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.

        
        
          
        
      

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