Кратко
СкопированоГрубо говоря, 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
.
На практике
Скопированосоветует Скопировано
🛠 Гибкий, нефиксированный контекст в JavaScript — это одновременно и удобно, и опасно.
Удобно это тем, что мы можем писать очень абстрактные функции, которые будут использовать контекст выполнения для своей работы. Так мы можем добиться полиморфизма.
Однако в то же время гибкий 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
Чтобы от этого защититься, можно использовать стрелочные функции, чтобы создать поля классов.
На собеседовании
Скопировано отвечает
СкопированоМетод Function
выполняет связывание (binding) функции с указанными параметрами: значением this
и набором аргументов.
Пример выполнения:
function targetFunc(x) { const id = this.id ?? 'неизвестно' console.log('id:', id, 'данные:', x)}const thisObj = {id: 42}const boundFunc = targetFunc.bind(thisObj, 7)
function targetFunc(x) { const id = this.id ?? 'неизвестно' console.log('id:', id, 'данные:', x) } const thisObj = {id: 42} const boundFunc = targetFunc.bind(thisObj, 7)
Для чего это может потребоваться?
Во-первых, чтобы указать и окончательно закрепить значение this
ещё до вызова самой функции. Например, это упрощает открепление метода от его родительского объекта.
Во-вторых, чтобы задать последовательность аргументов, которые при вызове функции будут предшествовать всем другим аргументам. Указанные аргументы будут связаны с функцией без необходимости передавать их при вызове. Такой подход называют частичным применением функции.
Как это работает?
При вызове .bind
создаётся новая функция-обёртка (bound function). Эта функция, хранит требуемый this
и приоритетные аргументы и защищена от возможности изменить эти параметры при вызове. В дальнейшем вызов функции-обёртки приводит к вызову целевой (target) функции.
Посмотрим как это работает на примере:
// Целевая функцияfunction show(a, b) { const name = this.name ?? 'неизвестно' console.log('имя:', name, 'данные:', a, b)}// Объект для указания thisconst character1 = { name: 'Pумпельштильцхен'}// Прямой вызов целевой функцииshow()// имя: неизвестно, данные: undefined undefined// Создаём функцию-обёртку, привязываем аргументы// `character1` как this и `true` как первый аргументconst boundShow = show.bind(character1, true)// Целевая функция использует привязанные параметрыboundShow()// имя: Pумпельштильцхен, данные: true undefinedboundShow(false)// имя: Pумпельштильцхен, данные: true falseboundShow.call({name: 'Риннебист'}, 2)// имя: Pумпельштильцхен, данные: true 2
// Целевая функция function show(a, b) { const name = this.name ?? 'неизвестно' console.log('имя:', name, 'данные:', a, b) } // Объект для указания this const character1 = { name: 'Pумпельштильцхен' } // Прямой вызов целевой функции show() // имя: неизвестно, данные: undefined undefined // Создаём функцию-обёртку, привязываем аргументы // `character1` как this и `true` как первый аргумент const boundShow = show.bind(character1, true) // Целевая функция использует привязанные параметры boundShow() // имя: Pумпельштильцхен, данные: true undefined boundShow(false) // имя: Pумпельштильцхен, данные: true false boundShow.call({name: 'Риннебист'}, 2) // имя: Pумпельштильцхен, данные: true 2
Единственное исключение, когда this
не будет равен аргументу при связывании внутри целевой функции, — вызов функции-обёртки как конструктора:
function Pixel(color) { this.color = color console.log('color:', this.color, 'hidden:', this.hidden)}// Создаём функцию-обёрткуconst MyPixel = Pixel.bind({hidden: true}, 'white')// Прямой вызов целевой функцииPixel('red')// color: red hidden: undefined// Вызов функции-обёрткиMyPixel('black')// color: white hidden: true// Вызов функции-обёртки как конструктораnew MyPixel('blue')// color: white hidden: undefined
function Pixel(color) { this.color = color console.log('color:', this.color, 'hidden:', this.hidden) } // Создаём функцию-обёртку const MyPixel = Pixel.bind({hidden: true}, 'white') // Прямой вызов целевой функции Pixel('red') // color: red hidden: undefined // Вызов функции-обёртки MyPixel('black') // color: white hidden: true // Вызов функции-обёртки как конструктора new MyPixel('blue') // color: white hidden: 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