Кратко
СкопированоГрубо говоря, 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