Разбираемся с контекстом THIS в JS
Как работает определение переменных в JS? Почему функции наследуют this?

Меня всегда путал ‘this’. Ладно, признал. Вот вопросы, на которые я хочу ответить в этой статье:
- Как он задаётся?
- почему он отличается для стрелочной функции?
- и почему обычные функции его не получают?
THIS не относится к КОНТЕКСТУ (лексической области видимости)
Вы должны знать, что когда вы пишете функцию вот так:
var name = "alex"
function greet() { console.log("hi "+this.name);}
greet() // что здесь вообще происходит?Что выведет?Обычный <script> в браузере, не модуль. Для остальных так же.
Показать вывод
hi alexФункцию вызвали «голой» в обычном скрипте, поэтому this — это window. Глобальный var прилипает к window, так что this.name равно "alex". В модуле это упадёт: там this — undefined.
Этот же код не заработает в Java, C или PHP. На самом деле и в JS он бы не вывел alex, если бы я не определил name через var. Это небольшое отступление, но переменные, определённые через var, живут в глобальной области видимости, а не в лексическом контексте.
| определена | var | let и const |
|---|---|---|
| глобально | глобальная область видимости | глобальный лексический контекст |
| в функции | область видимости функции | лексический контекст в рамках функции |
| в блоке | область видимости объемлющей функции (или глобальная) | лексический контекст в рамках блока |
Вот быстрая демонстрация разницы:
if (true) { var v_name = "alex" let l_name = "alex"}
console.log(v_name) // "alex"console.log(l_name) // ReferenceError: l_name is not definedНо почему this.name возвращает глобально объявленный var? Потому что в нестрогом окружении, вроде браузера, глобально заданный ‘this’ ссылается на объект window, а var, определённая в глобальной области видимости, попадает именно туда. Но на самом деле всё дело в том, что весь код браузера технически выполняется внутри объекта window, и если посмотреть с этой стороны, становится довольно очевидно, что переменные должны прилипать к нему, а функции — наследовать его.
вот несколько источников для дальнейшего чтения https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Closures https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/let#temporal_dead_zone_tdz
THIS — это синтаксический сахар
Вы когда-нибудь задумывались, как объекты заняли своё место в js? как js обращается с объектами? Взгляните на это: Давайте определим простой объект User
class User { name = ""; constructor(name) { this.name = name; } greet() { console.log("hi "+this.name) }}Когда мы вызываем функцию у экземпляра этого объекта, на самом деле происходит вот что:
const alex = new User("alex")alex.greet()
// эквивалентно:alex.greet.call(alex)// илиalex.greet.apply(alex, [])из этого простого примера уже можно понять, что this — это тот объект, на котором вызвали функцию, то, что стоит слева от точки. Нет объекта — нет и this. Ему всё равно, где функцию написали, важно только, как её вызвали.
так что следующий код не поздоровается с alex
const name = "alex";
function greet() { console.log("hi "+this.name);}
greet()
// эквивалентноgreet.call(undefined);Что выведет?
Показать вывод
hi ␣this всё ещё window, но const к нему не прилипает. Поэтому this.name читает window.name — настоящее свойство браузера, по умолчанию пустая строка. Не undefined. Браузеры.
не заработает и вот это
function setup(){ const name = "alex";
function greet() { console.log("hi "+this.name); }}
greet()
// было бы эквивалентно (если бы greet была видна здесь)greet.call(undefined);Что выведет?
Показать вывод
Uncaught ReferenceError: greet is not definedВопрос с подвохом. greet существует только внутри setup(), так что снаружи вызывать нечего. До путаницы с this дело даже не доходит.
Чтобы лучше понять, что я только что написал, пройдитесь по следующему примеру, пытаясь предсказать, что выведет программа
Вот наша заготовка: простой класс user со свойством this.name и двумя вариантами функции greet, одна обычная функция, вторая — стрелочная
class User { name = ""; constructor(name) { this.name = name; } normalGreet(){ console.log("hi "+this.name) } arrowGreet = () => { console.log("hi "+this.name) }}
let user = new User("alex");let miri = new User("miri")Первый вопрос
let alex = new User("alex")
// первыйalex.normalGreet();alex.arrowGreet();Надеюсь, не испорчу сюрприз, если скажу, что обе работают нормально и печатают наше приветственное сообщение для alex - hi alex
второй вопрос
let alex = new User("alex");
let normal = alex.normalGreet;let arrow = alex.arrowGreet;
normal();arrow();Что выведет?
Показать вывод
Uncaught TypeError: Cannot read properties of undefined (reading 'name')Код внутри class всегда в strict mode, поэтому оторванный normal() получает this === undefined вместо window, и чтение .name из него падает. До arrow() очередь не доходит.
Вот тут и начинается проблема, можете заметить, в чём дело?
что происходит, когда мы присваиваем функцию переменной перед тем, как её вызвать?
Как оказывается, обычная функция падает с TypeError: Cannot read properties of undefined (reading 'name'), теперь подумайте почему. Мы сказали, что у обычной функции динамический this, который вычисляется во время выполнения. Подумайте об этом: мы вызываем функцию greet как бы ни на чём, у неё нет контекста this, так как же она может обратиться к this.name? (Тело класса всегда выполняется в строгом режиме, так что this здесь просто undefined, а не тихо подменяется на window.) Она падает ещё до того, как до arrow() дойдёт очередь.
В случае стрелочной функции, чей this задаётся в момент определения, контекст остаётся неизменным даже после присвоения другой переменной. И это как раз и есть разница между обычными и стрелочными функциями.
Вы можете спросить себя: мне правда нужно это знать? где я вообще с этим столкнусь? Ответ зависит от того, как вы используете ООП в JS или в любом другом фреймворке. Например
<button onclick={alex.normalGreet}>Normal Greet</button><button onclick={alex.arrowGreet}>Arrow Greet</button>Код выше можно рассматривать как присвоение функции свойству объекта!
// это в точности повторяет наш второй примерbuttonElement.onclick = alex.normalGreetтак что для работы нашего tsx нужно либо использовать стрелочную функцию в объекте User, либо передавать её вот так
<button onclick={()=>alex.normalGreet()}>Normal Greet</button>Смотрите также: Livewire для WP, где инлайновый onclick передаёт обработчику this (саму кнопку).
Третий раздел просто для развлечения, если хотите лучше понять, как работают bind, call и apply:
let miri = new User("miri")
let normal = alex.normalGreet;let arrow = alex.arrowGreet;
normal = normal.bind(miri);arrow = arrow.bind(miri);
normal();arrow();Что выведет?
Показать вывод
hi mirihi alexbind задаёт this обычной функции, так что normal теперь здоровается с miri. Стрелочная функция уже взяла this там, где её написали, — у экземпляра alex, и bind его не отнимет.
Обычная функция выведет hi miri. bind возвращает новую функцию, у которой this намертво прибит к тому, что вы передали, и обычная функция с радостью его принимает.
Стрелочная функция по-прежнему выведет hi alex. У стрелочных функций вообще нет своего this — они берут его оттуда, где были определены, а для поля класса это создаваемый экземпляр. bind, call и apply могут сколько угодно пытаться подсунуть ей другой this, стрелочная функция их просто игнорирует. Она родилась принадлежащей alex и умрёт принадлежащей alex.
Итак, вернёмся к моим вопросам из начала. this задаётся в момент вызова тем, что стоит слева от точки (или через call, apply и bind). Стрелочные функции отличаются тем, что своего this у них никогда не бывает — они одалживают его у того места, где были написаны. А когда вы вызываете функцию ни на чём, задавать нечего, и вы получаете undefined (или window, если вы в нестрогом режиме и вам не повезло).
Всё ещё немного запутались? Я тоже. Просто меньше, чем в начале.
5 сниппетов, 5 догадок. Счёт появится здесь.
Комментарии
Комментарии живут в GitHub Discussions (через giscus), так что для участия нужен аккаунт GitHub.