* скорее всего, я что-нибудь да упустил, но уверен, в комментариях мне это подскажут

Эту статью я пишу для своих личных нужд. Планируется, что она будет содержать в себе ответы на все вопросы, которые мне задают студенты на эту тему. Если она пригодится кому-то ещё — здорово.

image

Содержание.

  1. Введение
  2. Заблуждения о this
  3. Как определить значение this

Введение


Ключевое слово this — одна из наиболее запутывающих особенностей языка JavaScript. Пришедшее из Java, оно было призвано помочь в реализации ООП. Я какое-то время писал на Java, и должен сказать, что за это время у меня, может быть, один раз возникло сомнение, чему равняется this в конкретном месте кода. В JavaScript такие сомнения могут возникать каждый день — по крайней мере, до того момента, как выучишь несколько простых, но неочевидных правил.

Заблуждения о this


Существует несколько распространённых заблуждений относительно этого ключевого слова. Я хочу их быстренько развеять перед тем, как перейти к сути.

this — это лексический контекст.

Такое впечатление часто возникает у новичков. Им кажется, что this — это объект, в котором, как свойства, хранятся все переменные в данной области видимости. Это заблуждение происходит из того, что в одном конкретном случае это, грубо говоря, так. Если мы находимся на самом верхнем уровне, то this равняется window (в случае обычного скрипта в браузере). А как мы знаем, все переменные, объявленные на верхнем уровне, доступны как свойства window.

В общем случае это неправда. Это легко проверить.

function test(){
  const a = "Кто прочитал этот текст в консоли, тот скоро умрёт";
  console.log(this.a);
}
test(); // не волнуйтесь, никто не умрёт

this — это объект, которому принадлежит метод

Опять же, это правда во многих конкретных случаях, но не всегда. Важно то, каким способом вызывается функция, а не то, является ли она свойством какого-то объекта. Это понятно даже из очень простой логики: предположим, одна и та же функция является свойством одновременно двух объектов.

const f = function(){
  console.log(this);
}

const object1 = {
  method: f
};

const object2 = {
  method: f
};

Так какой же из этих объектов будет её this'ом?

Важно! Даже если объект создан с помощью классов ES6, это совершенно не гарантирует, что у его метода всегда будет нужный this. Пусть вас не вводит в заблуждение сходство с классами из «нормальных» языков.

this — это джедайская техника, которую, изучив, нужно использовать везде

По возможности лучше избегать употребления this. Единственный случай, когда оно полностью обосновано — это ООП. В прочих местах употребление this — это не то чтобы зло, но, как правило, излишество, запутывающее код. По сути this — это дополнительный, «минус первый» аргумент функции, который передаётся туда сложным, неочевидным новичку путём. С высокой вероятностью его можно заместить обычным аргументом, и станет только лучше.

Как определить значение this


