Кратко
СкопированоГрубо говоря, 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
— это метод объекта 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
и вызвать их неправильно:
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
используется срабатывают два правила:
- При вызове
return
с объектом, вместоthis
вернётся этот объект. - При вызове
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