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

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

Кратко

Секция статьи "Кратко"

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

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

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

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

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

Функция

Секция статьи "Функция"

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

        
          
          function hello(whom) {  console.log(`Hello, ${whom}!`)}hello("World") // Hello, World!// Чтобы выполнить функцию, мы используем выражение `hello`// и скобки с аргументами.
          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"  // Будет выполняться в строгом режиме.}// Также можно настроить строгий режим для всего файла,// если указать '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// user.greet — это метод объекта user.
          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// В обоих случаях// в первом вызове this === user1,// во втором — user2.
          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.

        
          
          function greet(greetWord, emoticon) {  console.log(`${greetWord} ${this.name} ${emoticon}`)}const user1 = { name: "Alex" }const user2 = { name: "Ivan" }// .call() принимает аргументы списком через запятую:greet.call(user1, "Hello,", ":-)") // Hello, Alex :-)greet.call(user2, "Good morning,", ":-D") // Good morning, Ivan :-D// .apply() же — принимает массив аргументов: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" }

// .call() принимает аргументы списком через запятую:
greet.call(user1, "Hello,", ":-)") // Hello, Alex :-)
greet.call(user2, "Good morning,", ":-D") // Good morning, Ivan :-D

// .apply() же — принимает массив аргументов:
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// В строгом мы получим ошибку,// потому что изначально контекст внутри функции// в строгом режиме — undefined.function User() {  "use strict"  this.name = "Alex"}const user = User()// Uncaught TypeError: Cannot set property 'name' of undefined.
          // В нестрогом режиме, если мы забудем new,
// name станет полем на глобальном объекте.

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.

        
        
          
        
      

🛠 Всегда используйте 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.

        
        
          
        
      

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