Здесь я постараюсь дать строгий и лаконичный алгоритм, с помощью которого даже неискушённый кодер сумеет понять, чему равняется this в его конкретном случае. Более многословные пояснения буду спрятаны под спойлеры, дабы не захламлять визуальное пространство.

  1. Мы находимся внутри функции?


    Комментарий
    В коде верхнего уровня (не находящемся внутри никакой функции) this всегда ссылается на глобальный объект. В случае обычного скрипта в браузере это — объект window. Но вообще случаи бывают разные.
  2. Мы находимся внутри стрелочной функции?

    • Да: значение this такое же, как и в функции на уровень выше (т.е. содержащей данную). Вернитесь на предыдущий шаг и повторите алгоритм для неё. Если же функция не содержится ни в какой другой, this — глобальный объект.
    • Нет: смотрим следующий пункт.

    Комментарий
    Одна из основных особенностей стрелочных функций — это так называемый «лексический this». Это значит, что значение this в стрелочной функции определяется исключительно тем, где (в каком лексическом контексте) она была создана, и никак не зависит от того, как впоследствии она была вызвана. Иногда это не то, что нам нужно, но чаще всего это делает стрелочные функции очень удобными и предсказуемыми.
  3. Эта функция вызвана как конструктор (с помощью оператора new)?

    • Да: this ссылается на новый объект, находящийся «в процессе конструкции».
    • Нет: смотрим следующий пункт.

    Комментарий
    Пожалуй, стоит отдельно оговорить случай, когда речь идёт о конструкторе унаследованного ES6-класса. Тогда до вызова super() значения у this нет (обращение к нему вызовет ошибку), а после вызова super() он равняется новому объекту родительского класса.
  4. Эта функция создана с помощью метода bind?

    • Да: значение this равняется значению первого аргумента, который мы передали в метод bind при создании данной функции.
    • Нет: смотрим следующий пункт.

    Комментарий
    Метод bind создаёт копию функции, зафиксировав для неё this и, опционально, несколько первых аргументов. На самом деле при этом создаётся не просто копия функции, а, цитирую, «экзотический объект BoundFunction». Экзотичность его проявляется, в частности, в том, что повторным вызовом bind мы уже не сможем изменить this. Поэтому, строго говоря, ответ в этом пункте надо было сформулировать так: если да, то this равняется первому аргументу первого вызова bind, который привёл к созданию данной функции.
  5. Эта функция передана куда-то в качестве колбэка или обработчика?

    • Да: одному Господу известно, чему будет равен this при её вызове. Идите читать документацию по той штуке, которая её станет вызывать.
    • Нет: смотрим следующий пункт.

    Комментарий
    У не стрелочной и не связанной (bound) функции значение this зависит от обстоятельств, в которых она была вызвана. Если вы не вызываете её лично, а передаёте куда-то, то в качестве this может быть или не быть подставлено неизвестное вам значение.

    Примеры:

    `use strict`
    
    document.addEventListener('keydown', function(){
      console.log(this);
    }); // в этом случае this === document. при срабатывании обработчиков DOM-событий this равняется currentTarget
    
    [1, 2, 3].forEach(function(){
      console.log(this);
    }); // а в этом случае никакого специального значения не будет, будет undefined. почему? об этом в самом конце.
    
  6. Эта функция вызвана с помощью метода apply или call?

    • Да: в таком случае this равняется первому аргументу, переданному соответствующему методу.
    • Нет: смотрим следующий пункт.

    Комментарий
    Ещё один способ явно задать this. Точнее, два. Однако в плане this разницы между apply и call нет никакой, разница только в том, как передаются остальные аргументы.
  7. Эта функция получена как значение свойства объекта и сразу же вызвана?

    • Да: this равняется вышеупомянутому объекту.
    • Нет: смотрим следующий пункт.

    Комментарий
    Собственно, из этого механизма (а также — из опыта работы с другими языками) растут ноги у убеждения, что "this — это объект, чей метод мы вызвали". Пожалуй, я просто напишу код.

    'use strict';
    
    const object1 = {
      method: function(){
        console.log(this);
      }
    }
    
    const object2 = {
      method: object1.method
    }
    
    object1.method(); // в консоли будет object1 - мы получили функцию как свойство этого объекта и немедленно вызвали
    object1['method'](); // аналогичный результат. этот механизм не специфичен для точечной нотации
    object2.method(); // в консоли будет object2 - метод "не помнит", в каком объекте он был создан, ему важно только у какого объекта он вызван
    
  8. Код выполняется в строгом режиме? ('use strict', ES6 модуль)

    • Да: this равняется undefined.
    • Нет: this равен глобальному объекту.

    Комментарий
    Если мы дошли до этого пункта, значит, this не задан ни одним из механизмов, которые позволяют его задать. Существуют различные заблуждения относительно того, как ещё может передаваться this. Например, на собеседованиях мне часто говорят вот такую вещь:

    const obj = { 
      test: function(){
        (function(){
          console.log(this);
        })(); //немедленно вызываемая функция внутри другой функции
      }
    }
    
    obj.test(); // мне говорят, что в консоль выведется obj. это неправда 
    

    Или, как я уже говорил в секции «заблуждения», многие считают, что если функция является методом объекта, созданного с помощью классов ES6, то уж в ней-то this всегда будет равен этому объекту. Это тоже неправда.

    Так вот, если мы дошли до этого пункта, значит, прочие обстоятельства вызова нашей функции не имеют значения. И всё сводится к тому, находимся мы в строгом режиме или нет.

    Исторически в качестве «дефолтного» this в такие функции передавался глобальный объект. Позже этот подход был признан небезопасным. В ES5 появился строгий режим, исправляющий многие проблемы более ранних версий ECMAScript. Он включается директивой 'use strict' в начале файла или функции. В таком режиме «дефолтное» значение this — это undefined.

    В ES6 модулях строгий режим включен по умолчанию.

    Также существуют другие механизмы включения строгого режима, например, в NodeJS строгий режим для всех файлов можно включить флагом --use-strict.


