Кратко
Секция статьи "Кратко"Грубо говоря, 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
— это метод объекта 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
значение 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.
Чтобы от этого защититься, можно использовать стрелочные функции, чтобы создать поля классов.