Кратко
СкопированоГрубо говоря, 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.
Чтобы от этого защититься, можно использовать стрелочные функции, чтобы создать поля классов.
На собеседовании
СкопированоЭто партнёрская рубрика, мы выпускаем её совместно с сервисом онлайн-образования Яндекс Практикум. Приносите вопрос, на который не знаете ответа, в задачи, мы разложим всё по полочкам и опубликуем. Если знаете ответ, присылайте пулреквест на GitHub.
Это вопрос без ответа. Вы можете помочь! Почитайте о том, как контрибьютить в Доку.