Перейти к содержимому
Alex Kovalev

Разбираемся с контекстом THIS в JS

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

4мин чтения

Меня всегда путал ‘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, живут в глобальной области видимости, а не в лексическом контексте.

определенаvarlet и 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 alex

bind задаёт 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.

Чирикну, когда выйдет новый пост

Одно письмо на каждый новый пост, обычно по пятницам. Больше ничего, а отписаться можно в любой момент.

Удобнее читать в RSS-ридере? Есть RSS-лента.

© Alex Kovalev. Все права защищены. · Политика конфиденциальности