Вот, в общем-то, и всё. Определение значения this, конечно, менее просто, чем хотелось бы, но и не так сложно, как могло показаться. Выучите этот алгоритм, как таблицу умножения — и у вас больше никогда не возникнет проблем с this. Такие дела.

P.S. Пользователь Aingis подсказал, что в случае использования конструкции with объект, переданный в неё в качестве контекста, подменяет собой глобальный объект. Пожалуй, я не стану вносить это в свой классификатор, потому что шанс встретить with в 2019+ году довольно мал. Но в любом случае это интересный момент.

P.P.S. Пользователь rqrqrqrq подсказал, что у new выше приоритет, чем у bind. Соответствующая правка в классификатор уже внесена. Спасибо!

Комментарии (30)


  1. Sirion Автор
    19.08.2019 14:13
    -1

    Подозреваю, что этот вопрос не дойдёт до адресатов, но мне очень интересно, за что минусы. Если я где-то написал лажу, пожалуйста, скажите мне, где именно. Я на эту статью ещё не раз буду ссылаться. Я не хочу, чтобы в ней была написана лажа.


    1. mSnus
      20.08.2019 02:48

      Здесь часто минусуют материалы, которые "не для самых крутых". Такой вот снобизм!)


    1. flancer
      20.08.2019 08:37

      Я заметил, что многие свежие статьи сначала уходят в минуса, а потом — в плюса. Допускаю, что "принципиальных минусаторов" меньше, но они реагируют первыми (может с ними в основной жизни мало кто хочет иметь дело из-за их вредности характера, поэтому они больше в интернетах сидят). Со временем появляется большее количество тех, кто может оценить публикацию положительно (что и понятно, люди с мозгами работу работают, а не только на Хабре сидят). Судя по вашему комменту, коллега, вашу статью увели в минуса. Как раз таки те самые, которые вечно недовольны, но сами не знают чем. А судя по тому, что сейчас ваша статья в плюсах, Хабр, в целом, довольно дружелюбная среда ;)


      1. Sirion Автор
        20.08.2019 09:02

        Да нет, статья, кажется, не успела уйти в минус. Но в некоторый момент процент минусов оказался относительно велик, и я удивился. Не мог понять, в чём её противоречивость. Вопрос банальности я как-то забыл рассмотреть)


    1. Vahman
      20.08.2019 21:28

      Имхо всё просто. Многие минуснули глядя на заголовок, не прочитав статью. Так как статей про this в js великое множество — решили понизить мотивацию к повторам.


  1. Zenitchik
    19.08.2019 14:43
    +1

    Я минусы не ставил (и если бы мог — всё равно бы не ставил), но мне тоже давно надоело читать тысячу раз разжёванные вещи. Если новичёк не может нагуглить статью N-летней давность (в которой написано всё то же самое) — это на сто процентов его личные половые трудности.


    1. Sirion Автор
      19.08.2019 14:47

      Ну, я не новичок, но не смог найти статью, в которой всё это было бы разжёвано с достаточной чёткостью. Допустим, я пробовал давать ссылку на MDN, но всё равно оставалось какое-то недопонимание. Я решил использовать свой блестящий талант педагога и публициста (ирония), чтобы вероятность этого недопонимания свести к минимуму.

      Но если минусуют за это, то ок, я не против)


      1. Vahman
        20.08.2019 21:30
        +1

        На самом деле, в любой годной книге по js достаточно хорошо описано что такое this. Беда в том что многие нынешние js разработчики не удосужились прочитать хотя бы одну


    1. flancer
      20.08.2019 08:44

      А кто вас заставляет читать то, что вы уже читали тысячу раз, коллега? Научитесь по заголовку, предисловию и стилю изложения определять, насколько лично для вас будет интересна та или иная информация и как вам её читать — бегло, наискось или отложить на послеобеда и прочесть вдумчиво. Если осознаёте, что читаете банальные вещи, ближе к концу их прочтения, то это на сто процентов ваши личные половые трудности.


      Лично я этот справочный материал добавил в закладки, хотя пробежал взглядом наискось. Хорошая систематизация, надеюсь, что ещё и полная.


      1. Zenitchik
        20.08.2019 14:46

        Я и не читал. Просто мне не нравится, что Хабр заполняется перепечатками ранее изложенного. Пусть даже невольными.


        1. flancer
          20.08.2019 15:22

          А мне нравится, когда известная мне информация подана в удобном для её осознания виде. Я 20 лет в программировании и уже за ненадобностью забыл больше, чем знаю. Иногда бывает нужно освежить старые знания. В этом случае я очень ценю, когда информацию можно "загрузить" в голову максимально быстро и максимально полно, а не рыскать по всему инету в поисках что и как.


          Помните, коллега, как Тринити в Матрице загружала в мозг руководство по управлению вертолётом? А теперь представьте, что ей пришлось шариться по всей Сети в поисках деталей.


          Плюс ещё один немаловажный момент — Хабр очень хорошо индексируется Гуглом и в выдаче стоит на верхних позициях. Иногда хорошая перепечатка на Хабре гуглится быстрее, чем даже оригинальная информация ;)


  1. codecity
    19.08.2019 14:43

    одному Господу известно, чему будет равен this при её вызове

    Вот за это и не любят JS. Т.е. нужно в голове держать целый такой алгоритм из 8 пунктов, чтобы в отдельных случаях знать поведение кода.

    Вангую замену this на однозначный self или что-то подобное. Как пришлось добавлять === и !==.


    1. Aingis
      19.08.2019 15:05

      Да не нужно никаких восьми пунктов сразу. Обычно это очевидно, если прочитал хоть какую-то документацию про JS.
      — Вызываешь метод, call/apply — this указывает на заданный объект. Всё прозрачно.
      — Передаёшь колбэк (ссылку на функцию в общем случае, включая IIFE) — в this будет что угодно (скорее всего undefined/глобальный; в DOM-обработчиках — элемент, но это легаси).
      — Забиндил (.bind(), стрелочная функция, class properties) — будет забинденный. Стрелочные функции прозрачны для this.
      — В коде верхнего уровня редко кто вызывает this (обычно window/global в зависимости окружения), да и для этого случая ввели globalThis.


      Автор, кстати, упустил ещё экзотический случай с with({}). Но это тоже не тот код, который встретишь в продакшене.


      1. Sirion Автор
        19.08.2019 15:09

        Автор молод и не застал использование with в проде. А что там будет, кстати?) неужто this воспримется как имя свойства?


        1. Aingis
          19.08.2019 16:10

          Да то же самое что и глобальный (нестрогий, само собой), только вместо глобального объекта будет тот, что указан в with().


    1. Vahman
      20.08.2019 21:35

      Да, за это и не любят js. Сначала его не изучили, ничего не поняли, увидели что он не похож на другой, привычный яп, получили неожиданные, по их мнению, результаты, удивились, и решили что js неправильный. За это и не любят, за то что его не знают


  1. rqrqrqrq
    19.08.2019 16:14

    Вызов функции как конструктора с new приоритетнее, чем вызовы c bind, call и apply.
    cheatsheet: new > bind > call/apply > obj.method()


    1. Sirion Автор
      19.08.2019 16:15
      +1

      Так. А я не очень могу сообразить, в каком случае у нас может возникнуть вопрос о приоритетах new и call/apply. Приведёте пример?


      1. rqrqrqrq
        19.08.2019 16:46

        Вызов уже забинденой функции с помощью call не изменит this. Не верно сформулировал мысль.


        1. Sirion Автор
          19.08.2019 16:51

          Да, это подразумевается в моём определителе зисов. Бинд идёт третьим пунктом, раньше всего остального, кроме стрелочных функций и отсутствия функций.

          new > bind

          Слушайте, а вы имеете в виду, что если забинденную функцию вызвать как конструктор, у неё this всё-таки изменится?


          1. Sirion Автор
            19.08.2019 17:37

            Да, действительно. Вот это для меня новость. Отлично, значит, день прошёл не зря) сейчас внесу правки.


  1. kobor
    20.08.2019 08:15
    -2

    Это увлекательно изучать язык программирования методом «тыка», но всётаки лучше, как здесь уже советовали, изучить документацию.
    Есть хороший ресурс:
    HTML. The language for building web pages
    там есть раздел:
    JavaScript Tutorial
    с подробным описанием всех элементов языка и итерактивными примерами для каждого элемента.
    И в частности по мучающим вас вопросам:
    www.w3schools.com/js/js_this.asp
    www.w3schools.com/js/js_function_call.asp
    www.w3schools.com/js/js_function_apply.asp
    www.w3schools.com/js/js_const.asp


    1. Sirion Автор
      20.08.2019 08:17

      Хочу уточнить: лучше для кого? Я всё это уже читал, и не только это. Студенты, как показывает практика, всё равно что-то не догоняют, к тому же не все знают аглицкий.


      1. kobor
        20.08.2019 15:02

        Потому и «недогоняют» что не знают терминалогию, а она вся английская. Статьи и коментарии на HABR-е могли бы им в этом помочь но многие десь пишут на каком то слэнге какбудто «ботают по фене». Сплошные фьючерсы, тучерсы забиненные… вместо использования професиональных терминов в их исходном англиском написании или русских аналогов, раз уж это пишут наруском.
        Я сталкивался с вашей проблемой неопределённости THIS при написании сложнонавороченных объектов. Её удалось решить введением в констрактор объектов локальной переменной SELF которой присваивал значение THIS и использовал её в теле методов объекта.
        Примерно так:

        function createObjectxxx(a, b, c){
        this.properti1 = null;
        this.properti2 = null;
        this.properti3 = null;

        var self = this;
        this.set = function (d, e, f) {
        self.properti1 = d;
        self.properti2 = e;
        self.properti3 = f;
        };

        this.init = function(){
        self.set (a, b, c)
        }

        this.init();
        }


        1. Sirion Автор
          21.08.2019 08:17

          А есть какие-то причины, по которым set и init не лежат в прототипе?


          1. kobor
            21.08.2019 13:56

            Это просто «виртуальный» пример — как избежать неоднозначности THIS (просто схема). Вместо ключевого слова THIS, имеющего «нехорошую репутацию», используем переменную указатель на объект.
            Это актуально для сложных объектов содержащих другие объекты со сложным взаимодействием.
            В прототип можно запихнуть всё что угодно, но кладут туда лишь то, что повторяется в других объетах. А если в этих функциях много навороченных действий, в том числе с DOM-объектами и базами данных, наличие какй то «заготовки» в прототипе никакого выиргыша не даёт.


            1. Sirion Автор
              21.08.2019 16:15

              А какой выигрыш даёт написание кода в вашем стиле? Я вижу героическое преодоление собственноручно созданных трудностей.


    1. Aingis
      20.08.2019 11:33

      w3schools не раз был грешен своими неточностями. Лучше уж на классику ссылку давать вроде learn.javascript.ru или «Javascript. Подробное руководство» Дэвида Флэнагана.


      На английском есть MDN (на русский далеко не всё переведено), 2ality.com Акселя Раушмайера, в частности «JavaScript for impatient programmers» с лаконичным, но всеохватывающим описанием, и многое другое, вплоть до цикла видео лекций Дугласа Крокфорда.


  1. AstarothAst
    20.08.2019 08:35
    -1

    Шел 2019 год, в мире java script все еще ищут точное значение this… Нет, определенно с этим языком что-то очень сильно пошло не так.


  1. bopoh13
    20.08.2019 18:43

    Тоже самое в слайдах: urfu-2016.github.io/javascript-slides/05-this